/*
 * 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.FacebookException
import com.facebook.FacebookSdk
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

/**
 * 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 WorkQueue
@JvmOverloads
constructor(
    private val maxConcurrent: Int = DEFAULT_MAX_CONCURRENT,
    private val executor: Executor = FacebookSdk.getExecutor()
) {
  private val workLock = ReentrantLock()
  private var pendingJobs: WorkNode? = null
  private var runningJobs: WorkNode? = null
  private var runningCount = 0

  companion object {
    const val DEFAULT_MAX_CONCURRENT = 8
    private fun assert(condition: Boolean) {
      if (!condition) {
        throw FacebookException("Validation failed")
      }
    }
  }

  @JvmOverloads
  fun addActiveWorkItem(callback: Runnable, addToFront: Boolean = true): WorkItem {
    val node = WorkNode(callback)
    workLock.withLock { pendingJobs = node.addToList(pendingJobs, addToFront) }
    startItem()
    return node
  }

  fun validate() {
    // Verify that all running items know they are running, and counts match
    workLock.withLock {
      var count = 0
      if (runningJobs != null) {
        var walk = runningJobs
        do {
          checkNotNull(walk)
          walk.verify(true)
          count++
          walk = walk.next
        } while (walk !== runningJobs)
      }
      assert(runningCount == count)
    }
  }

  private fun startItem() {
    finishItemAndStartNew(null)
  }

  private fun finishItemAndStartNew(finished: WorkNode?) {
    var ready: WorkNode? = null
    workLock.lock()

    if (finished != null) {
      runningJobs = finished.removeFromList(runningJobs)
      runningCount--
    }
    if (runningCount < maxConcurrent) {
      ready = pendingJobs // Head of the pendingJobs queue
      if (ready != null) {
        // The Queue reassignments are necessary since 'ready' might have been
        // added / removed from the front of either queue, which changes its
        // respective head.
        pendingJobs = ready.removeFromList(pendingJobs)
        runningJobs = ready.addToList(runningJobs, false)
        runningCount++
        ready.isRunning = true
      }
    }
    workLock.unlock()
    if (ready != null) {
      execute(ready)
    }
  }

  private fun execute(node: WorkNode) {
    executor.execute {
      try {
        node.callback.run()
      } finally {
        finishItemAndStartNew(node)
      }
    }
  }

  private inner class WorkNode constructor(val callback: Runnable) : WorkItem {
    var next: WorkNode? = null
      private set
    private var prev: WorkNode? = null
    override var isRunning = false
    override fun cancel(): Boolean {
      workLock.withLock {
        if (!isRunning) {
          pendingJobs = removeFromList(pendingJobs)
          return true
        }
      }
      return false
    }

    override fun moveToFront() {
      workLock.withLock {
        if (!isRunning) {
          pendingJobs = removeFromList(pendingJobs)
          pendingJobs = addToList(pendingJobs, true)
        }
      }
    }

    fun addToList(list: WorkNode?, addToFront: Boolean): WorkNode {
      var list = list
      assert(next == null)
      assert(prev == null)
      if (list == null) {
        prev = this
        next = prev
        list = next
      } else {
        next = list
        prev = list.prev
        prev?.next = this
        next?.prev = prev?.next
      }
      checkNotNull(list)
      return if (addToFront) this else list
    }

    fun removeFromList(list: WorkNode?): WorkNode? {
      var list = list
      assert(next != null)
      assert(prev != null)
      if (list === this) {
        list =
            if (next === this) {
              null
            } else {
              next
            }
      }
      next?.prev = prev
      prev?.next = next
      prev = null
      next = prev
      return list
    }

    fun verify(shouldBeRunning: Boolean) {
      assert(prev?.next ?: this === this)
      assert(next?.prev ?: this === this)
      assert(isRunning == shouldBeRunning)
    }
  }

  interface WorkItem {
    fun cancel(): Boolean
    val isRunning: Boolean
    fun moveToFront()
  }
}


/*
 * 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.gatekeeper

data class GateKeeper(val name: String, val value: Boolean)


/*
 * 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.gatekeeper

import com.facebook.FacebookSdk
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.
 */
typealias AppID = String

class GateKeeperRuntimeCache {
  private val gateKeepers = ConcurrentHashMap<AppID, ConcurrentHashMap<String, GateKeeper>>()

  /**
   * Set the cache with a list of name-value pairs
   *
   * @param appId Application ID
   * @param gateKeeperList The list of GateKeeper name-value pairs
   */
  fun setGateKeepers(
      appId: AppID = FacebookSdk.getApplicationId(),
      gateKeeperList: List<GateKeeper>
  ) {
    val gateKeepersMap = ConcurrentHashMap<String, GateKeeper>()
    gateKeeperList.forEach { gateKeepersMap[it.name] = it }
    gateKeepers[appId] = gateKeepersMap
  }

  /**
   * Dump the cache into a list of GateKeeper
   *
   * @param appId Application ID
   * @return the list of GateKeepers where name is the key and GK value is the value
   */
  fun dumpGateKeepers(appId: AppID = FacebookSdk.getApplicationId()): List<GateKeeper>? {
    val gateKeepersMap = gateKeepers[appId]
    return gateKeepersMap?.map { it.value }
  }

  /**
   * Get GateKeeper value with the name
   *
   * @param name the name of the GateKeeper
   * @param defaultValue return it if the GateKeeper doesn't exists
   * @return the GateKeeper value
   */
  fun getGateKeeperValue(
      appId: AppID = FacebookSdk.getApplicationId(),
      name: String,
      defaultValue: Boolean
  ): Boolean = getGateKeeper(appId, name)?.value ?: defaultValue

  /**
   * Set GateKeeper value with the name
   *
   * @param appId Application ID
   * @param name the name of the GateKeeper
   * @param value the new value for this GateKeeper
   */
  fun setGateKeeperValue(
      appId: AppID = FacebookSdk.getApplicationId(),
      name: String,
      value: Boolean
  ) {
    setGateKeeper(appId, GateKeeper(name, value))
  }

  /**
   * Get GateKeeper with the name
   *
   * @param appId Application ID
   * @param name the name of the GateKeeper
   * @return the GateKeeper object for this name. null if it doesn't exists
   */
  fun getGateKeeper(appId: AppID = FacebookSdk.getApplicationId(), name: String) =
      gateKeepers[appId]?.get(name)

  /**
   * Set GateKeeper with a GateKeeper object
   * @param appId Application ID
   * @param gateKeeper the GateKeeper object of name-value pair.
   */
  fun setGateKeeper(appId: AppID = FacebookSdk.getApplicationId(), gateKeeper: GateKeeper) {
    if (!gateKeepers.containsKey(appId)) {
      gateKeepers[appId] = ConcurrentHashMap()
    }
    gateKeepers[appId]?.set(gateKeeper.name, gateKeeper)
  }

  /**
   * Reset GateKeeper cache of an application
   *
   * @param appId Application ID
   */
  fun resetCache(appId: AppID = FacebookSdk.getApplicationId()) {
    gateKeepers.remove(appId)
  }
}


/*
 * 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.instrument

import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.GraphRequestBatch
import com.facebook.core.BuildConfig
import com.facebook.internal.FeatureManager
import com.facebook.internal.FeatureManager.disableFeature
import com.facebook.internal.FeatureManager.getFeature
import com.facebook.internal.Utility.isDataProcessingRestricted
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ExceptionAnalyzer {
  private var enabled = false

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  @JvmStatic
  internal fun isDebug(): Boolean = BuildConfig.DEBUG

  @JvmStatic
  fun enable() {
    enabled = true
    if (FacebookSdk.getAutoLogAppEventsEnabled()) {
      sendExceptionAnalysisReports()
    }
  }

  @JvmStatic
  fun execute(e: Throwable?) {
    if (!enabled || isDebug() || e == null) {
      return
    }
    val disabledFeatures: MutableSet<String?> = HashSet()
    e.stackTrace.forEach {
      val feature = getFeature(it.className)
      if (feature !== FeatureManager.Feature.Unknown) {
        disableFeature(feature)
        disabledFeatures.add(feature.toString())
      }
    }
    if (FacebookSdk.getAutoLogAppEventsEnabled() && disabledFeatures.isNotEmpty()) {
      InstrumentData.Builder.build(JSONArray(disabledFeatures)).save()
    }
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  internal fun sendExceptionAnalysisReports() {
    if (isDataProcessingRestricted) {
      return
    }
    val reports = InstrumentUtility.listExceptionAnalysisReportFiles()
    val requests: MutableList<GraphRequest> = ArrayList()
    for (report in reports) {
      val instrumentData = InstrumentData.Builder.load(report)
      if (instrumentData.isValid) {
        val params = JSONObject()
        try {
          params.put("crash_shield", instrumentData.toString())
          val request =
              GraphRequest.newPostRequest(
                  null,
                  String.format("%s" + "/instruments", FacebookSdk.getApplicationId()),
                  params) { response ->
                    try {
                      if (response.error == null &&
                          response.jsonObject?.getBoolean("success") == true) {
                        instrumentData.clear()
                      }
                    } catch (e: JSONException) {
                      /* no op */
                    }
                  }
          requests.add(request)
        } catch (e: JSONException) {
          /* no op */
        }
      }
    }
    if (requests.isEmpty()) {
      return
    }
    val requestBatch = GraphRequestBatch(requests)
    requestBatch.executeAsync()
  }
}


