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

import android.media.Image
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import com.facebook.gamingservices.internal.DateFormatter
import com.facebook.gamingservices.internal.TournamentScoreType
import com.facebook.gamingservices.internal.TournamentSortOrder
import com.facebook.share.model.ShareModel
import com.facebook.share.model.ShareModelBuilder
import java.time.Instant

class TournamentConfig : ShareModel {

  /** Gets the tournament title. */
  val title: String?
  /** Gets the tournament sort order. */
  val sortOrder: TournamentSortOrder?
  /** Gets the tournament score format. */
  val scoreType: TournamentScoreType?
  /** Gets the tournament end time. */
  val endTime: Instant?
  /** Gets the tournament image. */
  val image: Image?
  /** Gets the tournament payload. */
  val payload: String?

  private constructor(builder: Builder) {
    title = builder.title
    sortOrder = builder.sortOrder
    scoreType = builder.scoreType
    endTime = builder.endTime
    image = builder.image
    payload = builder.payload
  }

  internal constructor(parcel: Parcel) {
    title = parcel.readString()
    sortOrder = TournamentSortOrder.values().find { it.name == parcel.readString() }
    scoreType = TournamentScoreType.values().find { it.name == parcel.readString() }
    endTime =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          parcel.readString()?.let {
            Instant.from(DateFormatter.format(it))
          }
        } else {
          null
        }
    payload = parcel.readString()
    image = null
  }

  override fun describeContents(): Int = 0

  override fun writeToParcel(out: Parcel, flags: Int) {
    out.writeString(sortOrder.toString())
    out.writeString(scoreType.toString())
    out.writeString(endTime.toString())
    out.writeString(title)
    out.writeString(payload)
  }

  /** Builder class for a concrete instance of TournamentCreationConfig */
  class Builder : ShareModelBuilder<TournamentConfig, Builder> {
    var title: String? = null
    var sortOrder: TournamentSortOrder? = null
    var scoreType: TournamentScoreType? = null
    var endTime: Instant? = null
    var image: Image? = null
    var payload: String? = null

    /**
     * Sets the tournament title
     *
     * @param title the title of the tournament
     * @return the builder
     */
    fun setTournamentTitle(title: String?): Builder {
      this.title = title
      return this
    }

    /**
     * Sets the sort order of the tournament.
     *
     * @param sortOrder The sort order for scores in the tournament
     * @return the builder
     */
    fun setTournamentSortOrder(sortOrder: TournamentSortOrder): Builder {
      this.sortOrder = sortOrder
      return this
    }

    /**
     * Sets the score type of the tournament.
     *
     * @param scoreType The type of score format for the tournament. {@link ScoreType}
     * @return the builder
     */
    fun setTournamentScoreType(scoreType: TournamentScoreType): Builder {
      this.scoreType = scoreType
      return this
    }

    /**
     * Sets the end time of the tournament.
     *
     * @param endTime The timestamp for the expiration of the tournament
     * @return the builder
     */
    fun setTournamentEndTime(endTime: Instant): Builder {
      this.endTime = endTime
      return this
    }

    /**
     * Sets the tournament image.
     *
     * @param image the payload
     * @return the builder
     */
    fun setTournamentImage(image: Image?): Builder {
      this.image = image
      return this
    }

    /**
     * Sets the tournament payload.
     *
     * @param payload the payload
     * @return the builder
     */
    fun setTournamentPayload(payload: String?): Builder {
      this.payload = payload
      return this
    }

    override fun build(): TournamentConfig {
      return TournamentConfig(this)
    }

    internal fun readFrom(parcel: Parcel): Builder {
      (parcel.readParcelable(TournamentConfig::class.java.classLoader) as TournamentConfig?).let {
        return this.readFrom(it)
      }
      return this
    }

    override fun readFrom(model: TournamentConfig?): Builder {
      if (model == null) {
        return this
      }
      model.sortOrder?.let { it -> setTournamentSortOrder(it) }
      model.scoreType?.let { it -> setTournamentScoreType(it) }
      model.endTime?.let { it -> setTournamentEndTime(it) }
      model.title?.let { it -> setTournamentTitle(it) }
      setTournamentPayload(model.payload)
      return this
    }
  }

  companion object CREATOR : Parcelable.Creator<TournamentConfig> {
    override fun createFromParcel(parcel: Parcel): TournamentConfig {
      return TournamentConfig(parcel)
    }

    override fun newArray(size: Int): Array<TournamentConfig?> {
      return arrayOfNulls(size)
    }
  }
}


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

import android.os.Bundle
import com.facebook.AccessToken
import com.facebook.AccessToken.Companion.getCurrentAccessToken
import com.facebook.FacebookException
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.HttpMethod
import com.facebook.bolts.TaskCompletionSource
import com.google.gson.GsonBuilder
import java.util.Locale
import org.json.JSONException

private const val GRAPH_RESPONSE_DATA_KEY = "data"

class TournamentFetcher {

  /**
   * Attempts to fetch the list tournaments where the current user is a participant
   *
   * @return TaskCompletionSource The task completion source that contains a list of tournaments as
   * a result
   */
  fun fetchTournaments(): TaskCompletionSource<List<Tournament>> {
    val task: TaskCompletionSource<List<Tournament>> = TaskCompletionSource<List<Tournament>>()
    val params = Bundle()

    val currentAccessToken: AccessToken? = getCurrentAccessToken()
    if (currentAccessToken == null || currentAccessToken.isExpired) {
      throw FacebookException("Attempted to fetch tournament with an invalid access token")
    }
    val isGamingLoggedIn =
        (currentAccessToken.graphDomain != null &&
            FacebookSdk.GAMING == currentAccessToken.graphDomain)
    if (!isGamingLoggedIn) {
      throw FacebookException("User is not using gaming login")
    }
    val request =
        GraphRequest(
            getCurrentAccessToken(),
            "me/tournaments",
            params,
            HttpMethod.GET,
            GraphRequest.Callback { response ->
              response.let { graphResponse ->
                if (graphResponse.error != null) {
                  if (graphResponse.error?.exception != null) {
                    task.setError(graphResponse.error?.exception)
                    return@Callback
                  }
                  task.setError(GraphAPIException("Graph API Error"))
                  return@Callback
                }
                try {
                  val jsonObject = graphResponse.getJSONObject()
                  if (jsonObject == null) {
                    val errorMessage = "Failed to get response"
                    task.setError(GraphAPIException(errorMessage))
                    return@Callback
                  }
                  val data = jsonObject?.getJSONArray(GRAPH_RESPONSE_DATA_KEY)
                  if (data == null || data.length() < 1) {
                    val errorMessage =
                        String.format(Locale.ROOT, "No tournament found", data.length(), 1)
                    task.setError(GraphAPIException(errorMessage))
                    return@Callback
                  }
                  val gson = GsonBuilder().create()
                  val dataString = data.toString()
                  val tournaments =
                      gson.fromJson(dataString, Array<Tournament>::class.java).toList()
                  task.setResult(tournaments)
                } catch (ex: JSONException) {
                  task.setError(ex)
                }
              }
            })
    request.parameters = params
    request.executeAsync()
    return task
  }
}

class GraphAPIException(message: String) : Exception(message)


// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.

package com.facebook.gamingservices

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.facebook.AccessToken
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.FacebookSdk
import com.facebook.gamingservices.cloudgaming.CloudGameLoginHandler
import com.facebook.gamingservices.cloudgaming.internal.SDKConstants
import com.facebook.gamingservices.internal.TournamentJoinDialogURIBuilder
import com.facebook.internal.*
import com.facebook.internal.CustomTabUtils.getChromePackage
import com.facebook.internal.CustomTabUtils.getDefaultRedirectURI
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import com.facebook.share.internal.ResultProcessor
import com.facebook.share.internal.ShareConstants
import com.facebook.share.internal.ShareInternalUtility

@AutoHandleExceptions
class TournamentJoinDialog : FacebookDialogBase<String?, TournamentJoinDialog.Result?> {
    companion object {
        private val DEFAULT_REQUEST_CODE =
                CallbackManagerImpl.RequestCodeOffset.TournamentJoinDialog.toRequestCode()
        private const val JOIN_TOURNAMENT_ACCESS_TOKEN_KEY = "access_token"
        private const val JOIN_TOURNAMENT_ACTION = "com.facebook.games.gaming_services.DEEPLINK"
        private const val JOIN_TOURNAMENT_CONTENT_TYPE = "text/plain"
        private const val JOIN_TOURNAMENT_DIALOG = "join_tournament"
        private const val JOIN_TOURNAMENT_ERROR_MESSAGE_KEY = "error_message"
    }

    private var tournamentID: String? = null
    private var requestID: Number? = null
    private var payload: String? = null

    /**
     * Constructs a new TournamentJoinDialog.
     *
     * @param activity Activity hosting the dialog.
     */
    constructor(activity: Activity) : super(activity, DEFAULT_REQUEST_CODE)

    /**
     * Constructs a new TournamentJoinDialog.
     *
     * @param fragment androidx.fragment.app.Fragment hosting the dialog.
     */
    constructor(fragment: Fragment) : this(FragmentWrapper(fragment))

    /**
     * Constructs a new TournamentJoinDialog.
     *
     * @param fragment android.app.Fragment hosting the dialog.
     */
    constructor(fragment: android.app.Fragment) : this(FragmentWrapper(fragment))
    private constructor(
            fragmentWrapper: FragmentWrapper
    ) : super(fragmentWrapper, DEFAULT_REQUEST_CODE)

    /**
     * Shows the tournament join dialog, where the user may join a specified tournament. If
     * no tournamentID is provided, the dialog present a list of suggested tournaments instead.
     *
     * @param tournamentID The tournament to be joined (optional).
     * @param payload The tournament payload.
     */
    fun show(tournamentID: String?, payload: String?) {
        this.tournamentID = tournamentID
        this.payload = payload
        super.showImpl(tournamentID, BASE_AUTOMATIC_MODE)
    }