/*
 * 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.instrument

import android.os.Build
import androidx.annotation.RestrictTo
import com.facebook.internal.Utility.getAppVersion
import com.facebook.internal.instrument.InstrumentUtility.deleteFile
import com.facebook.internal.instrument.InstrumentUtility.getCause
import com.facebook.internal.instrument.InstrumentUtility.getStackTrace
import com.facebook.internal.instrument.InstrumentUtility.readFile
import com.facebook.internal.instrument.InstrumentUtility.writeFile
import java.io.File
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class InstrumentData {
  enum class Type {
    Unknown,
    Analysis,
    AnrReport,
    CrashReport,
    CrashShield,
    ThreadCheck;

    override fun toString(): String {
      return when (this) {
        Analysis -> "Analysis"
        AnrReport -> "AnrReport"
        CrashReport -> "CrashReport"
        CrashShield -> "CrashShield"
        ThreadCheck -> "ThreadCheck"
        else -> UNKNOWN
      }
    }

    val logPrefix: String
      get() {
        return when (this) {
          Analysis -> InstrumentUtility.ANALYSIS_REPORT_PREFIX
          AnrReport -> InstrumentUtility.ANR_REPORT_PREFIX
          CrashReport -> InstrumentUtility.CRASH_REPORT_PREFIX
          CrashShield -> InstrumentUtility.CRASH_SHIELD_PREFIX
          ThreadCheck -> InstrumentUtility.THREAD_CHECK_PREFIX
          else -> UNKNOWN
        }
      }
  }

  private var filename: String
  private var type: Type?
  private var featureNames: JSONArray? = null
  private var appVersion: String? = null
  private var cause: String? = null
  private var stackTrace: String? = null
  private var timestamp: Long? = null

  private constructor(features: JSONArray) {
    type = Type.Analysis
    timestamp = System.currentTimeMillis() / 1000
    featureNames = features
    filename =
        StringBuffer()
            .append(InstrumentUtility.ANALYSIS_REPORT_PREFIX)
            .append(timestamp.toString())
            .append(".json")
            .toString()
  }

  private constructor(e: Throwable?, t: Type) {
    type = t
    appVersion = getAppVersion()
    cause = getCause(e)
    stackTrace = getStackTrace(e)
    timestamp = System.currentTimeMillis() / 1000
    filename =
        StringBuffer().append(t.logPrefix).append(timestamp.toString()).append(".json").toString()
  }

  private constructor(anrCause: String?, st: String?) {
    type = Type.AnrReport
    appVersion = getAppVersion()
    cause = anrCause
    stackTrace = st
    timestamp = System.currentTimeMillis() / 1000
    filename =
        StringBuffer()
            .append(InstrumentUtility.ANR_REPORT_PREFIX)
            .append(timestamp.toString())
            .append(".json")
            .toString()
  }

  private constructor(file: File) {
    filename = file.name
    type = getType(filename)
    val obj = readFile(filename, true)
    if (obj != null) {
      timestamp = obj.optLong(PARAM_TIMESTAMP, 0)
      appVersion = obj.optString(PARAM_APP_VERSION, null)
      cause = obj.optString(PARAM_REASON, null)
      stackTrace = obj.optString(PARAM_CALLSTACK, null)
      featureNames = obj.optJSONArray(PARAM_FEATURE_NAMES)
    }
  }

  operator fun compareTo(data: InstrumentData): Int {
    val ts = timestamp ?: return -1
    val dts = data.timestamp ?: return 1
    return dts.compareTo(ts)
  }

  val isValid: Boolean
    get() {
      return when (type) {
        Type.Analysis -> featureNames != null && timestamp != null
        Type.AnrReport -> stackTrace != null && cause != null && timestamp != null
        Type.CrashReport,
        Type.CrashShield,
        Type.ThreadCheck -> stackTrace != null && timestamp != null
        else -> false
      }
    }

  fun save() {
    if (!isValid) {
      return
    }
    writeFile(filename, this.toString())
  }

  fun clear() {
    deleteFile(filename)
  }

  override fun toString(): String {
    val params = parameters ?: return JSONObject().toString()
    return params.toString()
  }

  private val parameters: JSONObject?
    private get() {
      return when (type) {
        Type.Analysis -> analysisReportParameters
        Type.AnrReport,
        Type.CrashReport,
        Type.CrashShield,
        Type.ThreadCheck -> exceptionReportParameters
        else -> null
      }
    }

  private val analysisReportParameters: JSONObject?
    private get() {
      val obj = JSONObject()
      try {
        if (featureNames != null) {
          obj.put(PARAM_FEATURE_NAMES, featureNames)
        }
        if (timestamp != null) {
          obj.put(PARAM_TIMESTAMP, timestamp)
        }
        return obj
      } catch (e: JSONException) {
        /* no op */
      }
      return null
    }

  private val exceptionReportParameters: JSONObject?
    private get() {
      val obj = JSONObject()
      try {
        obj.put(PARAM_DEVICE_OS, Build.VERSION.RELEASE)
        obj.put(PARAM_DEVICE_MODEL, Build.MODEL)
        if (appVersion != null) {
          obj.put(PARAM_APP_VERSION, appVersion)
        }
        if (timestamp != null) {
          obj.put(PARAM_TIMESTAMP, timestamp)
        }
        if (cause != null) {
          obj.put(PARAM_REASON, cause)
        }
        if (stackTrace != null) {
          obj.put(PARAM_CALLSTACK, stackTrace)
        }
        if (type != null) {
          obj.put(PARAM_TYPE, type)
        }
        return obj
      } catch (e: JSONException) {
        /* no op */
      }
      return null
    }

  object Builder {
    @JvmStatic
    fun load(file: File): InstrumentData {
      return InstrumentData(file)
    }

    @JvmStatic
    fun build(e: Throwable?, t: Type): InstrumentData {
      return InstrumentData(e, t)
    }

    @JvmStatic
    fun build(features: JSONArray): InstrumentData {
      return InstrumentData(features)
    }

    @JvmStatic
    fun build(anrCause: String?, st: String?): InstrumentData {
      return InstrumentData(anrCause, st)
    }
  }

  companion object {
    private const val UNKNOWN = "Unknown"
    private const val PARAM_TIMESTAMP = "timestamp"
    private const val PARAM_APP_VERSION = "app_version"
    private const val PARAM_DEVICE_OS = "device_os_version"
    private const val PARAM_DEVICE_MODEL = "device_model"
    private const val PARAM_REASON = "reason"
    private const val PARAM_CALLSTACK = "callstack"
    private const val PARAM_TYPE = "type"
    private const val PARAM_FEATURE_NAMES = "feature_names"
    private fun getType(filename: String): Type {
      if (filename.startsWith(InstrumentUtility.CRASH_REPORT_PREFIX)) {
        return Type.CrashReport
      } else if (filename.startsWith(InstrumentUtility.CRASH_SHIELD_PREFIX)) {
        return Type.CrashShield
      } else if (filename.startsWith(InstrumentUtility.THREAD_CHECK_PREFIX)) {
        return Type.ThreadCheck
      } else if (filename.startsWith(InstrumentUtility.ANALYSIS_REPORT_PREFIX)) {
        return Type.Analysis
      } else if (filename.startsWith(InstrumentUtility.ANR_REPORT_PREFIX)) {
        return Type.AnrReport
      }
      return Type.Unknown
    }
  }
}


/*
 * 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.instrument

import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk
import com.facebook.internal.FeatureManager
import com.facebook.internal.instrument.anrreport.ANRHandler
import com.facebook.internal.instrument.crashreport.CrashHandler
import com.facebook.internal.instrument.crashshield.CrashShieldHandler
import com.facebook.internal.instrument.errorreport.ErrorReportHandler
import com.facebook.internal.instrument.threadcheck.ThreadCheckHandler

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object InstrumentManager {
  /**
   * Start Instrument functionality.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown when loading and sending crash reports.
   */
  @JvmStatic
  fun start() {
    if (!FacebookSdk.getAutoLogAppEventsEnabled()) {
      return
    }
    FeatureManager.checkFeature(FeatureManager.Feature.CrashReport) { enabled ->
      if (enabled) {
        CrashHandler.enable()
        if (FeatureManager.isEnabled(FeatureManager.Feature.CrashShield)) {
          ExceptionAnalyzer.enable()
          CrashShieldHandler.enable()
        }
        if (FeatureManager.isEnabled(FeatureManager.Feature.ThreadCheck)) {
          ThreadCheckHandler.enable()
        }
      }
    }
    FeatureManager.checkFeature(FeatureManager.Feature.ErrorReport) { enabled ->
      if (enabled) {
        ErrorReportHandler.enable()
      }
    }
    FeatureManager.checkFeature(FeatureManager.Feature.AnrReport) { enabled ->
      if (enabled) {
        ANRHandler.enable()
      }
    }
  }
}


/*
 * 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.instrument

import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.internal.Utility
import com.facebook.internal.Utility.readStreamToString
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.lang.Exception
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object InstrumentUtility {
  const val ANALYSIS_REPORT_PREFIX = "analysis_log_"
  const val ANR_REPORT_PREFIX = "anr_log_"
  const val CRASH_REPORT_PREFIX = "crash_log_"
  const val CRASH_SHIELD_PREFIX = "shield_log_"
  const val THREAD_CHECK_PREFIX = "thread_check_log_"
  const val ERROR_REPORT_PREFIX = "error_log_"
  private const val FBSDK_PREFIX = "com.facebook"
  private const val METASDK_PREFIX = "com.meta"
  private const val CODELESS_PREFIX = "com.facebook.appevents.codeless"
  private const val SUGGESTED_EVENTS_PREFIX = "com.facebook.appevents.suggestedevents"
  private const val INSTRUMENT_DIR = "instrument"

  /**
   * Get the cause of the raised exception.
   *
   * @param e The Throwable containing the exception that was raised
   * @return The String containing the cause of the raised exception
   */
  @JvmStatic
  fun getCause(e: Throwable?): String? {
    if (e == null) {
      return null
    }
    return if (e.cause == null) {
      e.toString()
    } else e.cause.toString()
  }

  /**
   * Get the iterated call stack traces of the raised exception.
   *
   * @param e The Throwable containing the exception that was raised
   * @return The String containing the stack traces of the raised exception
   */
  @JvmStatic
  fun getStackTrace(e: Throwable?): String? {
    if (e == null) {
      return null
    }

    // Iterate on causes recursively
    val array = JSONArray()
    var previous: Throwable? = null // Prevent infinite loops
    var t = e
    while (t != null && t !== previous) {
      for (element in t.stackTrace) {
        array.put(element.toString())
      }
      previous = t
      t = t.cause
    }
    return array.toString()
  }

  /**
   * Get the stack trace of the input Thread.
   *
   * @param thread The Thread to obtain the stack trace
   * @return The String containing the stack traces of the raised exception
   */
  @JvmStatic
  fun getStackTrace(thread: Thread): String? {
    val stackTrace = thread.stackTrace
    val array = JSONArray()
    for (element in stackTrace) {
      array.put(element.toString())
    }
    return array.toString()
  }

  /**
   * Check whether a Throwable is related to Facebook SDK by looking at iterated stack traces and
   * return true if one of the traces has prefix "com.facebook".
   *
   * @param e The Throwable containing the exception that was raised
   * @return Whether the raised exception is related to Facebook SDK
   */
  @JvmStatic
  fun isSDKRelatedException(e: Throwable?): Boolean {
    if (e == null) {
      return false
    }

    // Iterate on causes recursively
    var previous: Throwable? = null // Prevent infinite loops
    var t = e
    while (t != null && t !== previous) {
      for (element in t.stackTrace) {
        if (isFromFbOrMeta(element)) {
          return true
        }
      }
      previous = t
      t = t.cause
    }
    return false
  }

  /**
   * Check whether an Thread is related to Facebook SDK by looking at iterated stack traces
   *
   * @param thread The Thread to obtain the stack trace
   * @return Whether the thread is related to Facebook SDK
   */
  @JvmStatic
  fun isSDKRelatedThread(thread: Thread?): Boolean {

    // Iterate on thread's stack traces
    thread?.stackTrace?.forEach { element ->
      if (isFromFbOrMeta(element)) {

        // Ignore the ANR caused by calling app itself's click listener or touch listener
        if (element.className.startsWith(CODELESS_PREFIX) ||
            element.className.startsWith(SUGGESTED_EVENTS_PREFIX)) {
          if (element.methodName.startsWith("onClick") ||
              element.methodName.startsWith("onItemClick") ||
              element.methodName.startsWith("onTouch")) {
            return@forEach
          }
        }

        return true
      }
    }

    return false
  }

  /**
   * Get the list of anr report files from instrument report directory defined in
   * [InstrumentUtility.getInstrumentReportDir] method.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   *
   * @return The list of anr files
   */
  @JvmStatic
  fun listAnrReportFiles(): Array<File> {
    val reportDir = getInstrumentReportDir() ?: return arrayOf()
    val reports =
        reportDir.listFiles { _, name ->
          name.matches(String.format("^%s[0-9]+.json$", ANR_REPORT_PREFIX).toRegex())
        }
    return reports ?: arrayOf()
  }

  /**
   * Get the list of exception analysis report files from instrument report directory defined in
   * [InstrumentUtility.getInstrumentReportDir] method.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   *
   * @return The list of exception analysis report files
   */
  @JvmStatic
  fun listExceptionAnalysisReportFiles(): Array<File> {
    val reportDir = getInstrumentReportDir() ?: return arrayOf()
    val reports =
        reportDir.listFiles { _, name ->
          name.matches(String.format("^%s[0-9]+.json$", ANALYSIS_REPORT_PREFIX).toRegex())
        }
    return reports ?: arrayOf()
  }

  /**
   * Get the list of exception report files from instrument report directory defined in [ ]
   * [InstrumentUtility.getInstrumentReportDir] method.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   *
   * @return The list of crash report files
   */
  @JvmStatic
  fun listExceptionReportFiles(): Array<File> {
    val reportDir = getInstrumentReportDir() ?: return arrayOf()
    val reports =
        reportDir.listFiles { _, name ->
          name.matches(
              String.format(
                      "^(%s|%s|%s)[0-9]+.json$",
                      CRASH_REPORT_PREFIX,
                      CRASH_SHIELD_PREFIX,
                      THREAD_CHECK_PREFIX)
                  .toRegex())
        }
    return reports ?: arrayOf()
  }

  /**
   * Read the content from the file which is denoted by filename and the directory is the instrument
   * report directory defined in [InstrumentUtility.getInstrumentReportDir] method.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   */
  @JvmStatic
  fun readFile(filename: String?, deleteOnException: Boolean): JSONObject? {
    val reportDir = getInstrumentReportDir()
    if (reportDir == null || filename == null) {
      return null
    }
    val file = File(reportDir, filename)
    val inputStream: FileInputStream
    try {
      inputStream = FileInputStream(file)
      val content = readStreamToString(inputStream)
      return JSONObject(content)
    } catch (e: Exception) {
      if (deleteOnException) {
        deleteFile(filename)
      }
    }
    return null
  }

  /**
   * Write the content to the file which is denoted by filename and the file will be put in
   * instrument report directory defined in [InstrumentUtility.getInstrumentReportDir] method.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   */
  @JvmStatic
  fun writeFile(filename: String?, content: String?) {
    val reportDir = getInstrumentReportDir()
    if (reportDir == null || filename == null || content == null) {
      return
    }
    val file = File(reportDir, filename)
    val outputStream: FileOutputStream
    try {
      outputStream = FileOutputStream(file)
      outputStream.write(content.toByteArray())
      outputStream.close()
    } catch (e: Exception) {
      /* no op */
    }
  }

  /**
   * Deletes the cache file under instrument report directory. If the instrument report directory
   * exists and the file exists under the directory, the file will be deleted.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   *
   * @return Whether the file is successfully deleted
   */
  @JvmStatic
  fun deleteFile(filename: String?): Boolean {
    val reportDir = getInstrumentReportDir()
    if (reportDir == null || filename == null) {
      return false
    }
    val file = File(reportDir, filename)
    return file.delete()
  }

  /** Create Graph Request for Instrument reports and send the reports to Facebook. */
  @JvmStatic
  fun sendReports(key: String?, reports: JSONArray, callback: GraphRequest.Callback?) {
    if (reports.length() == 0) {
      return
    }
    val params = JSONObject()
    try {
      params.put(key, reports.toString())
      val dataProcessingOptions = Utility.dataProcessingOptions
      if (dataProcessingOptions != null) {
        val it = dataProcessingOptions.keys()
        while (it.hasNext()) {
          val k = it.next()
          params.put(k, dataProcessingOptions[k])
        }
      }
    } catch (e: JSONException) {
      return
    }
    val request =
        GraphRequest.newPostRequest(
            null,
            String.format("%s" + "/instruments", FacebookSdk.getApplicationId()),
            params,
            callback)
    request.executeAsync()
  }

  /**
   * Get the instrument directory for report if the directory exists. If the directory doesn't
   * exist, will attempt to create the directory. Note that, the instrument directory is under cache
   * directory of the Application.
   *
   * Note that the function should be called after FacebookSdk is initialized. Otherwise, exception
   * FacebookSdkNotInitializedException will be thrown.
   *
   * @return The instrument cache directory if and only if the directory exists or it's successfully
   * created, otherwise return null.
   */
  @JvmStatic
  fun getInstrumentReportDir(): File? {
    val cacheDir = FacebookSdk.getApplicationContext().cacheDir
    val dir = File(cacheDir, INSTRUMENT_DIR)
    return if (dir.exists() || dir.mkdirs()) {
      dir
    } else null
  }

  @JvmStatic
  fun isFromFbOrMeta(element: StackTraceElement): Boolean {
    return element.className.startsWith(FBSDK_PREFIX)|| element.className.startsWith(METASDK_PREFIX);
  }
}