    override fun showImpl(content: String?, mode: Any) {
        if (CloudGameLoginHandler.isRunningInCloud()) {
            return
        }
        super.showImpl(content, mode)
    }

    override fun registerCallbackImpl(
        callbackManager: CallbackManagerImpl,
        callback: FacebookCallback<Result?>
    ) {
        val resultProcessor: ResultProcessor =
                object : ResultProcessor(callback) {
                    override fun onSuccess(appCall: AppCall, results: Bundle?) {
                        if (results != null) {
                            if (results.getString(JOIN_TOURNAMENT_ERROR_MESSAGE_KEY) != null) {
                                callback.onError(FacebookException(results.getString(JOIN_TOURNAMENT_ERROR_MESSAGE_KEY)))
                                return
                            }

                            if (results.getString(SDKConstants.PARAM_PAYLOAD) != null) {
                                callback.onSuccess(Result(results))
                                return
                            }
                        }

                        onCancel(appCall)
                    }

                }
        callbackManager.registerCallback(requestCode) { resultCode, data ->
            ShareInternalUtility.handleActivityResult(requestCode, resultCode, data, resultProcessor)

        }
    }

    override val orderedModeHandlers: List<ModeHandler>
        get() {
            return listOf(FacebookAppHandler(), ChromeCustomTabHandler())
        }

    override fun createBaseAppCall(): AppCall {
        return AppCall(requestCode)
    }

    /**
     * The result of a successful TournamentJoinDialog.
     */
    class Result(results: Bundle) {
        var requestID: String? = null
        var tournamentID: String? = null
        var payload: String? = null

        init {
            if (results.getString(ShareConstants.WEB_DIALOG_RESULT_PARAM_REQUEST_ID) != null) {
                this.requestID = results.getString(ShareConstants.WEB_DIALOG_RESULT_PARAM_REQUEST_ID)
            }
            this.tournamentID = results.getString(SDKConstants.PARAM_TOURNAMENTS_ID)
            this.payload = results.getString(SDKConstants.PARAM_PAYLOAD)
        }
    }

    override fun canShow(content: String?): Boolean {
        return if (CloudGameLoginHandler.isRunningInCloud()) {
            false
        } else if (FacebookAppHandler().canShow(content, true)) {
            true
        } else {
            ChromeCustomTabHandler().canShow(content, true)
        }
    }

    private inner class FacebookAppHandler : ModeHandler() {
        override fun canShow(content: String?, isBestEffort: Boolean): Boolean {
            val packageManager: PackageManager = FacebookSdk.getApplicationContext().packageManager
            val intent = Intent(JOIN_TOURNAMENT_ACTION)
            intent.type = JOIN_TOURNAMENT_CONTENT_TYPE
            return intent.resolveActivity(packageManager) != null
        }

        override fun createAppCall(content: String?): AppCall {
            val currentAccessToken = AccessToken.getCurrentAccessToken()
            val appCall = createBaseAppCall()
            val intent = Intent(JOIN_TOURNAMENT_ACTION)
            intent.type = JOIN_TOURNAMENT_CONTENT_TYPE

            if (currentAccessToken == null || currentAccessToken.isExpired) {
                throw FacebookException("Attempted to present TournamentJoinDialog with an invalid access token")
            }
            if (currentAccessToken.graphDomain != null &&
                    FacebookSdk.GAMING != currentAccessToken.graphDomain) {
                throw FacebookException("Attempted to present TournamentJoinDialog while user is not gaming logged in")
            }
            val appID = currentAccessToken.applicationId
            val args = TournamentJoinDialogURIBuilder.bundle(appID, tournamentID, payload)

            NativeProtocol.setupProtocolRequestIntent(
                intent,
                appCall.callId.toString(),
                "",
                NativeProtocol.PROTOCOL_VERSION_20210906,
                args
            )
            appCall.requestIntent = intent

            return appCall
        }
    }

    private inner class ChromeCustomTabHandler : ModeHandler() {
        override fun canShow(content: String?, isBestEffort: Boolean): Boolean {
            val chromePackage = getChromePackage()
            return chromePackage != null
        }

        override fun createAppCall(content: String?): AppCall {
            val appCall = createBaseAppCall()
            val accessToken = AccessToken.getCurrentAccessToken()

            val params = Bundle()
            val payload = Bundle()

            params.putString(
                    ServerProtocol.DIALOG_PARAM_APP_ID,
                    accessToken?.applicationId ?: FacebookSdk.getApplicationId())
            params.putString(SDKConstants.PARAM_PAYLOAD, payload.toString())
            accessToken?.token.let {
                params.putString(JOIN_TOURNAMENT_ACCESS_TOKEN_KEY, accessToken?.token)
            }
            params.putString(
                    ServerProtocol.DIALOG_PARAM_REDIRECT_URI, getDefaultRedirectURI()) // fbconnect://cct.{packageName}
            DialogPresenter.setupAppCallForCustomTabDialog(
                    appCall, JOIN_TOURNAMENT_DIALOG, params)
            return appCall
        }
    }
}


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

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.facebook.AccessToken.Companion.getCurrentAccessToken
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.FacebookSdk
import com.facebook.FacebookSdk.getApplicationContext
import com.facebook.gamingservices.cloudgaming.CloudGameLoginHandler
import com.facebook.gamingservices.cloudgaming.internal.SDKConstants
import com.facebook.gamingservices.internal.TournamentShareDialogURIBuilder
import com.facebook.internal.AppCall
import com.facebook.internal.CallbackManagerImpl
import com.facebook.internal.FacebookDialogBase
import com.facebook.internal.FragmentWrapper
import com.facebook.internal.NativeProtocol
import com.facebook.internal.NativeProtocol.setupProtocolRequestIntent
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import com.facebook.share.internal.ResultProcessor
import com.facebook.share.internal.ShareConstants
import com.facebook.share.internal.ShareInternalUtility
import java.util.ArrayList

@AutoHandleExceptions
class TournamentShareDialog : FacebookDialogBase<TournamentConfig?, TournamentShareDialog.Result?> {
  companion object {
    private val defaultRequestCode =
        CallbackManagerImpl.RequestCodeOffset.TournamentShareDialog.toRequestCode()
  }

  var score: Number? = null
  var tournament: Tournament? = null

  /**
   * Constructs a new TournamentShareDialog.
   *
   * @param activity Activity hosting the dialog.
   */
  constructor(activity: Activity) : super(activity, defaultRequestCode)

  /**
   * Constructs a new TournamentShareDialog.
   *
   * @param fragment androidx.fragment.app.Fragment hosting the dialog.
   */
  constructor(fragment: Fragment) : this(FragmentWrapper(fragment))

  /**
   * Constructs a new TournamentShareDialog.
   *
   * @param fragment android.app.Fragment hosting the dialog.
   */
  constructor(fragment: android.app.Fragment) : this(FragmentWrapper(fragment))
  private constructor(fragmentWrapper: FragmentWrapper) : super(fragmentWrapper, defaultRequestCode)

  /**
   * Shows the tournament share dialog, where the user has the option to share a newly created
   * tournament with the provided score and configuration.
   *
   * @param score A number representing an score initial score for the tournament that will be
   * created with the provided config
   * @param newTournamentConfig A tournament configuration that will be used to create a new
   * tournament and post the provided score in said tournament
   */
  fun show(
      score: Number,
      newTournamentConfig: TournamentConfig,
  ) {
    this.score = score
    showImpl(newTournamentConfig, BASE_AUTOMATIC_MODE)
  }

  /**
   * Shows the tournament share dialog, where the user has the option to share the provided
   * tournament with the provided score if it's greater than their previous score.
   *
   * @param score A number representing a score
   * @param tournament An existing tournament that the user wants to post the provided score and
   * share.
   */
  fun show(score: Number, tournament: Tournament) {
    this.score = score
    this.tournament = tournament
    showImpl(null, BASE_AUTOMATIC_MODE)
  }

  override fun showImpl(content: TournamentConfig?, mode: Any) {
    if (CloudGameLoginHandler.isRunningInCloud()) {
      return
    }
    super.showImpl(content, mode)
  }

  override fun registerCallbackImpl(
      callbackManager: CallbackManagerImpl,
      callback: FacebookCallback<Result?>
  ) {
    val resultProcessor: ResultProcessor? =
        object : ResultProcessor(callback) {
          override fun onSuccess(appCall: AppCall, results: Bundle?) {
            if (results != null) {
              if (results.getString("error_message") != null) {
                callback.onError(FacebookException(results.getString("error_message")))
                return
              }

              if (results.getString(SDKConstants.PARAM_TOURNAMENTS_ID) != null) {
                callback.onSuccess(Result(results))
                return
              }
            }

            onCancel(appCall)
          }
        }
    callbackManager.registerCallback(requestCode) { resultCode, data ->
      ShareInternalUtility.handleActivityResult(requestCode, resultCode, data, resultProcessor)
    }
  }

  override val orderedModeHandlers: List<ModeHandler>
    get() {
      val handlers = ArrayList<ModeHandler>()
      handlers.add(FacebookAppHandler())
      handlers.add(AppSwitchHandler())
      return handlers
    }

  override fun createBaseAppCall(): AppCall {
    return AppCall(requestCode)
  }

  /*
   * Describes the result of a Tournament Share Dialog.
   *
   */
  class Result {
    var requestID: String? = null
    var tournamentID: String? = null

    constructor(results: Bundle) {
      if (results.getString(ShareConstants.WEB_DIALOG_RESULT_PARAM_REQUEST_ID) != null) {
        this.requestID = results.getString(ShareConstants.WEB_DIALOG_RESULT_PARAM_REQUEST_ID)
      }
      this.tournamentID = results.getString(SDKConstants.PARAM_TOURNAMENTS_ID)
    }
  }

  private inner class FacebookAppHandler : ModeHandler() {

    override fun canShow(content: TournamentConfig?, isBestEffort: Boolean): Boolean {
      val packageManager: PackageManager = getApplicationContext().packageManager
      val intent = Intent("com.facebook.games.gaming_services.DEEPLINK")
      intent.type = "text/plain"
      val fbAppCanShow = intent.resolveActivity(packageManager) != null

      return fbAppCanShow
    }

    override fun createAppCall(content: TournamentConfig?): AppCall {
      val currentAccessToken = getCurrentAccessToken()
      val appCall = createBaseAppCall()
      val intent = Intent("com.facebook.games.gaming_services.DEEPLINK")
      intent.type = "text/plain"

      if (currentAccessToken == null || currentAccessToken.isExpired) {
        throw FacebookException("Attempted to share tournament with an invalid access token")
      }
      if (currentAccessToken.graphDomain != null &&
          FacebookSdk.GAMING != currentAccessToken.graphDomain) {
        throw FacebookException("Attempted to share tournament while user is not gaming logged in")
      }
      val appID = currentAccessToken.applicationId
      val score = score ?: throw FacebookException("Attempted to share tournament without a score")
      val args =
          if (content != null)
              TournamentShareDialogURIBuilder.bundleForCreating(content, score, appID)
          else
              tournament?.let {
                TournamentShareDialogURIBuilder.bundleForUpdating(it.identifier, score, appID)
              }

      setupProtocolRequestIntent(
          intent, appCall.callId.toString(), "", NativeProtocol.PROTOCOL_VERSION_20210906, args)
      appCall.requestIntent = intent

      return appCall
    }
  }

  private inner class AppSwitchHandler : ModeHandler() {

    override fun canShow(content: TournamentConfig?, isBestEffort: Boolean): Boolean = true

    override fun createAppCall(content: TournamentConfig?): AppCall {
      val appCall: AppCall = this@TournamentShareDialog.createBaseAppCall()
      val currentAccessToken = getCurrentAccessToken()
      if (currentAccessToken == null || currentAccessToken.isExpired) {
        throw FacebookException("Attempted to share tournament with an invalid access token")
      }
      if (currentAccessToken.graphDomain != null &&
          FacebookSdk.GAMING != currentAccessToken.graphDomain) {
        throw FacebookException("Attempted to share tournament without without gaming login")
      }
      val score: Number =
          score ?: throw FacebookException("Attempted to share tournament without a score")

      val uri =
          if (content != null)
              TournamentShareDialogURIBuilder.uriForCreating(
                  content, score, currentAccessToken.applicationId)
          else {
            this@TournamentShareDialog.tournament?.let {
              TournamentShareDialogURIBuilder.uriForUpdating(
                  it.identifier, score, currentAccessToken.applicationId)
            }
          }

      val intent = Intent(Intent.ACTION_VIEW, uri)
      startActivityForResult(intent, requestCode)
      return appCall
    }
  }
}


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

import android.os.Bundle
import com.facebook.AccessToken
import com.facebook.AccessToken.Companion.getCurrentAccessToken
import com.facebook.FacebookException
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.HttpMethod
import com.facebook.bolts.TaskCompletionSource

private const val GRAPH_RESPONSE_SUCCESS_KEY = "success"

class TournamentUpdater {

  /**
   * Attempts to update the provided tournament with the provided score
   *
   * @return The task completion source that contains a boolean value on whether or not the Graph
   * API call succeed.
   */
  fun update(tournament: Tournament, score: Number): TaskCompletionSource<Boolean>? {
    return update(tournament.identifier, score)
  }

  /**
   * Attempts to update a tournament with the provided identifier and score
   *
   * @return The task completion source that contains a boolean value on whether or not the Graph
   * API call succeed.
   */
  fun update(identifier: String, score: Number): TaskCompletionSource<Boolean>? {
    val currentAccessToken: AccessToken? = getCurrentAccessToken()
    if (currentAccessToken == null || currentAccessToken.isExpired) {
      throw FacebookException("Attempted to fetch tournament with an invalid access token")
    }
    val isGamingLoggedIn =
        (currentAccessToken.graphDomain != null &&
            FacebookSdk.GAMING == currentAccessToken.graphDomain)
    if (!isGamingLoggedIn) {
      throw FacebookException("User is not using gaming login")
    }

    val task: TaskCompletionSource<Boolean> = TaskCompletionSource<Boolean>()
    val graphPath = "${identifier}/update_score"
    val params = Bundle()
    params.putInt("score", score.toInt())
    val request =
        GraphRequest(
            currentAccessToken,
            graphPath,
            params,
            HttpMethod.POST,
            GraphRequest.Callback { response ->
              if (response.error != null) {
                if (response.error?.exception != null) {
                  task.setError(response.error?.exception)
                  return@Callback
                }
                task.setError(GraphAPIException("Graph API Error"))
                return@Callback
              }
              val jsonObject = response.getJSONObject()
              val result = jsonObject?.optString(GRAPH_RESPONSE_SUCCESS_KEY)
              if (result == null || result.isEmpty()) {
                val errorMessage = "Graph API Error"
                task.setError(GraphAPIException(errorMessage))
                return@Callback
              }
              val success = result.equals("true")
              task.setResult(success)
            })
    request.executeAsync()
    return task
  }
}


/*
 * 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.gamingservices.internal

import android.os.Build
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

internal object DateFormatter {

  internal fun format(isoDate: String): ZonedDateTime? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")
      ZonedDateTime.parse(isoDate, formatter)
    } else {
      return null
    }
  }
}


/*
 * 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.gamingservices.internal

import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import com.facebook.AccessToken
import com.facebook.GraphRequest
import com.facebook.HttpMethod
import com.facebook.internal.Utility
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import java.io.File
import java.io.FileNotFoundException

/**
 * com.facebook.gamingservices.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.
 */
@AutoHandleExceptions
object GamingMediaUploader {

  private const val photoUploadEdge = "me/photos"

  @JvmStatic
  fun uploadToGamingServices(
      caption: String?,
      imageBitmap: Bitmap,
      params: Bundle?,
      callback: GraphRequest.Callback?
  ) =
      GraphRequest.newUploadPhotoRequest(
              AccessToken.getCurrentAccessToken(),
              photoUploadEdge,
              imageBitmap,
              caption,
              params,
              callback)
          .executeAsync()

  @JvmStatic
  @Throws(FileNotFoundException::class)
  fun uploadToGamingServices(
      caption: String?,
      imageFile: File,
      params: Bundle?,
      callback: GraphRequest.Callback?
  ) =
      GraphRequest.newUploadPhotoRequest(
              AccessToken.getCurrentAccessToken(),
              photoUploadEdge,
              imageFile,
              caption,
              params,
              callback)
          .executeAsync()

  @JvmStatic
  @Throws(FileNotFoundException::class)
  fun uploadToGamingServices(
      caption: String?,
      imageUri: Uri,
      params: Bundle?,
      callback: GraphRequest.Callback?
  ) =
      if (Utility.isFileUri(imageUri) || Utility.isContentUri(imageUri)) {
        GraphRequest.newUploadPhotoRequest(
                AccessToken.getCurrentAccessToken(),
                photoUploadEdge,
                imageUri,
                caption,
                params,
                callback)
            .executeAsync()
      } else {
        val parameters = Bundle()
        params?.let { parameters.putAll(it) }
        parameters.putString("url", imageUri.toString())
        if (!caption.isNullOrEmpty()) {
           parameters.putString("caption", caption)
        }
        GraphRequest(
                AccessToken.getCurrentAccessToken(),
                photoUploadEdge,
                parameters,
                HttpMethod.POST,
                callback)
            .executeAsync()
      }
}


// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.

package com.facebook.gamingservices.internal

import android.net.Uri
import android.os.Bundle
import com.facebook.FacebookSdk
import com.facebook.gamingservices.cloudgaming.internal.SDKConstants

internal object TournamentJoinDialogURIBuilder {

    private fun baseUriBuilder(): Uri.Builder {
        return Uri.Builder()
                .scheme("https")
                .authority(FacebookSdk.getFacebookGamingDomain())
                .appendPath("dialog")
                .appendPath("join_tournament")
    }

    // If we don't send a tournamentID, the user will be presented with a list of suggested tournaments.
    internal fun uri(tournamentID: String? = null, payload: String? = null): Uri {
        val builder = baseUriBuilder()
        tournamentID?.let {
            builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_ID, it)
        }
        payload?.let {
            builder.appendQueryParameter(SDKConstants.PARAM_PAYLOAD, it)
        }
        return builder.build()
    }

    internal fun bundle(
            appID: String,
            tournamentID: String? = null,
            payload: String? = null
    ): Bundle {
        val args = Bundle()
        args.putString(SDKConstants.PARAM_TOURNAMENTS_DEEPLINK, SDKConstants.PARAM_TOURNAMENTS)
        args.putString(SDKConstants.PARAM_TOURNAMENTS_APP_ID, appID)
        tournamentID?.let { args.putString(SDKConstants.PARAM_TOURNAMENTS_ID, it) }
        payload?.let { args.putString(SDKConstants.PARAM_PAYLOAD, it) }
        return args
    }
}