/*
 * 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.instrument.anrreport

import android.app.ActivityManager
import android.content.Context
import android.os.Looper
import android.os.Process
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.facebook.FacebookSdk
import com.facebook.internal.instrument.InstrumentData.Builder.build
import com.facebook.internal.instrument.InstrumentUtility.getStackTrace
import com.facebook.internal.instrument.InstrumentUtility.isSDKRelatedThread
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

@AutoHandleExceptions
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ANRDetector {
  private const val DETECTION_INTERVAL_IN_MS = 500
  private val myUid = Process.myUid() // the identity of its app-specific sandbox.
  private val scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
  private var previousStackTrace: String? = ""
  private val anrDetectorRunnable = Runnable {
    try {
      val am: ActivityManager? =
          FacebookSdk.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE)
              as ActivityManager
      checkProcessError(am)
    } catch (e: Exception) {
      /*no op*/
    }
  }

  @JvmStatic
  @VisibleForTesting
  fun checkProcessError(am: ActivityManager?) {
    am?.processesInErrorState?.forEach { info ->
      if (info.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING &&
          info.uid == myUid) {
        val mainThread = Looper.getMainLooper().thread
        val stackTrace = getStackTrace(mainThread)
        if (stackTrace == previousStackTrace || !isSDKRelatedThread(mainThread)) {
          return@forEach
        }
        previousStackTrace = stackTrace
        build(info.shortMsg, stackTrace).save()
      }
    }
  }

  // Should be only called by ANRHandler
  @JvmStatic
  @VisibleForTesting
  fun start() {
    scheduledExecutorService.scheduleAtFixedRate(
        anrDetectorRunnable, 0, DETECTION_INTERVAL_IN_MS.toLong(), TimeUnit.MILLISECONDS)
  }
}


/*
 * 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.instrument.anrreport

import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.facebook.FacebookSdk
import com.facebook.internal.Utility.isDataProcessingRestricted
import com.facebook.internal.instrument.InstrumentData
import com.facebook.internal.instrument.InstrumentUtility
import com.facebook.internal.instrument.InstrumentUtility.listAnrReportFiles
import com.facebook.internal.instrument.InstrumentUtility.sendReports
import com.facebook.internal.instrument.anrreport.ANRDetector.start
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
import org.json.JSONArray
import org.json.JSONException

@AutoHandleExceptions
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ANRHandler {
  private const val MAX_ANR_REPORT_NUM = 5
  private val enabled = AtomicBoolean(false)

  @JvmStatic
  @Synchronized
  fun enable() {
    if (enabled.getAndSet(true)) {
      return
    }
    if (FacebookSdk.getAutoLogAppEventsEnabled()) {
      sendANRReports()
    }
    start()
  }

  /**
   * Load cached ANR reports from cache directory defined in
   * [InstrumentUtility.getInstrumentReportDir], create Graph Request and send the request to
   * Facebook along with crash reports.
   */
  @JvmStatic
  @VisibleForTesting
  fun sendANRReports() {
    if (isDataProcessingRestricted) {
      return
    }
    val reports = listAnrReportFiles()
    val validReports =
        reports
            .map { InstrumentData.Builder.load(it) }
            .filter { it.isValid }
            .sortedWith(Comparator { o1, o2 -> o1.compareTo(o2) })
    val anrLogs = JSONArray()
    // keep at most MAX_CRASH_REPORT_NUM reports
    (0 until min(validReports.size, ANRHandler.MAX_ANR_REPORT_NUM)).forEach {
      anrLogs.put(validReports[it])
    }
    sendReports("anr_reports", anrLogs) { response ->
      try {
        if (response.error == null && response.jsonObject?.getBoolean("success") == true) {
          validReports.forEach { it.clear() }
        }
      } catch (e: JSONException) {
        /* no op */
      }
    }
  }
}


/*
 * 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.instrument.crashreport

import android.util.Log
import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk
import com.facebook.internal.Utility.isDataProcessingRestricted
import com.facebook.internal.instrument.ExceptionAnalyzer
import com.facebook.internal.instrument.InstrumentData
import com.facebook.internal.instrument.InstrumentUtility
import kotlin.math.min
import org.json.JSONArray
import org.json.JSONException

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class CrashHandler
private constructor(private val previousHandler: Thread.UncaughtExceptionHandler?) :
    Thread.UncaughtExceptionHandler {
  override fun uncaughtException(t: Thread, e: Throwable) {
    if (InstrumentUtility.isSDKRelatedException(e)) {
      ExceptionAnalyzer.execute(e)
      InstrumentData.Builder.build(e, InstrumentData.Type.CrashReport).save()
    }
    previousHandler?.uncaughtException(t, e)
  }

  companion object {
    private val TAG = CrashHandler::class.java.canonicalName
    private const val MAX_CRASH_REPORT_NUM = 5
    private var instance: CrashHandler? = null

    @JvmStatic
    @Synchronized
    fun enable() {
      if (FacebookSdk.getAutoLogAppEventsEnabled()) {
        sendExceptionReports()
      }
      if (instance != null) {
        Log.w(TAG, "Already enabled!")
        return
      }
      val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
      instance = CrashHandler(oldHandler)
      Thread.setDefaultUncaughtExceptionHandler(instance)
    }

    /**
     * Load cached exception reports from cache directory defined in
     * [InstrumentUtility.getInstrumentReportDir], create Graph Request and send the request to
     * Facebook along with crash reports.
     */
    private fun sendExceptionReports() {
      if (isDataProcessingRestricted) {
        return
      }
      val reports = InstrumentUtility.listExceptionReportFiles()
      val validReports =
          reports
              .map { InstrumentData.Builder.load(it) }
              .filter { it.isValid }
              .sortedWith(Comparator { o1, o2 -> o1.compareTo(o2) })
      val crashLogs = JSONArray()

      // keep at most MAX_CRASH_REPORT_NUM reports
      (0 until (min(validReports.size, MAX_CRASH_REPORT_NUM))).forEach {
        crashLogs.put(validReports[it])
      }

      InstrumentUtility.sendReports("crash_reports", crashLogs) { response ->
        try {
          if (response.error == null && response.jsonObject?.getBoolean("success") == true) {
            validReports.forEach { it.clear() }
          }
        } catch (e: JSONException) {
          /* no op */
        }
      }
    }
  }
}