/*
 * 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.gamingservices.internal

/// The type of score a tournament can handle
enum class TournamentScoreType {
  NUMERIC,
  TIME,
}


/*
 * 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.gamingservices.internal

import android.net.Uri
import android.os.Build
import android.os.Bundle
import com.facebook.gamingservices.TournamentConfig
import com.facebook.gamingservices.cloudgaming.internal.SDKConstants

internal object TournamentShareDialogURIBuilder {
  const val scheme = "https"
  const val authority = "fb.gg"
  const val me = "me"
  const val tournament = "instant_tournament"

  internal fun uriForUpdating(tournamentID: String, score: Number, appID: String): Uri {
    return Uri.Builder()
        .scheme(scheme)
        .authority(authority)
        .appendPath(me)
        .appendPath(tournament)
        .appendPath(appID)
        .appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_ID, tournamentID)
        .appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_SCORE, score.toString())
        .build()
  }

  internal fun uriForCreating(config: TournamentConfig, score: Number, appID: String): Uri {
    val builder =
        Uri.Builder()
            .scheme(scheme)
            .authority(authority)
            .appendPath(me)
            .appendPath(tournament)
            .appendPath(appID)
            .appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_SCORE, score.toString())
    config.endTime?.let {
      builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_END_TIME, it.toString())
    }
    config.sortOrder?.let {
      builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_SORT_ORDER, it.toString())
    }
    config.scoreType?.let {
      builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_SCORE_FORMAT, it.toString())
    }
    config.title?.let { builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_TITLE, it) }
    config.payload?.let { builder.appendQueryParameter(SDKConstants.PARAM_TOURNAMENTS_PAYLOAD, it) }
    return builder.build()
  }

  internal fun bundleForUpdating(tournamentID: String, score: Number, appID: String): Bundle {
    val args = Bundle()
    args.putString(SDKConstants.PARAM_TOURNAMENTS_DEEPLINK, SDKConstants.PARAM_TOURNAMENTS)
    args.putString(SDKConstants.PARAM_TOURNAMENTS_APP_ID, appID)
    args.putString(SDKConstants.PARAM_TOURNAMENTS_SCORE, score.toString())
    args.putString(SDKConstants.PARAM_TOURNAMENTS_ID, tournamentID)
    return args
  }

  internal fun bundleForCreating(config: TournamentConfig, score: Number, appID: String): Bundle {
    val args = Bundle()
    args.putString(SDKConstants.PARAM_TOURNAMENTS_DEEPLINK, SDKConstants.PARAM_TOURNAMENTS)
    args.putString(SDKConstants.PARAM_TOURNAMENTS_APP_ID, appID)
    args.putString(SDKConstants.PARAM_TOURNAMENTS_SCORE, score.toString())

    config.sortOrder?.let {
      args.putString(SDKConstants.PARAM_TOURNAMENTS_SORT_ORDER, it.toString())
    }
    config.scoreType?.let {
      args.putString(SDKConstants.PARAM_TOURNAMENTS_SCORE_FORMAT, it.toString())
    }
    config.title?.let { args.putString(SDKConstants.PARAM_TOURNAMENTS_TITLE, it.toString()) }
    config.payload?.let { args.putString(SDKConstants.PARAM_TOURNAMENTS_PAYLOAD, it.toString()) }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      config.endTime?.let {
        val endTime = it.epochSecond.toInt()
        args.putString(SDKConstants.PARAM_TOURNAMENTS_END_TIME, endTime.toString())
      }
    }
    return args
  }
}


/*
 * 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.gamingservices.internal

// The sort order for how scores are ranked in the tournament.
enum class TournamentSortOrder(val rawValue: String) {
  LowerIsBetter("LOWER_IS_BETTER"),
  HigherIsBetter("HIGHER_IS_BETTER");

  override fun toString(): String {
    return rawValue
  }
}


/*
 * 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.gamingservices.model

import android.os.Parcel
import android.os.Parcelable
import com.facebook.share.model.ShareModel
import com.facebook.share.model.ShareModelBuilder
import java.util.Collections

class ContextChooseContent : ShareModel {
  private val filters: List<String>?
  val maxSize: Int?
  val minSize: Int?

  private constructor(builder: Builder) {
    filters = builder.filters
    maxSize = builder.maxSize
    minSize = builder.minSize
  }

  internal constructor(parcel: Parcel) {
    filters = parcel.createStringArrayList()
    maxSize = parcel.readInt()
    minSize = parcel.readInt()
  }

  fun getFilters(): List<String>? = filters?.let { Collections.unmodifiableList(it) }

  override fun writeToParcel(out: Parcel, flags: Int) {
    out.writeStringList(filters)
    out.writeInt(maxSize ?: 0)
    out.writeInt(minSize ?: 0)
  }

  override fun describeContents(): Int = 0

  /** Builder class for a concrete instance of ContextChooseContent */
  class Builder : ShareModelBuilder<ContextChooseContent, Builder> {
    internal var filters: List<String>? = null
    internal var maxSize: Int? = null
    internal var minSize: Int? = null

    /**
     * Sets the set of filters to apply to the context suggestions.
     *
     * @param filters the set of filter to apply
     * @return the builder
     */
    fun setFilters(filters: List<String>?): Builder {
      this.filters = filters
      return this
    }

    /**
     * Sets the maximum number of participants that a suggested context should ideally have.
     *
     * @param maxSize the maximum number of participants
     * @return the builder
     */
    fun setMaxSize(maxSize: Int?): Builder {
      this.maxSize = maxSize
      return this
    }

    /**
     * Sets the minimum number of participants that a suggested context should ideally have.
     *
     * @param minSize the minimum number of participants
     * @return the builder
     */
    fun setMinSize(minSize: Int?): Builder {
      this.minSize = minSize
      return this
    }

    override fun build(): ContextChooseContent = ContextChooseContent(this)

    override fun readFrom(model: ContextChooseContent?): Builder =
        model?.let { setFilters(it.getFilters()).setMaxSize(it.maxSize).setMinSize(it.minSize) }
            ?: this

    fun readFrom(parcel: Parcel): Builder =
        readFrom(
            parcel.readParcelable<Parcelable>(ContextChooseContent::class.java.classLoader)
                as ContextChooseContent?)
  }

  companion object CREATOR : Parcelable.Creator<ContextChooseContent> {
    override fun createFromParcel(parcel: Parcel): ContextChooseContent {
      return ContextChooseContent(parcel)
    }

    override fun newArray(size: Int): Array<ContextChooseContent?> {
      return arrayOfNulls(size)
    }
  }
}


/*
 * 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.gamingservices.model

import android.os.Parcel
import android.os.Parcelable
import com.facebook.share.model.ShareModel
import com.facebook.share.model.ShareModelBuilder

class ContextCreateContent : ShareModel {
  val suggestedPlayerID: String?

  private constructor(builder: Builder) {
    suggestedPlayerID = builder.suggestedPlayerID
  }

  internal constructor(parcel: Parcel) {
    suggestedPlayerID = parcel.readString()
  }

  override fun writeToParcel(out: Parcel, flags: Int) = out.writeString(suggestedPlayerID)

  override fun describeContents(): Int = 0

  /** Builder class for a concrete instance of ContextCreateContent */
  class Builder : ShareModelBuilder<ContextCreateContent, Builder> {
    internal var suggestedPlayerID: String? = null

    /**
     * Sets the string of the id of the suggested player
     *
     * @param suggestedPlayerID string of the id of the suggested player
     * @return the builder
     */
    fun setSuggestedPlayerID(suggestedPlayerID: String?): Builder {
      this.suggestedPlayerID = suggestedPlayerID
      return this
    }

    override fun build(): ContextCreateContent = ContextCreateContent(this)

    override fun readFrom(model: ContextCreateContent?): Builder =
        model?.let { setSuggestedPlayerID(it.suggestedPlayerID) } ?: this

    fun readFrom(parcel: Parcel): Builder =
        this.readFrom(
            parcel.readParcelable<Parcelable>(ContextCreateContent::class.java.classLoader)
                as ContextCreateContent?)
  }

  companion object CREATOR : Parcelable.Creator<ContextCreateContent> {
    override fun createFromParcel(parcel: Parcel): ContextCreateContent {
      return ContextCreateContent(parcel)
    }

    override fun newArray(size: Int): Array<ContextCreateContent?> {
      return arrayOfNulls(size)
    }
  }
}


/*
 * 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.gamingservices.model

import android.os.Parcel
import android.os.Parcelable
import com.facebook.share.model.ShareModel
import com.facebook.share.model.ShareModelBuilder

class ContextSwitchContent : ShareModel {
  val contextID: String?

  private constructor(builder: Builder) {
    contextID = builder.contextID
  }

  internal constructor(parcel: Parcel) {
    contextID = parcel.readString()
  }

  override fun writeToParcel(out: Parcel, flags: Int) = out.writeString(contextID)

  override fun describeContents(): Int = 0

  /** Builder class for a concrete instance of ContextSwitchContent */
  class Builder : ShareModelBuilder<ContextSwitchContent, Builder> {
    internal var contextID: String? = null

    /**
     * Sets the context ID that the player will switch into.
     *
     * @param contextID the context ID
     * @return the builder
     */
    fun setContextID(contextID: String?): Builder {
      this.contextID = contextID
      return this
    }

    override fun build(): ContextSwitchContent = ContextSwitchContent(this)

    override fun readFrom(model: ContextSwitchContent?): Builder =
        model?.let { setContextID(it.contextID) } ?: this

    fun readFrom(parcel: Parcel): Builder =
        this.readFrom(
            parcel.readParcelable<Parcelable>(ContextSwitchContent::class.java.classLoader)
                as ContextSwitchContent?)
  }

  companion object CREATOR : Parcelable.Creator<ContextSwitchContent> {
    override fun createFromParcel(parcel: Parcel): ContextSwitchContent {
      return ContextSwitchContent(parcel)
    }

    override fun newArray(size: Int): Array<ContextSwitchContent?> {
      return arrayOfNulls(size)
    }
  }
}


/*
 * 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.gamingservices.model

import android.graphics.Bitmap
import android.util.Base64
import com.facebook.gamingservices.GamingContext
import java.io.ByteArrayOutputStream
import java.lang.IllegalArgumentException
import java.util.HashMap
import org.json.JSONObject

private const val CONTEXT_TOKEN_KEY = "context_token_id"
private const val CTA_KEY = "cta"
private const val DATA_KEY = "data"
private const val DEFAULT_KEY = "default"
private const val GIF_KEY = "gif"
private const val IMAGE_KEY = "image"
private const val LOCALIZATIONS_KEY = "localizations"
private const val MEDIA_KEY = "media"
private const val TEXT_KEY = "text"
private const val URL_KEY = "url"
private const val VIDEO_KEY = "video"

/**
 * Represents a text string that can have different Locale values provided.
 *
 * @property default The default text to use if an override for the locale is not included in the
 * localizations Map.
 * @property localizations A key-value Map of Locale_Code: Locale String Value for this text. For a
 * list of valid locale codes see:
 * https://lookaside.facebook.com/developers/resources/?id=FacebookLocales.xml
 *
 * Example: default: Hello , localizations {'es_LA': 'hola'}
 */
data class CustomUpdateLocalizedText(
    val default: String,
    val localizations: HashMap<String, String>? = null
) {
  fun toJSONObject(): JSONObject {
    val out = JSONObject()
    out.put(DEFAULT_KEY, default)
    localizations?.let {
      val localizationsJson = JSONObject()
      for ((k, v) in it) {
        localizationsJson.put(k, v)
      }
      out.put(LOCALIZATIONS_KEY, localizationsJson)
    }
    return out
  }
}

/**
 * Represents a media that will be included in a Custom Update Message
 *
 * <b>Note:</b> gif and video are mutually exclusive.
 *
 * @property gif Gif that will be included in the Update Message
 * @property video Video that will be included in the Update Message. Currently this is not yet
 * supported but will be in a server side update so it is already included in the SDK. this
 * disclaimer will be removed as soon as it is.
 */
data class CustomUpdateMedia(
    val gif: CustomUpdateMediaInfo? = null,
    val video: CustomUpdateMediaInfo? = null,
) {
  fun toJSONObject(): JSONObject {
    val out = JSONObject()
    gif?.let {
      val gifObject = JSONObject()
      gifObject.put(URL_KEY, it.url)
      out.put(GIF_KEY, gifObject)
    }
    video?.let {
      val videoObject = JSONObject()
      videoObject.put(URL_KEY, it.url)
      out.put(VIDEO_KEY, videoObject)
    }
    return out
  }
}

/**
 * Stores Information about a Media that will be part of a Custom Update.
 *
 * @property url: The URL to fetch this media from.
 */
data class CustomUpdateMediaInfo(val url: String)

class CustomUpdateContent
private constructor(
    val contextTokenId: String,
    val text: CustomUpdateLocalizedText,
    val cta: CustomUpdateLocalizedText? = null,
    val image: String? = null,
    val media: CustomUpdateMedia? = null,
    val data: String? = null
) {

  /**
   * Converts this CustomUpdateContent to a JSONObject to be used by a GraphRequest
   *
   * This will be called automatically by CustomUpdate.newCustomUpdateRequest()
   *
   * @return a JSONObject that can be attached to a GraphRequest
   */
  fun toGraphRequestContent(): JSONObject {
    var json = JSONObject()
    json.put(CONTEXT_TOKEN_KEY, contextTokenId)
    json.put(TEXT_KEY, text.toJSONObject().toString())
    cta?.let { json.put(CTA_KEY, it.toJSONObject().toString()) }
    image?.let { json.put(IMAGE_KEY, it) }
    media?.let { json.put(MEDIA_KEY, it.toJSONObject().toString()) }
    data?.let { json.put(DATA_KEY, it) }
    return json
  }

  class Builder
  private constructor(
      private val contextTokenId: String?,
      private val text: CustomUpdateLocalizedText,
      private val image: Bitmap?,
      private val media: CustomUpdateMedia?,
  ) {
    /**
     * Creates a CustomUpdateContent Builder
     *
     * @param contextToken GamingContext A valid GamingContext to send the update to.
     * @param text The text that will be included in the update
     * @param image An image that will be included in the update
     */
    constructor(
        contextToken: GamingContext,
        text: CustomUpdateLocalizedText,
        image: Bitmap
    ) : this(contextToken.contextID, text, image, null)
    /**
     * Creates a CustomUpdateContent Builder
     *
     * @param contextToken GamingContext A valid GamingContext to send the update to.
     * @param text The text that will be included in the update
     * @param media A gif or video that will be included in the update
     */
    constructor(
        contextToken: GamingContext,
        text: CustomUpdateLocalizedText,
        media: CustomUpdateMedia,
    ) : this(contextToken.contextID, text, null, media)

    var cta: CustomUpdateLocalizedText? = null
      private set

    var data: String? = null
      private set

    /**
     * Sets the CTA (Call to Action) text in the update message
     *
     * If none is provided then a localized version of 'play' will be used.
     *
     * @param cta The Custom CTA to use.
     */
    fun setCta(cta: CustomUpdateLocalizedText): Builder {
      this.cta = cta
      return this
    }

    /**
     * Sets a Data that will be sent back to the game when a user clicks on the message. When the
     * game is launched from a Custom Update message the data here will be forwarded as a Payload.
     * Please use the GamingPayload class to retrieve it.
     *
     * @param data A String that will be sent back to the game.
     */
    fun setData(data: String): Builder {
      this.data = data
      return this
    }

    /**
     * Returns a CustomUpdateContent with the values defined in this builder.
     *
     * @return CustomUpdateContent instance that can be used with
     * CustomUpdate.newCustomUpdateRequest()
     * @throws IllegalArgumentException if CustomUpdateMedia is invalid or if the ContextToken is
     * null.
     */
    fun build(): CustomUpdateContent {
      if (media != null && !((media.gif != null).xor(media.video != null))) {
        throw IllegalArgumentException("Invalid CustomUpdateMedia, please set either gif or video")
      }

      var imageStr = bitmapToBase64String(image)
      if (this.contextTokenId == null) {
        throw IllegalArgumentException("parameter contextToken must not be null")
      }
      return CustomUpdateContent(
          this.contextTokenId, this.text, this.cta, imageStr, this.media, this.data)
    }

    private fun bitmapToBase64String(bitmap: Bitmap?): String? {
      if (bitmap == null) {
        return null
      }
      val outputStream = ByteArrayOutputStream()
      bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)

      return "data:image/png;base64," +
          Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
    }
  }
}


/*
 * 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.graphics.Bitmap
import com.facebook.gamingservices.GamingContext
import com.facebook.gamingservices.model.CustomUpdateContent
import com.facebook.gamingservices.model.CustomUpdateLocalizedText
import com.facebook.gamingservices.model.CustomUpdateMedia
import com.facebook.gamingservices.model.CustomUpdateMediaInfo
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class CustomUpdateContentTest : FacebookPowerMockTestCase() {
  @Test
  fun `constructor with media`() {
    val content =
        CustomUpdateContent.Builder(
                GamingContext("1233"),
                CustomUpdateLocalizedText("text"),
                CustomUpdateMedia(CustomUpdateMediaInfo("https://www.facebook.com/cool.gif")))
            .build()
    assertThat(content.contextTokenId).isEqualTo("1233")
    assertThat(content.text.default).isEqualTo("text")
  }

  fun `constructor with media gif and video`() {
    try {
      val builder =
          CustomUpdateContent.Builder(
                  GamingContext("1233"),
                  CustomUpdateLocalizedText("text"),
                  CustomUpdateMedia(
                      CustomUpdateMediaInfo("https://www.facebook.com/cool.gif"),
                      CustomUpdateMediaInfo("https://www.facebook.com/cool.mp4")))
              .build()
    } catch (e: IllegalArgumentException) {
      assertThat(e).isNotNull()
      assertThat(e.message).startsWith("Invalid CustomUpdateMedia")
    }
  }

  @Test
  fun `constructor with media and empty gif and empty video`() {
    try {
      val builder =
          CustomUpdateContent.Builder(
                  GamingContext("1233"),
                  CustomUpdateLocalizedText("text"),
                  CustomUpdateMedia(null, null))
              .build()
    } catch (e: IllegalArgumentException) {
      assertThat(e).isNotNull()
      assertThat(e.message).startsWith("Invalid CustomUpdateMedia")
    }
  }

  @Test
  fun `constructor with image`() {
    val content =
        CustomUpdateContent.Builder(
                GamingContext("1233"),
                CustomUpdateLocalizedText("text"),
                Bitmap.createBitmap(200, 200, Bitmap.Config.ALPHA_8))
            .build()
    assertThat(content.contextTokenId).isEqualTo("1233")
    assertThat(content.image).isNotNull()
  }

  @Test
  fun `bitmap is converted to Base64`() {
    val content =
        CustomUpdateContent.Builder(
                GamingContext("1233"),
                CustomUpdateLocalizedText("text"),
                Bitmap.createBitmap(200, 200, Bitmap.Config.ALPHA_8))
            .build()
    assertThat(content.image).isNotNull()
    assertThat(content.image).startsWith("data:image/png;base64,")
  }

  @Test
  fun `toGraphRequestContent returns valid JSON`() {
    val localizations = hashMapOf<String, String>()
    localizations.put("es_LA", "abc")
    val content =
        CustomUpdateContent.Builder(
                GamingContext("1233"),
                CustomUpdateLocalizedText("text", localizations),
                CustomUpdateMedia(CustomUpdateMediaInfo("https://www.facebook.com/cool.gif")))
            .build()
    val json = content.toGraphRequestContent()
    assertThat(json.getString("context_token_id")).isEqualTo("1233")
    assertThat(json.getString("text"))
        .isEqualTo("""{"default":"text","localizations":{"es_LA":"abc"}}""")
    assertThat(json.getString("media"))
        .isEqualTo("""{"gif":{"url":"https:\/\/www.facebook.com\/cool.gif"}}""")
  }
}


package com.facebook

import android.net.Uri
import com.facebook.gamingservices.internal.TournamentJoinDialogURIBuilder
import org.junit.Assert.assertEquals
import org.junit.Test

class TournamentJoinDialogURIBuilderTest: FacebookPowerMockTestCase() {
    @Test
    fun `base uri`() {
        val uri = TournamentJoinDialogURIBuilder.uri()
        val expected = Uri.parse("https://fb.gg/dialog/join_tournament")
        assertEquals("URI builder produced the wrong URI", uri.toString(), expected.toString())
    }

    @Test
    fun `uri with tournament_id`() {
        val uri = TournamentJoinDialogURIBuilder.uri("123")
        val expected = Uri
                .parse("https://fb.gg/dialog/join_tournament")
                .buildUpon()
                .appendQueryParameter("tournament_id", "123")
                .build()
        assertEquals("URI builder produced the wrong URI", uri.toString(), expected.toString())
    }

    @Test
    fun `uri with payload`() {
        val uri = TournamentJoinDialogURIBuilder.uri(null, "test")
        val expected = Uri
                .parse("https://fb.gg/dialog/join_tournament")
                .buildUpon()
                .appendQueryParameter("payload", "test")
                .build()
        assertEquals("URI builder produced the wrong URI", uri.toString(), expected.toString())
    }

    @Test
    fun `uri with tournament_id and payload`() {
        val uri = TournamentJoinDialogURIBuilder.uri("123", "test")
        val expected = Uri
                .parse("https://fb.gg/dialog/join_tournament")
                .buildUpon()
                .appendQueryParameter("tournament_id", "123")
                .appendQueryParameter("payload", "test")
                .build()
        assertEquals("URI builder produced the wrong URI", uri.toString(), expected.toString())
    }
}


/*
 * 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 android.net.Uri
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions

/** This class manages device login and permissions for Facebook. */
@AutoHandleExceptions
class DeviceLoginManager : LoginManager() {