/*
 * 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.instrument.crashshield

@kotlin.annotation.Retention(AnnotationRetention.BINARY) annotation class AutoHandleExceptions


/*
 * 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.instrument.crashshield

import android.os.Handler
import android.os.Looper
import androidx.annotation.VisibleForTesting
import com.facebook.FacebookSdk
import com.facebook.core.BuildConfig
import com.facebook.internal.instrument.ExceptionAnalyzer
import com.facebook.internal.instrument.InstrumentData
import java.util.Collections
import java.util.WeakHashMap

object CrashShieldHandler {
  private val crashingObjects = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
  private var enabled = false

  @JvmStatic
  fun enable() {
    enabled = true
  }

  @VisibleForTesting
  @JvmStatic
  fun disable() {
    enabled = false
  }

  @JvmStatic
  fun handleThrowable(e: Throwable?, o: Any) {
    if (!enabled) {
      return
    }
    crashingObjects.add(o)
    if (FacebookSdk.getAutoLogAppEventsEnabled()) {
      ExceptionAnalyzer.execute(e)
      InstrumentData.Builder.build(e, InstrumentData.Type.CrashShield).save()
    }
    scheduleCrashInDebug(e)
  }

  @JvmStatic
  fun isObjectCrashing(o: Any): Boolean {
    return crashingObjects.contains(o)
  }

  @JvmStatic fun methodFinished(o: Any?) = Unit

  @JvmStatic
  fun reset() {
    resetCrashingObjects()
  }

  @JvmStatic
  fun resetCrashingObjects() {
    crashingObjects.clear()
  }

  @VisibleForTesting @JvmStatic fun isDebug() = BuildConfig.DEBUG

  @VisibleForTesting
  @JvmStatic
  fun scheduleCrashInDebug(e: Throwable?) {
    if (isDebug()) {
      Handler(Looper.getMainLooper())
          .post(
              object : Runnable {
                @NoAutoExceptionHandling
                override fun run() {
                  // throw on main thread during development to avoid catching by crash shield
                  throw RuntimeException(e)
                }
              })
    }
  }
}


/*
 * 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.instrument.crashshield

@kotlin.annotation.Retention(AnnotationRetention.BINARY) annotation class NoAutoExceptionHandling


/*
 * 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.instrument.errorreport

import androidx.annotation.RestrictTo
import com.facebook.internal.instrument.InstrumentUtility
import java.io.File
import org.json.JSONException
import org.json.JSONObject

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ErrorReportData {
  private var filename: String
  private var errorMessage: String? = null
  private var timestamp: Long? = null

  constructor(message: String?) {
    timestamp = System.currentTimeMillis() / 1000
    errorMessage = message
    filename =
        StringBuffer()
            .append(InstrumentUtility.ERROR_REPORT_PREFIX)
            .append(timestamp as Long)
            .append(".json")
            .toString()
  }

  constructor(file: File) {
    filename = file.name
    val obj = InstrumentUtility.readFile(filename, true)
    if (obj != null) {
      timestamp = obj.optLong(PARAM_TIMESTAMP, 0)
      errorMessage = obj.optString(PRARAM_ERROR_MESSAGE, null)
    }
  }

  operator fun compareTo(data: ErrorReportData): Int {
    val ts = timestamp ?: return -1
    val dts = data.timestamp ?: return 1
    return dts.compareTo(ts)
  }

  val isValid: Boolean
    get() = errorMessage != null && timestamp != null

  fun save() {
    if (isValid) {
      InstrumentUtility.writeFile(filename, this.toString())
    }
  }

  fun clear() {
    InstrumentUtility.deleteFile(filename)
  }

  override fun toString(): String {
    val params = parameters ?: return super.toString()
    return params.toString()
  }

  val parameters: JSONObject?
    get() {
      val obj = JSONObject()
      try {
        if (timestamp != null) {
          obj.put(PARAM_TIMESTAMP, timestamp)
        }
        obj.put(PRARAM_ERROR_MESSAGE, errorMessage)
        return obj
      } catch (e: JSONException) {
        /* no op */
      }
      return null
    }

  companion object {
    private const val PRARAM_ERROR_MESSAGE = "error_message"
    private const val PARAM_TIMESTAMP = "timestamp"
  }
}


/*
 * 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.instrument.errorreport

import androidx.annotation.RestrictTo
import com.facebook.FacebookSdk
import com.facebook.internal.Utility.isDataProcessingRestricted
import com.facebook.internal.instrument.InstrumentUtility
import java.io.File
import org.json.JSONArray
import org.json.JSONException

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ErrorReportHandler {
  private const val MAX_ERROR_REPORT_NUM = 1000

  @JvmStatic
  fun save(msg: String?) {
    try {
      val errorReport = ErrorReportData(msg)
      errorReport.save()
    } catch (ex: Exception) {
      /*no op*/
    }
  }

  @JvmStatic
  fun enable() {
    if (FacebookSdk.getAutoLogAppEventsEnabled()) {
      sendErrorReports()
    }
  }

  /**
   * Load cached error reports from cache directory defined in [ ]
   * [InstrumentUtility.getInstrumentReportDir], create Graph Request and send the request to
   * Facebook along with crash reports.
   */
  @JvmStatic
  fun sendErrorReports() {
    if (isDataProcessingRestricted) {
      return
    }
    val reports = listErrorReportFiles()
    val validReports = ArrayList<ErrorReportData>()
    for (report in reports) {
      val errorData = ErrorReportData(report)
      if (errorData.isValid) {
        validReports.add(errorData)
      }
    }
    validReports.sortWith(Comparator { o1, o2 -> o1.compareTo(o2) })
    val errorLogs = JSONArray()
    var i = 0
    while (i < validReports.size && i < MAX_ERROR_REPORT_NUM) {
      errorLogs.put(validReports[i])
      i++
    }
    InstrumentUtility.sendReports("error_reports", errorLogs) { response ->
      try {
        if (response.error == null && response.jsonObject?.getBoolean("success") == true) {
          validReports.forEach { it.clear() }
        }
      } catch (e: JSONException) {
        /* no op */
      }
    }
  }

  @JvmStatic
  fun listErrorReportFiles(): Array<File> {
    val reportDir = InstrumentUtility.getInstrumentReportDir() ?: return arrayOf()
    return reportDir.listFiles { dir, name ->
      name.matches(Regex(String.format("^%s[0-9]+.json$", InstrumentUtility.ERROR_REPORT_PREFIX)))
    }
  }
}


/*
 * 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.instrument.threadcheck

import android.os.Looper
import android.util.Log
import androidx.annotation.RestrictTo
import com.facebook.internal.instrument.InstrumentData
import java.util.Locale

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ThreadCheckHandler {
  private val TAG = ThreadCheckHandler::class.java.canonicalName
  private var enabled = false

  @JvmStatic
  fun enable() {
    enabled = true
  }

  @JvmStatic
  fun uiThreadViolationDetected(clazz: Class<*>, methodName: String, methodDesc: String) {
    log("@UiThread", clazz, methodName, methodDesc)
  }

  @JvmStatic
  fun workerThreadViolationDetected(clazz: Class<*>, methodName: String, methodDesc: String) {
    log("@WorkerThread", clazz, methodName, methodDesc)
  }

  private fun log(annotation: String, clazz: Class<*>, methodName: String, methodDesc: String) {
    if (!enabled) {
      return
    }
    val message =
        String.format(
            Locale.US,
            "%s annotation violation detected in %s.%s%s. Current looper is %s and main looper is %s.",
            annotation,
            clazz.name,
            methodName,
            methodDesc,
            Looper.myLooper(),
            Looper.getMainLooper())
    val e = Exception()
    Log.e(TAG, message, e)
    InstrumentData.Builder.build(e, InstrumentData.Type.ThreadCheck).save()
  }
}


/*
 * 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.qualityvalidation

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

/** @see ExcusesForDesignViolations */
@Retention(RetentionPolicy.SOURCE)
@Target
annotation class Excuse(val type: String, val reason: 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.qualityvalidation

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

/**
 * Annotations to mark code files as having known design issues and avoid failing the design
 * integration test on them
 *
 *
Please avoid using it for everything that can be fixed trivially, and delaying fixes to future
 * diffs without a good reason.
 *
 *
To use it list one of the type of issue you want to ignore, and supply and extra string
 * explaining why this is okay for this case.
 *
 *
{@code
 * @ExcusesForDesignViolations(
 * @Excuse(type = "STORY_VIEWER_CAPITALIZATION", reason = "Legacy"),
 * @Excuse(type = "MISSING_JAVA_DOC", reason = "DI Module") ) }

 */
@Retention(RetentionPolicy.SOURCE)
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
annotation class ExcusesForDesignViolations(vararg val value: Excuse)


/*
 * 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.security

import android.content.Context
import android.content.pm.PackageManager
import android.util.Base64
import androidx.annotation.VisibleForTesting
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

/*
  This would need to be changed if we change the docs for how to submit the certificate hashes
  https://developers.facebook.com/docs/android/getting-started/
*/

object CertificateUtil {
  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  internal const val DELIMITER = ":" // not part of valid characters for base64

  /** @return String of concatenated signatures, since there can be more than one */
  @JvmStatic
  fun getCertificateHash(ctx: Context): String {
    try {
      val signatures =
          ctx.packageManager
              .getPackageInfo(ctx.packageName, PackageManager.GET_SIGNATURES)
              .signatures
      val sb = StringBuilder()
      val md = MessageDigest.getInstance("SHA1")
      for (signature in signatures) {
        md.update(signature.toByteArray())
        sb.append(Base64.encodeToString(md.digest(), Base64.DEFAULT))
        sb.append(DELIMITER)
      }
      if (sb.isNotEmpty()) {
        sb.setLength(sb.length - 1) // remove last delimiter
      }
      return sb.toString()
    } catch (e: PackageManager.NameNotFoundException) {
      // do nothing
    } catch (e: NoSuchAlgorithmException) {
      // do nothing
    }
    return ""
  }
}


/*
 * 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.login

import com.facebook.internal.NativeProtocol

/**
 * Certain operations such as publishing a status or publishing a photo require an audience. When
 * the user grants an application permission to perform a publish operation, a default audience is
 * selected as the publication ceiling for the application. This enumerated value allows the
 * application to select which audience to ask the user to grant publish permission for.
 */
enum class DefaultAudience(val nativeProtocolAudience: String?) {
  /** Represents an invalid default audience value, can be used when only reading. */
  NONE(null),

  /** Indicates only the user is able to see posts made by the application. */
  ONLY_ME(NativeProtocol.AUDIENCE_ME),

  /** Indicates that the user's friends are able to see posts made by the application. */
  FRIENDS(NativeProtocol.AUDIENCE_FRIENDS),
  /** Indicates that all Facebook users are able to see posts made by the application. */
  EVERYONE(NativeProtocol.AUDIENCE_EVERYONE)
}


/*
 * 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.login

enum class LoginTargetApp(private val targetApp: String) {
  FACEBOOK("facebook"),
  INSTAGRAM("instagram");

  override fun toString(): String = targetApp

  companion object {
    /**
     * Return the LoginTargetApp by given string
     *
     * @param stringValue
     * @return LoginTargetApp default return LoginTargetApp.FACEBOOK
     */
    @JvmStatic
    fun fromString(stringValue: String?): LoginTargetApp {
      for (targetApp in values()) {
        if (targetApp.toString() == stringValue) {
          return targetApp
        }
      }
      return FACEBOOK
    }
  }
}