  /**
   * Uri to redirect the user to after they complete the device login flow on the external device.
   *
   * The Uri must be configured in your App Settings -> Advanced -> OAuth Redirect URIs.
   */
  var deviceRedirectUri: Uri? = null

  /** Target user id for the device request, if any. */
  var deviceAuthTargetUserId: String? = null

  override fun createLoginRequest(permissions: Collection<String>?): LoginClient.Request {
    val request = super.createLoginRequest(permissions)
    deviceRedirectUri?.let { request.deviceRedirectUriString = it.toString() }
    deviceAuthTargetUserId?.let { request.deviceAuthTargetUserId = it }
    return request
  }

  companion object {
    val instance: DeviceLoginManager by lazy { DeviceLoginManager() }
  }
}


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

class Login


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

import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import com.facebook.login.DeviceLoginManager
import com.facebook.login.LoginBehavior
import com.facebook.login.LoginManager

/**
 * A Log In/Log Out button that maintains login state and logs in/out for the app.
 *
 * This control requires the app ID and client token to be specified in the AndroidManifest.xml.
 */
class DeviceLoginButton : LoginButton {
  /**
   * Uri that will be used to redirect the user to after they complete the
   * device login flow on the external device.
   *
   * The Uri must be configured in your App Settings -> Advanced -> OAuth Redirect URIs.
   */
  var deviceRedirectUri: Uri? = null

  /**
   * Create the LoginButton by inflating from XML and applying a style.
   *
   * @see LoginButton
   */
  constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

  /**
   * Create the LoginButton by inflating from XML
   *
   * @see LoginButton
   */
  constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

  /**
   * Create the LoginButton by inflating from XML
   *
   * @see LoginButton
   */
  constructor(context: Context) : super(context)

  override val newLoginClickListener: LoginClickListener
    get() = DeviceLoginClickListener()

  @AutoHandleExceptions
  private inner class DeviceLoginClickListener : LoginClickListener() {
    override fun getLoginManager(): LoginManager {
      val manager = DeviceLoginManager.instance
      manager.setDefaultAudience(defaultAudience)
      manager.setLoginBehavior(LoginBehavior.DEVICE_AUTH)
      manager.deviceRedirectUri = deviceRedirectUri
      return manager
    }
  }
}


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

import android.annotation.TargetApi
import android.app.AlertDialog
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import com.facebook.AccessToken
import com.facebook.AccessTokenTracker
import com.facebook.CallbackManager
import com.facebook.FacebookButtonBase
import com.facebook.FacebookCallback
import com.facebook.FacebookSdk
import com.facebook.Profile
import com.facebook.appevents.InternalAppEventsLogger
import com.facebook.internal.AnalyticsEvents
import com.facebook.internal.CallbackManagerImpl
import com.facebook.internal.FetchedAppSettings
import com.facebook.internal.FetchedAppSettingsManager
import com.facebook.internal.ServerProtocol
import com.facebook.internal.Utility
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import com.facebook.login.DefaultAudience
import com.facebook.login.LoginBehavior
import com.facebook.login.LoginManager
import com.facebook.login.LoginResult
import com.facebook.login.LoginTargetApp
import com.facebook.login.R
import java.util.UUID
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min

/**
 * A Log In/Log Out button that maintains login state and logs in/out for the app.
 *
 * This control requires the app ID to be specified in the AndroidManifest.xml.
 */