/*
 * 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

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import com.facebook.internal.Utility
import java.util.Date
import org.assertj.core.api.Assertions.assertThat
import org.json.JSONObject
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
import org.powermock.api.mockito.PowerMockito
import org.powermock.api.support.membermodification.MemberModifier
import org.powermock.core.classloader.annotations.PrepareForTest
import org.robolectric.RuntimeEnvironment

@PrepareForTest(FacebookSdk::class, LegacyTokenHelper::class, Utility::class)
class AccessTokenCacheTest : FacebookPowerMockTestCase() {
  companion object {
    private const val TOKEN_STRING = "A token of my esteem"
    private const val USER_ID = "1000"
    private val PERMISSIONS = listOf("walk", "chew gum")
    private val EXPIRES = Date(2_025, 5, 3)
    private val LAST_REFRESH = Date(2_023, 8, 15)
    private const val APP_ID = "1234"
  }
  private lateinit var sharedPreferences: SharedPreferences
  private lateinit var cachingStrategy: LegacyTokenHelper
  private lateinit var cachingStrategyFactory:
      AccessTokenCache.SharedPreferencesTokenCachingStrategyFactory

  @Before
  fun before() {
    PowerMockito.mockStatic(FacebookSdk::class.java)
    sharedPreferences =
        RuntimeEnvironment.application.getSharedPreferences(
            AccessTokenManager.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
    sharedPreferences.edit().clear().commit()
    cachingStrategy = mock()
    cachingStrategyFactory = mock()
    whenever(cachingStrategyFactory.create()).thenReturn(cachingStrategy)
    MemberModifier.stub<Any>(
            PowerMockito.method(
                Utility::class.java, "awaitGetGraphMeRequestWithCache", String::class.java))
        .toReturn(JSONObject().put("id", "1000"))
  }

  @Test
  fun `test load returns false if no cached token`() {
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val accessToken = cache.load()
    Assert.assertNull(accessToken)
    verifyZeroInteractions(cachingStrategyFactory)
  }

  @Test
  fun `test load returns false if no cached or legacy token`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val accessToken = cache.load()
    Assert.assertNull(accessToken)
  }

  @Test
  fun `test load returns false if empty cached token and does not check legacy`() {
    val jsonObject = JSONObject()
    sharedPreferences
        .edit()
        .putString(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY, jsonObject.toString())
        .commit()
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val accessToken = cache.load()
    Assert.assertNull(accessToken)
    verifyZeroInteractions(cachingStrategy)
  }

  @Test
  fun `test load returns false if no cached token and empty legacy token`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    whenever(cachingStrategy.load()).thenReturn(Bundle())
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val accessToken = cache.load()
    Assert.assertNull(accessToken)
  }

  @Test
  fun `test load valid cached token`() {
    val accessToken = createAccessToken()
    val jsonObject = accessToken.toJSONObject()
    sharedPreferences
        .edit()
        .putString(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY, jsonObject.toString())
        .commit()
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val loadedAccessToken = cache.load()
    Assert.assertNotNull(loadedAccessToken)
    Assert.assertEquals(accessToken, loadedAccessToken)
  }

  @Test
  fun `test load sets current token if no cached token but valid legacy token`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    val accessToken = createAccessToken()
    whenever(cachingStrategy.load())
        .thenReturn(AccessTokenTestHelper.toLegacyCacheBundle(accessToken))
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    val loadedAccessToken = cache.load()
    Assert.assertNotNull(loadedAccessToken)
    Assert.assertEquals(accessToken, loadedAccessToken)
  }

  @Test
  fun `test load saves token when upgrading from legacy token`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    val accessToken = createAccessToken()
    whenever(cachingStrategy.load())
        .thenReturn(AccessTokenTestHelper.toLegacyCacheBundle(accessToken))
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    cache.load()
    assertThat(sharedPreferences.contains(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY)).isTrue
    val savedAccessToken =
        AccessToken.createFromJSONObject(
            JSONObject(
                checkNotNull(
                    sharedPreferences.getString(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY, null))))
    Assert.assertEquals(accessToken, savedAccessToken)
  }

  @Test
  fun `test load clears legacy cache when upgrading from legacy token`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    val accessToken = createAccessToken()
    whenever(cachingStrategy.load())
        .thenReturn(AccessTokenTestHelper.toLegacyCacheBundle(accessToken))
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    cache.load()
    verify(cachingStrategy, times(1)).clear()
  }

  @Test
  fun `test save writes to cache if token is valid`() {
    val accessToken = createAccessToken()
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    cache.save(accessToken)
    verify(cachingStrategy, never()).save(any())
    assertThat(sharedPreferences.contains(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY)).isTrue
    val savedAccessToken =
        AccessToken.createFromJSONObject(
            JSONObject(
                checkNotNull(
                    sharedPreferences.getString(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY, null))))
    Assert.assertEquals(accessToken, savedAccessToken)
  }

  @Test
  fun `test clear cache does clear cache`() {
    val accessToken = createAccessToken()
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    cache.save(accessToken)
    cache.clear()
    assertThat(sharedPreferences.contains(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY)).isFalse
    verify(cachingStrategy, never()).clear()
  }

  @Test
  fun `test clear cache clears legacy cache`() {
    whenever(FacebookSdk.isLegacyTokenUpgradeSupported()).thenReturn(true)
    val accessToken = createAccessToken()
    val cache = AccessTokenCache(sharedPreferences, cachingStrategyFactory)
    cache.save(accessToken)
    cache.clear()
    assertThat(sharedPreferences.contains(AccessTokenCache.CACHED_ACCESS_TOKEN_KEY)).isFalse
    verify(cachingStrategy, times(1)).clear()
  }

  private fun createAccessToken(
      tokenString: String = TOKEN_STRING,
      userId: String = USER_ID
  ): AccessToken {
    return AccessToken(
        tokenString,
        APP_ID,
        userId,
        PERMISSIONS,
        null,
        null,
        AccessTokenSource.WEB_VIEW,
        EXPIRES,
        LAST_REFRESH,
        null)
  }

  @Test
  fun `test the token caching strategy factory will create a cache from the application context`() {
    val mockContext = mock<Context>()
    whenever(mockContext.applicationContext).thenReturn(mockContext)
    whenever(mockContext.getSharedPreferences(any<String>(), any())).thenReturn(mock())
    whenever(FacebookSdk.getApplicationContext()).thenReturn(mockContext)

    val factory = AccessTokenCache.SharedPreferencesTokenCachingStrategyFactory()
    factory.create()
    verify(mockContext)
        .getSharedPreferences(eq(LegacyTokenHelper.DEFAULT_CACHE_KEY), eq(Context.MODE_PRIVATE))
  }
}


/*
 * 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

import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.test.core.app.ApplicationProvider
import com.facebook.internal.FeatureManager
import com.facebook.internal.Utility
import com.facebook.util.common.mockLocalBroadcastManager
import java.util.Date
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.json.JSONObject
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.powermock.api.mockito.PowerMockito
import org.powermock.api.support.membermodification.MemberMatcher
import org.powermock.api.support.membermodification.MemberModifier
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.reflect.Whitebox
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

@PrepareForTest(
    FacebookSdk::class,
    AccessTokenCache::class,
    Utility::class,
    LocalBroadcastManager::class,
    FeatureManager::class)
class AccessTokenManagerTest : FacebookPowerMockTestCase() {
  companion object {
    private const val TOKEN_STRING = "A token of my esteem"
    private const val USER_ID = "1000"
    private const val PACKAGE_NAME = "com.testapp"
    private val PERMISSIONS = listOf("walk", "chew gum")
    private val EXPIRES = Date(2_025, 5, 3)
    private val LAST_REFRESH = Date(2_023, 8, 15)
    private val DATA_ACCESS_EXPIRATION_TIME = Date(2_025, 5, 3)
    private const val APP_ID = "1234"
  }
  private lateinit var localBroadcastManager: LocalBroadcastManager
  private lateinit var accessTokenCache: AccessTokenCache
  private lateinit var mockGraphRequestCompanionObject: GraphRequest.Companion
  private lateinit var mockGraphRequest: GraphRequest
  private lateinit var mockApplicationContext: Context
  private lateinit var mockAlarmManager: AlarmManager

  override fun setup() {
    super.setup()
    mockApplicationContext = mock()
    whenever(mockApplicationContext.applicationContext).thenReturn(mockApplicationContext)
    whenever(mockApplicationContext.packageName).thenReturn(PACKAGE_NAME)
    mockAlarmManager = mock()
    whenever(mockApplicationContext.getSystemService(Context.ALARM_SERVICE))
        .thenReturn(mockAlarmManager)

    PowerMockito.mockStatic(FacebookSdk::class.java)
    whenever(FacebookSdk.isInitialized()).thenReturn(true)
    whenever(FacebookSdk.getApplicationContext()).thenReturn(mockApplicationContext)

    MemberModifier.suppress(MemberMatcher.method(Utility::class.java, "clearFacebookCookies"))
    accessTokenCache = mock()
    localBroadcastManager = mockLocalBroadcastManager(ApplicationProvider.getApplicationContext())
    PowerMockito.mockStatic(LocalBroadcastManager::class.java)
    whenever(LocalBroadcastManager.getInstance(any())).thenReturn(localBroadcastManager)
    mockGraphRequestCompanionObject = mock()
    Whitebox.setInternalState(
        GraphRequest::class.java, "Companion", mockGraphRequestCompanionObject)
    mockGraphRequest = mock()
    whenever(mockGraphRequestCompanionObject.newGraphPathRequest(any(), any(), any()))
        .thenReturn(mockGraphRequest)
    PowerMockito.mockStatic(FeatureManager::class.java)
  }

  @Test
  fun testDefaultsToNoCurrentAccessToken() {
    val accessTokenManager = createAccessTokenManager()
    assertThat(accessTokenManager.currentAccessToken).isNull()
  }

  @Test
  fun testCanSetCurrentAccessToken() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    accessTokenManager.currentAccessToken = accessToken
    assertThat(accessTokenManager.currentAccessToken).isEqualTo(accessToken)
  }

  @Test
  @Config(sdk = [21])
  fun `test set current access token will register an expiration pending intent without immutable flag on API lower than 23`() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    val pendingIntentCaptor = argumentCaptor<PendingIntent>()

    accessTokenManager.currentAccessToken = accessToken

    verify(mockAlarmManager).set(anyOrNull(), anyOrNull(), pendingIntentCaptor.capture())
    val capturedPendingIntent = pendingIntentCaptor.firstValue
    val shadowPendingIntent = shadowOf(capturedPendingIntent)
    assertThat(shadowPendingIntent.savedIntent.component?.packageName).isEqualTo(PACKAGE_NAME)
    assertThat(shadowPendingIntent.savedIntent.component?.className)
        .isEqualTo(CurrentAccessTokenExpirationBroadcastReceiver::class.java.name)
    assertThat(shadowPendingIntent.flags and PendingIntent.FLAG_IMMUTABLE).isEqualTo(0)
  }

  @Test
  @Config(sdk = [23])
  fun `test set current access token will register an expiration pending intent with correct flag on API 23+`() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    val pendingIntentCaptor = argumentCaptor<PendingIntent>()
    accessTokenManager.currentAccessToken = accessToken
    verify(mockAlarmManager).set(anyOrNull(), anyOrNull(), pendingIntentCaptor.capture())
    val capturedPendingIntent = pendingIntentCaptor.firstValue
    val shadowPendingIntent = shadowOf(capturedPendingIntent)
    assertThat(shadowPendingIntent.savedIntent.component?.packageName).isEqualTo(PACKAGE_NAME)
    assertThat(shadowPendingIntent.savedIntent.component?.className)
        .isEqualTo(CurrentAccessTokenExpirationBroadcastReceiver::class.java.name)
    assertThat(shadowPendingIntent.flags and PendingIntent.FLAG_IMMUTABLE)
        .isEqualTo(PendingIntent.FLAG_IMMUTABLE)
  }

  @Test
  fun testChangingAccessTokenSendsBroadcast() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    accessTokenManager.currentAccessToken = accessToken
    val intents = arrayOfNulls<Intent>(1)
    val broadcastReceiver: BroadcastReceiver =
        object : BroadcastReceiver() {
          override fun onReceive(context: Context, intent: Intent) {
            intents[0] = intent
          }
        }
    localBroadcastManager.registerReceiver(
        broadcastReceiver, IntentFilter(AccessTokenManager.ACTION_CURRENT_ACCESS_TOKEN_CHANGED))
    val anotherAccessToken = createAccessToken("another string", "1000")
    accessTokenManager.currentAccessToken = anotherAccessToken
    localBroadcastManager.unregisterReceiver(broadcastReceiver)
    val intent = intents[0]
    checkNotNull(intent)
    val oldAccessToken: AccessToken =
        checkNotNull(intent.getParcelableExtra(AccessTokenManager.EXTRA_OLD_ACCESS_TOKEN))
    val newAccessToken: AccessToken =
        checkNotNull(intent.getParcelableExtra(AccessTokenManager.EXTRA_NEW_ACCESS_TOKEN))
    assertThat(accessToken.token).isEqualTo(oldAccessToken.token)
    assertThat(anotherAccessToken.token).isEqualTo(newAccessToken.token)
  }

  @Test
  fun testLoadReturnsFalseIfNoCachedToken() {
    val accessTokenManager = createAccessTokenManager()
    assertThat(accessTokenManager.loadCurrentAccessToken()).isFalse
  }

  @Test
  fun testLoadReturnsTrueIfCachedToken() {
    val accessToken = createAccessToken()
    whenever(accessTokenCache.load()).thenReturn(accessToken)
    val accessTokenManager = createAccessTokenManager()
    assertThat(accessTokenManager.loadCurrentAccessToken()).isTrue
  }

  @Test
  fun testLoadSetsCurrentTokenIfCached() {
    val accessToken = createAccessToken()
    whenever(accessTokenCache.load()).thenReturn(accessToken)
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.loadCurrentAccessToken()
    assertThat(accessToken).isEqualTo(accessTokenManager.currentAccessToken)
  }

  @Test
  fun testSaveWritesToCacheIfToken() {
    val accessToken = createAccessToken()
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.currentAccessToken = accessToken
    verify(accessTokenCache, times(1)).save(any())
  }

  @Test
  fun testSetEmptyTokenClearsCache() {
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.currentAccessToken = null
    verify(accessTokenCache, times(1)).clear()
  }

  @Test
  fun testLoadDoesNotSave() {
    val accessToken = createAccessToken()
    whenever(accessTokenCache.load()).thenReturn(accessToken)
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.loadCurrentAccessToken()
    verify(accessTokenCache, never()).save(any())
  }

  @Test
  fun testRefreshingAccessTokenDoesSendRequests() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    val grantedPermissionsCallbackCaptor = argumentCaptor<GraphRequest.Callback>()
    val extendedTokenCallbackCaptor = argumentCaptor<GraphRequest.Callback>()
    whenever(
            mockGraphRequestCompanionObject.newGraphPathRequest(
                any(), eq("me/permissions"), grantedPermissionsCallbackCaptor.capture()))
        .thenReturn(mock())
    whenever(
            mockGraphRequestCompanionObject.newGraphPathRequest(
                any(), eq("oauth/access_token"), extendedTokenCallbackCaptor.capture()))
        .thenReturn(mock())

    accessTokenManager.currentAccessToken = accessToken
    accessTokenManager.refreshCurrentAccessToken(null)

    // verify the requests are sent
    val requestBatchCaptor = argumentCaptor<GraphRequestBatch>()
    verify(mockGraphRequestCompanionObject, times(1))
        .executeBatchAsync(requestBatchCaptor.capture())
    val capturedRequestBatch = requestBatchCaptor.firstValue
    assertThat(capturedRequestBatch.size).isEqualTo(2)

    // mock the response for granted permissions
    val mockGrantedPermissionsResponse = mock<GraphResponse>()
    val mockGrantedPermissionsResponseData =
        JSONObject(
            """{"data":[{"permission":"walk","status":"granted"},{"permission":"chew gum","status":"declined"}]}""")
    whenever(mockGrantedPermissionsResponse.jsonObject)
        .thenReturn(mockGrantedPermissionsResponseData)
    grantedPermissionsCallbackCaptor.firstValue.onCompleted(mockGrantedPermissionsResponse)

    // mock the response for extend access token request
    val mockExtendedAccessTokenResponse = mock<GraphResponse>()
    val mockExtendedAccessTokenResponseData =
        JSONObject(
            """{"access_token":"new token string","expires_at":${EXPIRES.time},"graph_domain":"facebook"}""")
    whenever(mockExtendedAccessTokenResponse.jsonObject)
        .thenReturn(mockExtendedAccessTokenResponseData)
    extendedTokenCallbackCaptor.firstValue.onCompleted(mockExtendedAccessTokenResponse)

    // invoke the batch callback to finish refreshing
    capturedRequestBatch.callbacks.forEach { it.onBatchCompleted(capturedRequestBatch) }

    val currentAccessToken = checkNotNull(accessTokenManager.currentAccessToken)
    assertThat(currentAccessToken.token).isEqualTo("new token string")
    assertThat(currentAccessToken.permissions).containsExactlyInAnyOrder("walk")
    assertThat(currentAccessToken.declinedPermissions).containsExactlyInAnyOrder("chew gum")
  }

  @Test
  fun testRefreshingAccessTokenBlocksSecondAttempt() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    accessTokenManager.currentAccessToken = accessToken
    accessTokenManager.refreshCurrentAccessToken(null)
    var capturedException: FacebookException? = null
    accessTokenManager.refreshCurrentAccessToken(
        object : AccessToken.AccessTokenRefreshCallback {
          override fun OnTokenRefreshed(accessToken: AccessToken?) {
            return fail("AccessToken should not be refresh")
          }
          override fun OnTokenRefreshFailed(exception: FacebookException?) {
            capturedException = exception
          }
        })
    assertThat(capturedException).isNotNull
  }

  @Test
  fun testExtendFBAccessToken() {
    val accessToken = createAccessToken()
    whenever(accessTokenCache.load()).thenReturn(accessToken)
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.loadCurrentAccessToken()
    whenever(mockGraphRequestCompanionObject.executeBatchAsync(any<GraphRequestBatch>()))
        .thenReturn(mock<GraphRequestAsyncTask>())
    val mockRefreshTokenRequest = mock<GraphRequest>()
    whenever(
            mockGraphRequestCompanionObject.newGraphPathRequest(
                eq(accessToken), eq("oauth/access_token"), any()))
        .thenReturn(mockRefreshTokenRequest)
    val bundleArgumentCaptor = argumentCaptor<Bundle>()

    accessTokenManager.refreshCurrentAccessToken(null)

    verify(mockGraphRequestCompanionObject)
        .newGraphPathRequest(eq(accessToken), eq("me/permissions"), any())
    verify(mockGraphRequestCompanionObject)
        .newGraphPathRequest(eq(accessToken), eq("oauth/access_token"), any())
    verify(mockGraphRequest).httpMethod = HttpMethod.GET
    verify(mockRefreshTokenRequest).httpMethod = HttpMethod.GET
    verify(mockRefreshTokenRequest).parameters = bundleArgumentCaptor.capture()
    val capturedGrantType = bundleArgumentCaptor.firstValue.getString("grant_type")
    assertThat(capturedGrantType).contains("fb_extend_sso_token")
  }

  @Test
  fun testExtendIGAccessToken() {
    val accessToken = createAccessToken(TOKEN_STRING, USER_ID, "instagram")
    whenever(accessTokenCache.load()).thenReturn(accessToken)
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.loadCurrentAccessToken()

    val mockRefreshTokenRequest = mock<GraphRequest>()
    whenever(
            mockGraphRequestCompanionObject.newGraphPathRequest(
                eq(accessToken), eq("refresh_access_token"), any()))
        .thenReturn(mockRefreshTokenRequest)
    val bundleArgumentCaptor = argumentCaptor<Bundle>()

    accessTokenManager.refreshCurrentAccessToken(null)
    verify(mockGraphRequestCompanionObject)
        .newGraphPathRequest(eq(accessToken), eq("me/permissions"), any())
    verify(mockGraphRequestCompanionObject)
        .newGraphPathRequest(eq(accessToken), eq("refresh_access_token"), any())
    verify(mockGraphRequest).httpMethod = HttpMethod.GET
    verify(mockRefreshTokenRequest).parameters = bundleArgumentCaptor.capture()
    val capturedGrantType = bundleArgumentCaptor.firstValue.getString("grant_type")
    assertThat(capturedGrantType).contains("ig_refresh_token")
  }

  @Test
  fun `test current access token changed will broadcast`() {
    val accessTokenManager = createAccessTokenManager()
    val accessToken = createAccessToken()
    accessTokenManager.currentAccessToken = accessToken
    val receiver: BroadcastReceiver = mock()
    val intentCaptor = argumentCaptor<Intent>()
    localBroadcastManager.registerReceiver(receiver, mock())
    whenever(localBroadcastManager.sendBroadcast(intentCaptor.capture())).thenReturn(true)

    accessTokenManager.currentAccessTokenChanged()

    val capturedIntent = intentCaptor.firstValue
    assertThat(capturedIntent.action)
        .isEqualTo(AccessTokenManager.ACTION_CURRENT_ACCESS_TOKEN_CHANGED)
    assertThat(
            capturedIntent.getParcelableExtra<AccessToken>(
                AccessTokenManager.EXTRA_OLD_ACCESS_TOKEN))
        .isEqualTo(accessToken)
    assertThat(
            capturedIntent.getParcelableExtra<AccessToken>(
                AccessTokenManager.EXTRA_NEW_ACCESS_TOKEN))
        .isEqualTo(accessToken)
  }

  @Test
  fun `test getInstance`() {
    val instance = AccessTokenManager.getInstance()
    assertThat(instance).isNotNull
  }

  @Test
  fun `test extendAccessTokenIfNeeded when it should extend`() {
    val mockAccessToken = mock<AccessToken>()
    val mockAccessTokenSource = mock<AccessTokenSource>()
    whenever(mockAccessToken.source).thenReturn(mockAccessTokenSource)
    whenever(mockAccessTokenSource.canExtendToken()).thenReturn(true)
    whenever(mockAccessToken.lastRefresh).thenReturn(Date(1))
    whenever(mockAccessToken.toJSONObject()).thenReturn(JSONObject())
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.currentAccessToken = mockAccessToken
    accessTokenManager.extendAccessTokenIfNeeded()
    verify(mockGraphRequestCompanionObject).executeBatchAsync(any<GraphRequestBatch>())
  }

  @Test
  fun `test extendAccessTokenIfNeeded when the token cannot be extended`() {
    val mockAccessToken = mock<AccessToken>()
    val mockAccessTokenSource = mock<AccessTokenSource>()
    whenever(mockAccessToken.source).thenReturn(mockAccessTokenSource)
    whenever(mockAccessTokenSource.canExtendToken()).thenReturn(false)
    whenever(mockAccessToken.lastRefresh).thenReturn(Date(1))
    whenever(mockAccessToken.toJSONObject()).thenReturn(JSONObject())
    val accessTokenManager = createAccessTokenManager()
    accessTokenManager.currentAccessToken = mockAccessToken
    accessTokenManager.extendAccessTokenIfNeeded()
    verify(mockGraphRequestCompanionObject, never()).executeBatchAsync(any<GraphRequestBatch>())
  }

  private fun createAccessTokenManager(): AccessTokenManager {
    val accessTokenManager = AccessTokenManager(localBroadcastManager, accessTokenCache)
    Whitebox.setInternalState(AccessTokenManager::class.java, "instanceField", accessTokenManager)
    return accessTokenManager
  }

  private fun createAccessToken(
      tokenString: String = TOKEN_STRING,
      userId: String = USER_ID,
      graphDomain: String = "facebook"
  ): AccessToken {
    return AccessToken(
        tokenString,
        APP_ID,
        userId,
        PERMISSIONS,
        null,
        null,
        AccessTokenSource.WEB_VIEW,
        EXPIRES,
        LAST_REFRESH,
        DATA_ACCESS_EXPIRATION_TIME,
        graphDomain)
  }
}


/*
 * 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

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class AccessTokenSourceTest : FacebookTestCase() {
  @Test
  fun `test token sources from Instagram`() {
    assertThat(AccessTokenSource.INSTAGRAM_APPLICATION_WEB.fromInstagram()).isTrue()
    assertThat(AccessTokenSource.INSTAGRAM_CUSTOM_CHROME_TAB.fromInstagram()).isTrue()
    assertThat(AccessTokenSource.INSTAGRAM_WEB_VIEW.fromInstagram()).isTrue()
  }

  @Test
  fun `test token sources from Facebook`() {
    assertThat(AccessTokenSource.FACEBOOK_APPLICATION_WEB.fromInstagram()).isFalse()
    assertThat(AccessTokenSource.FACEBOOK_APPLICATION_NATIVE.fromInstagram()).isFalse()
    assertThat(AccessTokenSource.FACEBOOK_APPLICATION_SERVICE.fromInstagram()).isFalse()
    assertThat(AccessTokenSource.WEB_VIEW.fromInstagram()).isFalse()
    assertThat(AccessTokenSource.CHROME_CUSTOM_TAB.fromInstagram()).isFalse()
  }

  @Test
  fun `test non-extensible token type`() {
    assertThat(AccessTokenSource.NONE.canExtendToken()).isFalse()
  }
}