open class LoginButton
protected constructor(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int,
    analyticsButtonCreatedEventName: String,
    analyticsButtonTappedEventName: String
) : FacebookButtonBase(
    context,
    attrs,
    defStyleAttr,
    defStyleRes,
    analyticsButtonCreatedEventName,
    analyticsButtonTappedEventName
) {
  // ***
  // Keep all the enum values in sync with attrs.xml
  // ***
  /** The display modes for the login button tool tip.  */
  enum class ToolTipMode(private val stringValue: String, val intValue: Int) {
    /**
     * Default display mode. A server query will determine if the tool tip should be displayed and,
     * if so, what the string shown to the user should be.
     */
    AUTOMATIC("automatic", 0),

    /** Display the tool tip with a local string--regardless of what the server returns  */
    DISPLAY_ALWAYS("display_always", 1),

    /** Never display the tool tip--regardless of what the server says  */
    NEVER_DISPLAY("never_display", 2);

    override fun toString(): String {
      return stringValue
    }

    companion object {
      val DEFAULT = AUTOMATIC

      fun fromInt(enumValue: Int): ToolTipMode? = values().find { it.intValue == enumValue }
    }
  }

  private var confirmLogout = false

  var loginText: String? = null
    set(value) {
      field = value
      setButtonText()
    }

  var logoutText: String? = null
    set(value) {
      field = value
      setButtonText()
    }

  protected val properties: LoginButtonProperties = LoginButtonProperties()

  private var toolTipChecked = false

  /**
   * Style (background) of the Tool Tip popup. Currently a blue style and a black style are
   * supported. Blue is default
   */
  var toolTipStyle = ToolTipPopup.Style.BLUE

  /**
   * The mode of the Tool Tip popup. Currently supported modes are default (normal behavior),
   * always_on (popup remains up until forcibly dismissed), and always_off (popup doesn't show)
   */
  var toolTipMode: ToolTipMode = ToolTipMode.DEFAULT

  /**
   * The amount of time (in milliseconds) that the tool tip will be shown to the user. The
   * default is {@value com.facebook.login.widget.ToolTipPopup#DEFAULT_POPUP_DISPLAY_TIME}. Any
   * value that is less than or equal to zero will cause the tool tip to be displayed indefinitely.
   */
  var toolTipDisplayTime = ToolTipPopup.DEFAULT_POPUP_DISPLAY_TIME

  private var toolTipPopup: ToolTipPopup? = null
  private var accessTokenTracker: AccessTokenTracker? = null

  protected var loginManagerLazy: Lazy<LoginManager> = lazy { LoginManager.getInstance() }

  private var customButtonRadius: Float? = null
  private var customButtonTransparency: Int = MAX_BUTTON_TRANSPARENCY

  /**
   * The logger ID associated with the LoginButton
   */
  val loggerID = UUID.randomUUID().toString()

  /** The callback manager registered to the login button. */
  var callbackManager: CallbackManager? = null
    private set

  private var androidXLoginCaller: ActivityResultLauncher<Collection<String>>? = null

  open class LoginButtonProperties {
    var defaultAudience = DefaultAudience.FRIENDS
    var permissions: List<String> = emptyList()
    var loginBehavior = LoginBehavior.NATIVE_WITH_FALLBACK
    var authType = ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE
    var loginTargetApp = LoginTargetApp.FACEBOOK
    var shouldSkipAccountDeduplication = false
      protected set
    var messengerPageId: String? = null
    var resetMessengerState = false

    fun clearPermissions() {
      permissions = emptyList()
    }
  }

  /**
   * Create the LoginButton by inflating from XML
   */
  constructor(context: Context) : this(
      context,
      null,
      0,
      0,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_CREATE,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_DID_TAP
  )

  /**
   * Create the LoginButton by inflating from XML
   */
  constructor(context: Context, attrs: AttributeSet?) : this(
      context,
      attrs,
      0,
      0,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_CREATE,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_DID_TAP
  )

  /**
   * Create the LoginButton by inflating from XML and applying a style.
   */
  constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : this(
      context,
      attrs,
      defStyle,
      0,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_CREATE,
      AnalyticsEvents.EVENT_LOGIN_BUTTON_DID_TAP
  )

  /**
   * Default audience to use when the user logs in. This value is only useful when
   * specifying publish permissions for the native login dialog.
   */
  var defaultAudience: DefaultAudience
    get() = properties.defaultAudience
    set(value) {
      properties.defaultAudience = value
    }

  /**
   * Set the permissions to use when the user logs in. The permissions here can only be read
   * permissions. If any publish permissions are included, the login attempt by the user will fail.
   * The LoginButton can only be associated with either read permissions or publish permissions, but
   * not both. Calling both setReadPermissions and setPublishPermissions on the same instance of
   * LoginButton will result in an exception being thrown unless clearPermissions is called in
   * between.
   *
   * This method is only meaningful if called before the user logs in. If this is called after
   * login, and the list of permissions passed in is not a subset of the permissions granted during
   * the authorization, it will log an error.
   *
   * It's important to always pass in a consistent set of permissions to this method, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   *
   * @param permissions the read permissions to use
   * @throws UnsupportedOperationException if setPublishPermissions has been called
   */
  @Deprecated("Use setPermissions instead", replaceWith = ReplaceWith("setPermissions"))
  fun setReadPermissions(permissions: List<String>) {
    properties.permissions = permissions
  }

  /**
   * Set the permissions to use when the user logs in. The permissions here can only be read
   * permissions. If any publish permissions are included, the login attempt by the user will fail.
   * The LoginButton can only be associated with either read permissions or publish permissions, but
   * not both. Calling both setReadPermissions and setPublishPermissions on the same instance of
   * LoginButton will result in an exception being thrown unless clearPermissions is called in
   * between.
   *
   * This method is only meaningful if called before the user logs in. If this is called after
   * login, and the list of permissions passed in is not a subset of the permissions granted during
   * the authorization, it will log an error.
   *
   * It's important to always pass in a consistent set of permissions to this method, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   *
   * @param permissions the read permissions to use
   * @throws UnsupportedOperationException if setPublishPermissions has been called
   */
  @Deprecated("Use setPermissions instead", replaceWith = ReplaceWith("setPermissions"))
  fun setReadPermissions(vararg permissions: String?) {
    properties.permissions = listOfNotNull(*permissions)
  }

  /**
   * Set the permissions to use when the user logs in. The permissions here can be either read or
   * write permissions.
   *
   * This method is only meaningful if called before the user logs in. If this is called
   * after login, and the list of permissions passed in is not a subset of the permissions granted
   * during the authorization, it will log an error.
   *
   * It's important to always pass in a consistent set of permissions to this method, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   *
   * @param permissions the read and write permissions to use
   */
  fun setPermissions(vararg permissions: String?) {
    properties.permissions = listOfNotNull(*permissions)
  }

  /**
   * Set the permissions to use when the user logs in. The permissions here should only be publish
   * permissions. If any read permissions are included, the login attempt by the user may fail. The
   * LoginButton can only be associated with either read permissions or publish permissions, but not
   * both. Calling both setReadPermissions and setPublishPermissions on the same instance of
   * LoginButton will result in an exception being thrown unless clearPermissions is called in
   * between.
   *
   * This method is only meaningful if called before the user logs in. If this is called after
   * login, and the list of permissions passed in is not a subset of the permissions granted during
   * the authorization, it will log an error.
   *
   * It's important to always pass in a consistent set of permissions to this method, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   *
   * @param permissions the publish permissions to use
   * @throws UnsupportedOperationException if setReadPermissions has been called
   * @throws IllegalArgumentException if permissions is null or empty
   */
  @Deprecated("Use setPermissions instead", replaceWith = ReplaceWith("setPermissions"))
  fun setPublishPermissions(permissions: List<String>) {
    properties.permissions = permissions
  }

  /**
   * Set the permissions to use when the user logs in. The permissions here should only be publish
   * permissions. If any read permissions are included, the login attempt by the user may fail. The
   * LoginButton can only be associated with either read permissions or publish permissions, but not
   * both. Calling both setReadPermissions and setPublishPermissions on the same instance of
   * LoginButton will result in an exception being thrown unless clearPermissions is called in
   * between.
   *
   * This method is only meaningful if called before the user logs in. If this is called after
   * login, and the list of permissions passed in is not a subset of the permissions granted during
   * the authorization, it will log an error.
   *
   * It's important to always pass in a consistent set of permissions to this method, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   *
   * @param permissions the publish permissions to use
   * @throws UnsupportedOperationException if setReadPermissions has been called
   * @throws IllegalArgumentException if permissions is null or empty
   */
  @Deprecated("Use setPermissions instead", replaceWith = ReplaceWith("setPermissions"))
  fun setPublishPermissions(vararg permissions: String?) {
    properties.permissions = listOfNotNull(*permissions)
  }

  /** Clears the permissions currently associated with this LoginButton. */
  fun clearPermissions() {
    properties.clearPermissions()
  }

  /**
   * The [LoginBehavior][com.facebook.login.LoginBehavior] that specifies what behaviors
   * should be attempted during authorization. If it is null, the default
   * [LoginBehavior.NATIVE_WITH_FALLBACK] will be used.
   */
  var loginBehavior: LoginBehavior
    get() = properties.loginBehavior
    set(value) {
      properties.loginBehavior = value
    }

  /**
   * The [LoginTargetApp] that specifies what app in the Facebook Family of Apps will be used for
   * authorization. If null is specified, the default [LoginTargetApp.FACEBOOK] will be used.
   */
  var loginTargetApp: LoginTargetApp
    get() = properties.loginTargetApp
    set(value) {
      properties.loginTargetApp = value
    }

  /**
   * The authType to be used.
   */
  var authType: String
    get() = properties.authType
    set(value) {
      properties.authType = value
    }

  /**
   * The messengerPageId being used.
   */
  var messengerPageId: String?
    get() = properties.messengerPageId
    set(value) {
      properties.messengerPageId = value
    }

  /**
   * The resetMessengerState being used.
   */
  var resetMessengerState: Boolean
    get() = properties.resetMessengerState
    set(value) {
      properties.resetMessengerState = value
    }

  /**
   * Indicates if we should skip account deduplication. The setter is only available in
   * FamilyLoginButton as this is an x-FoA specific feature. Default is false(not skipping).
   */
  val shouldSkipAccountDeduplication: Boolean
    get() = properties.shouldSkipAccountDeduplication

  /** Dismisses the Tooltip if it is currently visible */
  fun dismissToolTip() {
    toolTipPopup?.dismiss()
    toolTipPopup = null
  }

  /**
   * Registers a login callback to the given callback manager.
   *
   * @param callbackManager The callback manager that will encapsulate the callback.
   * @param callback The login callback that will be called on login completion.
   */
  fun registerCallback(callbackManager: CallbackManager, callback: FacebookCallback<LoginResult>) {
    loginManagerLazy.value.registerCallback(callbackManager, callback)
    if (this.callbackManager == null) {
      this.callbackManager = callbackManager
    } else if (this.callbackManager !== callbackManager) {
      Log.w(
          TAG,
          "You're registering a callback on the one Facebook login button with two different callback managers. "
              + "It's almost wrong and may cause unexpected results. "
              + "Only the first callback manager will be used for handling activity result with androidx."
      )
    }
  }

  /**
   * Unregisters a login callback to the given callback manager.
   *
   * @param callbackManager The callback manager that will encapsulate the callback.
   */
  fun unregisterCallback(callbackManager: CallbackManager) =
      loginManagerLazy.value.unregisterCallback(callbackManager)

  @get:StringRes
  protected val loginButtonContinueLabel: Int
    get() = R.string.com_facebook_loginview_log_in_button_continue

  @AutoHandleExceptions
  override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    if (context is ActivityResultRegistryOwner) {
      val context = context as ActivityResultRegistryOwner
      androidXLoginCaller = context
          .activityResultRegistry
          .register(
              "facebook-login",
              loginManagerLazy.value.createLogInActivityResultContract(callbackManager, loggerID)
          ) { }
    }
    accessTokenTracker?.let {
      if (it.isTracking) {
        it.startTracking()
        setButtonText()
      }
    }
  }

  @AutoHandleExceptions
  override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    if (!toolTipChecked && !isInEditMode) {
      toolTipChecked = true
      checkToolTipSettings()
    }
  }

  @AutoHandleExceptions
  private fun showToolTipPerSettings(settings: FetchedAppSettings?) {
    if (settings != null && settings.nuxEnabled && visibility == VISIBLE) {
      displayToolTip(settings.nuxContent)
    }
  }

  @AutoHandleExceptions
  private fun displayToolTip(toolTipString: String) {
    val toolTipPopup = ToolTipPopup(toolTipString, this)
    toolTipPopup.setStyle(toolTipStyle)
    toolTipPopup.setNuxDisplayTime(toolTipDisplayTime)
    toolTipPopup.show()
    this.toolTipPopup = toolTipPopup
  }

  @AutoHandleExceptions
  private fun checkToolTipSettings() {
    when (toolTipMode) {
      ToolTipMode.AUTOMATIC -> {
        // kick off an async request
        val appId = Utility.getMetadataApplicationId(context)
        FacebookSdk.getExecutor().execute {
          val settings = FetchedAppSettingsManager.queryAppSettings(appId, false)
          activity.runOnUiThread { showToolTipPerSettings(settings) }
        }
      }
      ToolTipMode.DISPLAY_ALWAYS -> {
        val toolTipString = resources.getString(R.string.com_facebook_tooltip_default)
        displayToolTip(toolTipString)
      }
      ToolTipMode.NEVER_DISPLAY -> {}
    }
  }

  @AutoHandleExceptions
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    setButtonText()
  }

  @AutoHandleExceptions
  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    androidXLoginCaller?.unregister()
    accessTokenTracker?.stopTracking()
    dismissToolTip()
  }

  @AutoHandleExceptions
  override fun onVisibilityChanged(changedView: View, visibility: Int) {
    super.onVisibilityChanged(changedView, visibility)
    // If the visibility is not VISIBLE, we want to dismiss the tooltip if it is there
    if (visibility != VISIBLE) {
      dismissToolTip()
    }
  }

  /**
   * The permissions to use when the user logs in. The permissions here can be either read or
   * write permissions.
   *
   * Setting this field is only meaningful if called before the user logs in. If this is set
   * after login, and the list of permissions passed in is not a subset of the permissions granted
   * during the authorization, it will log an error.
   *
   * It's important to always set a consistent set of permissions to this field, or manage
   * the setting of permissions outside of the LoginButton class altogether (by using the
   * LoginManager explicitly).
   */
  // For testing purposes only
  var permissions: List<String>
    get() = properties.permissions
    set(value) {
      properties.permissions = value
    }

  @AutoHandleExceptions
  override fun configureButton(
      context: Context,
      attrs: AttributeSet?,
      defStyleAttr: Int,
      defStyleRes: Int) {
    super.configureButton(context, attrs, defStyleAttr, defStyleRes)
    setInternalOnClickListener(newLoginClickListener)

    parseLoginButtonAttributes(context, attrs, defStyleAttr, defStyleRes)

    if (isInEditMode) {
      // cannot use a drawable in edit mode, so setting the background color instead
      // of a background resource.
      setBackgroundColor(resources.getColor(com.facebook.common.R.color.com_facebook_blue))
      // hardcoding in edit mode as getResources().getString() doesn't seem to work in
      // Android Studio
      loginText = "Continue with Facebook"
    } else {
      accessTokenTracker = object : AccessTokenTracker() {
        override fun onCurrentAccessTokenChanged(
            oldAccessToken: AccessToken?, currentAccessToken: AccessToken?) {
          setButtonText()
          setButtonIcon()
        }
      }
    }
    setButtonText()
    setButtonRadius()
    setButtonTransparency()
    setButtonIcon()
  }

  protected open val newLoginClickListener: LoginClickListener
    get() = LoginClickListener()

  override val defaultStyleResource: Int
    get() = R.style.com_facebook_loginview_default_style

  @AutoHandleExceptions
  protected fun parseLoginButtonAttributes(
      context: Context,
      attrs: AttributeSet?,
      defStyleAttr: Int,
      defStyleRes: Int) {
    toolTipMode = ToolTipMode.DEFAULT
    val styleAttributes =
        context
            .theme
            .obtainStyledAttributes(
                attrs, R.styleable.com_facebook_login_view, defStyleAttr, defStyleRes)
    try {
      confirmLogout =
          styleAttributes.getBoolean(R.styleable.com_facebook_login_view_com_facebook_confirm_logout, true)
      loginText = styleAttributes.getString(R.styleable.com_facebook_login_view_com_facebook_login_text)
      logoutText = styleAttributes.getString(R.styleable.com_facebook_login_view_com_facebook_logout_text)
      toolTipMode =
          ToolTipMode.fromInt(
              styleAttributes.getInt(
                  R.styleable.com_facebook_login_view_com_facebook_tooltip_mode,
                  ToolTipMode.DEFAULT.intValue)) ?: ToolTipMode.DEFAULT
      // If no button radius specified, defaults to 'com_facebook_button_corner_radius'
      if (styleAttributes.hasValue(R.styleable.com_facebook_login_view_com_facebook_login_button_radius)) {
        customButtonRadius =
            styleAttributes.getDimension(
                R.styleable.com_facebook_login_view_com_facebook_login_button_radius, 0.0f)
      }
      customButtonTransparency =
          styleAttributes.getInteger(
              R.styleable.com_facebook_login_view_com_facebook_login_button_transparency,
              MAX_BUTTON_TRANSPARENCY)
      customButtonTransparency = max(MIN_BUTTON_TRANSPARENCY, customButtonTransparency)
      customButtonTransparency = min(MAX_BUTTON_TRANSPARENCY, customButtonTransparency)
    } finally {
      styleAttributes.recycle()
    }
  }

  @AutoHandleExceptions
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val fontMetrics = paint.fontMetrics
    val height =
        (compoundPaddingTop
            + ceil((abs(fontMetrics.top) + abs(fontMetrics.bottom)).toDouble()).toInt()
            + compoundPaddingBottom)

    val resources = resources
    val logInWidth = getLoginButtonWidth(widthMeasureSpec)

    val text = logoutText ?: resources.getString(R.string.com_facebook_loginview_log_out_button)

    val logOutWidth = measureButtonWidth(text)

    val width = resolveSize(max(logInWidth, logOutWidth), widthMeasureSpec)
    setMeasuredDimension(width, height)
  }

  @AutoHandleExceptions
  protected fun getLoginButtonWidth(widthMeasureSpec: Int): Int {
    val resources = resources
    var logInWidth: Int
    var text = loginText
    val width: Int
    if (text == null) {
      text = resources.getString(R.string.com_facebook_loginview_log_in_button_continue)
      logInWidth = measureButtonWidth(text)
      width = resolveSize(logInWidth, widthMeasureSpec)
      if (width < logInWidth) {
        text = resources.getString(R.string.com_facebook_loginview_log_in_button)
      }
    }
    logInWidth = measureButtonWidth(text)
    return logInWidth
  }

  @AutoHandleExceptions
  private fun measureButtonWidth(text: String): Int {
    val textWidth = measureTextWidth(text)
    return (compoundPaddingLeft
        + compoundDrawablePadding
        + textWidth
        + compoundPaddingRight)
  }

  @AutoHandleExceptions
  protected fun setButtonText() {
    val resources = resources
    if (!isInEditMode && AccessToken.isCurrentAccessTokenActive()) {
      text = logoutText ?: resources.getString(R.string.com_facebook_loginview_log_out_button)
    } else {
      if (loginText != null) {
        text = loginText
      } else {
        var text = resources.getString(loginButtonContinueLabel)
        val width = width
        // if the width is 0, we are going to measure size, so use the long text
        if (width != 0) {
          // we have a specific width, check if the long text fits
          val measuredWidth = measureButtonWidth(text)
          if (measuredWidth > width) {
            // it doesn't fit, use the shorter text
            text = resources.getString(R.string.com_facebook_loginview_log_in_button)
          }
        }
        setText(text)
      }
    }
  }

  @AutoHandleExceptions
  protected fun setButtonIcon() {
    this.setCompoundDrawablesWithIntrinsicBounds(
        AppCompatResources.getDrawable(
            context, com.facebook.common.R.drawable.com_facebook_button_icon),
        null,
        null,
        null)
  }

  @TargetApi(29)
  @AutoHandleExceptions
  protected fun setButtonRadius() {
    val customButtonRadius = this.customButtonRadius ?: return
    val buttonDrawable = this.background
    // Only configure corner radius for GradientDrawables or wrappers
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
        && buttonDrawable is StateListDrawable) {
      for (i in 0 until buttonDrawable.stateCount) {
        val childDrawable = buttonDrawable.getStateDrawable(i) as? GradientDrawable
        childDrawable?.let { it.cornerRadius = customButtonRadius }
      }
    }
    if (buttonDrawable is GradientDrawable) {
      buttonDrawable.cornerRadius = customButtonRadius
    }
  }

  @AutoHandleExceptions
  protected fun setButtonTransparency() {
    background.alpha = customButtonTransparency
  }

  @get:AutoHandleExceptions
  override val defaultRequestCode: Int
    get() = CallbackManagerImpl.RequestCodeOffset.Login.toRequestCode()

  @AutoHandleExceptions
  protected open inner class LoginClickListener : OnClickListener {
    override fun onClick(v: View) {
      callExternalOnClickListener(v)
      val accessToken = AccessToken.getCurrentAccessToken()
      val accessTokenActive = AccessToken.isCurrentAccessTokenActive()
      if (accessTokenActive) {
        // Log out
        performLogout(context)
      } else {
        performLogin()
      }
      val logger = InternalAppEventsLogger(context)
      val parameters = Bundle()
      parameters.putInt("logging_in", if (accessToken != null) 0 else 1)
      parameters.putInt("access_token_expired", if (accessTokenActive) 1 else 0)
      logger.logEventImplicitly(AnalyticsEvents.EVENT_LOGIN_VIEW_USAGE, parameters)
    }

    protected fun performLogin() {
      val loginManager = getLoginManager()
      val androidXLoginCaller = androidXLoginCaller
      if (androidXLoginCaller != null) {
        // if no callback manager is registered with the button, an empty callback manager is
        // created for calling the static callbacks.
        val resultContact = androidXLoginCaller.contract
            as LoginManager.FacebookLoginActivityResultContract

        resultContact.callbackManager = callbackManager ?: CallbackManagerImpl()
        androidXLoginCaller.launch(properties.permissions)
      } else if (fragment !== null) {
        fragment?.let { loginManager.logIn(it, properties.permissions, loggerID) }
      } else if (nativeFragment !== null) {
        nativeFragment?.let { loginManager.logIn(it, properties.permissions, loggerID) }
      } else {
        loginManager.logIn(activity, properties.permissions, loggerID)
      }
    }

    protected fun performLogout(context: Context) {
      val loginManager = getLoginManager()
      if (confirmLogout) {
        // Create a confirmation dialog
        val logout = resources.getString(R.string.com_facebook_loginview_log_out_action)
        val cancel = resources.getString(R.string.com_facebook_loginview_cancel_action)
        val message: String
        val profile = Profile.getCurrentProfile()
        message = if (profile?.name != null) {
          String.format(
              resources.getString(R.string.com_facebook_loginview_logged_in_as), profile.name)
        } else {
          resources.getString(R.string.com_facebook_loginview_logged_in_using_facebook)
        }
        val builder = AlertDialog.Builder(context)
        builder
            .setMessage(message)
            .setCancelable(true)
            .setPositiveButton(logout) { _, _ -> loginManager.logOut() }
            .setNegativeButton(cancel, null)
        builder.create().show()
      } else {
        loginManager.logOut()
      }
    }

    protected open fun getLoginManager(): LoginManager {
      val manager = LoginManager.getInstance()
      manager.setDefaultAudience(defaultAudience)
      manager.setLoginBehavior(loginBehavior)
      manager.setLoginTargetApp(loginTargetApp)
      manager.setAuthType(authType)
      manager.setFamilyLogin(isFamilyLogin)
      manager.setShouldSkipAccountDeduplication(shouldSkipAccountDeduplication)
      manager.setMessengerPageId(messengerPageId)
      manager.setResetMessengerState(resetMessengerState)
      return manager
    }

    protected val loginTargetApp: LoginTargetApp
      get() = LoginTargetApp.FACEBOOK
    protected val isFamilyLogin: Boolean
      get() = false
  }

  companion object {
    private val TAG = LoginButton::class.java.name
    private const val MAX_BUTTON_TRANSPARENCY = 255
    private const val MIN_BUTTON_TRANSPARENCY = 0
  }
}