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

import android.net.Uri
import android.os.Parcel
import com.facebook.FacebookTestCase
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class ShareVideoContentTest : FacebookTestCase() {
  private lateinit var content: ShareVideoContent

  override fun setUp() {
    super.setUp()
    val shareVideo = ShareVideo.Builder().setLocalUrl(Uri.parse("/tmp/test.avi")).build()
    val previewPhoto = SharePhoto.Builder().setImageUrl(Uri.parse("/tmp/test.jpg")).build()
    content =
        ShareVideoContent.Builder()
            .setVideo(shareVideo)
            .setContentTitle("test title")
            .setContentDescription("test description")
            .setPreviewPhoto(previewPhoto)
            .build()
  }

  @Test
  fun `test build share video content with ShareVideo object`() {
    assertThat(content.video?.localUrl.toString()).isEqualTo("/tmp/test.avi")
    assertThat(content.contentDescription).isEqualTo("test description")
    assertThat(content.contentTitle).isEqualTo("test title")
    assertThat(content.previewPhoto?.imageUrl.toString()).isEqualTo("/tmp/test.jpg")
  }

  @Test
  fun `test parcelize`() {
    val parcel = Parcel.obtain()
    content.writeToParcel(parcel, 0)
    parcel.setDataPosition(0)

    val recoveredContent = ShareVideoContent.CREATOR.createFromParcel(parcel)
    assertThat(recoveredContent.video?.localUrl.toString()).isEqualTo("/tmp/test.avi")
    assertThat(recoveredContent.contentDescription).isEqualTo("test description")
    assertThat(recoveredContent.contentTitle).isEqualTo("test title")
    assertThat(recoveredContent.previewPhoto?.imageUrl.toString()).isEqualTo("/tmp/test.jpg")
  }

  @Test
  fun `test builder read from existing content`() {
    val recoveredContent = ShareVideoContent.Builder().readFrom(content).build()
    assertThat(recoveredContent.video?.localUrl.toString()).isEqualTo("/tmp/test.avi")
    assertThat(recoveredContent.contentDescription).isEqualTo("test description")
    assertThat(recoveredContent.contentTitle).isEqualTo("test title")
    assertThat(recoveredContent.previewPhoto?.imageUrl.toString()).isEqualTo("/tmp/test.jpg")
  }
}


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

import android.net.Uri
import android.os.Parcel
import com.facebook.FacebookTestCase
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test

class ShareVideoTest : FacebookTestCase() {
  private lateinit var shareVideo: ShareVideo
  @Before
  fun init() {
    shareVideo = ShareVideo.Builder().setLocalUrl(Uri.parse("https://facebook.com")).build()
  }

  @Test
  fun `test build video`() {
    assertThat(shareVideo).isNotNull
  }

  @Test
  fun `test get video`() {
    assertThat(shareVideo.localUrl.toString()).isEqualTo("https://facebook.com")
  }

  @Test
  fun `test serialize with parcel`() {
    val parcel = Parcel.obtain()
    parcel.writeParcelable(shareVideo, 0)
    parcel.setDataPosition(0)

    val recoveredContent = parcel.readParcelable<ShareVideo>(ShareVideo::class.java.classLoader)
    checkNotNull(recoveredContent)
    assertThat(recoveredContent.localUrl).isEqualTo(shareVideo.localUrl)
    parcel.recycle()
  }

  @Test
  fun `test builder read from existing content`() {
    val recoveredContent = ShareVideo.Builder().readFrom(shareVideo).build()
    assertThat(recoveredContent.localUrl).isEqualTo(shareVideo.localUrl)
  }
}


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

import android.app.Activity
import android.net.Uri
import android.os.Parcel
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultRegistry
import com.facebook.AccessToken
import com.facebook.FacebookPowerMockTestCase
import com.facebook.FacebookSdk
import com.facebook.appevents.InternalAppEventsLogger
import com.facebook.internal.AppCall
import com.facebook.internal.DialogPresenter
import com.facebook.internal.FragmentWrapper
import com.facebook.share.internal.ShareDialogFeature
import com.facebook.share.internal.ShareInternalUtility
import com.facebook.share.model.ShareContent
import com.facebook.share.model.ShareLinkContent
import com.facebook.share.model.ShareMediaContent
import com.facebook.share.model.SharePhotoContent
import com.facebook.share.model.ShareVideoContent
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.kotlin.KArgumentCaptor
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.whenever
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.reflect.Whitebox

@PrepareForTest(ShareInternalUtility::class, FacebookSdk::class, DialogPresenter::class)
class ShareDialogTest : FacebookPowerMockTestCase() {
  private lateinit var testShareLinkContent: ShareLinkContent
  private lateinit var staticShareCallbackRequestCodeCaptor: KArgumentCaptor<Int>
  private lateinit var mockInternalAppEventsLogger: InternalAppEventsLogger
  private lateinit var mockAccessTokenCompanion: AccessToken.Companion

  override fun setup() {
    super.setup()
    testShareLinkContent =
        ShareLinkContent.Builder().setContentUrl(Uri.parse("https://facebook.com/")).build()

    PowerMockito.mockStatic(FacebookSdk::class.java)
    whenever(FacebookSdk.isInitialized()).thenReturn(true)
    whenever(FacebookSdk.getApplicationId()).thenReturn("123456789")

    staticShareCallbackRequestCodeCaptor = argumentCaptor()
    PowerMockito.mockStatic(ShareInternalUtility::class.java)
    whenever(
            ShareInternalUtility.registerStaticShareCallback(
                staticShareCallbackRequestCodeCaptor.capture()))
        .thenAnswer {}

    PowerMockito.mockStatic(DialogPresenter::class.java)

    mockInternalAppEventsLogger = mock()
    val mockInternalAppEventsLoggerCompanion = mock<InternalAppEventsLogger.Companion>()
    whenever(mockInternalAppEventsLoggerCompanion.createInstance(anyOrNull(), anyOrNull()))
        .thenReturn(mockInternalAppEventsLogger)
    Whitebox.setInternalState(
        InternalAppEventsLogger::class.java, "Companion", mockInternalAppEventsLoggerCompanion)

    mockAccessTokenCompanion = mock()
    Whitebox.setInternalState(AccessToken::class.java, "Companion", mockAccessTokenCompanion)
  }

  @Test
  fun `test show share dialog with an Android legacy activity will call DialogPresenter with the activity`() {
    val mockActivity = mock<Activity>()
    val appCallCaptor = argumentCaptor<AppCall>()
    var isPresentCalled = false
    whenever(DialogPresenter.present(appCallCaptor.capture(), eq(mockActivity))).thenAnswer {
      isPresentCalled = true
      Unit
    }

    ShareDialog.show(mockActivity, testShareLinkContent)

    val capturedAppCall = appCallCaptor.firstValue
    // verify DialogPresenter.present is called
    assertThat(isPresentCalled).isTrue
    // verify the request code is registered as a static callback
    assertThat(capturedAppCall.requestCode)
        .isEqualTo(staticShareCallbackRequestCodeCaptor.firstValue)
  }

  @Test
  fun `test show share dialog with an AndroidX activity will call DialogPresenter with the activity result registry`() {
    val mockActivity = mock<ComponentActivity>()
    val mockActivityResultRegistry = mock<ActivityResultRegistry>()
    whenever(mockActivity.activityResultRegistry).thenReturn(mockActivityResultRegistry)
    val appCallCaptor = argumentCaptor<AppCall>()
    var isPresentCalled = false
    whenever(
            DialogPresenter.present(
                appCallCaptor.capture(), eq(mockActivityResultRegistry), anyOrNull()))
        .thenAnswer {
          isPresentCalled = true
          Unit
        }

    ShareDialog.show(mockActivity, testShareLinkContent)

    val capturedAppCall = appCallCaptor.firstValue
    // verify DialogPresenter.present is called
    assertThat(isPresentCalled).isTrue
    // verify the request code is registered as a static callback
    assertThat(capturedAppCall.requestCode)
        .isEqualTo(staticShareCallbackRequestCodeCaptor.firstValue)
  }

  @Test
  fun `test show share dialog with an AndroidX fragment will call DialogPresenter with the activity result registry`() {
    val mockActivity = mock<androidx.fragment.app.FragmentActivity>()
    val mockActivityResultRegistry = mock<ActivityResultRegistry>()
    val mockFragment = mock<androidx.fragment.app.Fragment>()
    whenever(mockFragment.activity).thenReturn(mockActivity)
    whenever(mockActivity.activityResultRegistry).thenReturn(mockActivityResultRegistry)
    val appCallCaptor = argumentCaptor<AppCall>()
    var isPresentCalled = false
    whenever(
            DialogPresenter.present(
                appCallCaptor.capture(), eq(mockActivityResultRegistry), anyOrNull()))
        .thenAnswer {
          isPresentCalled = true
          Unit
        }

    ShareDialog.show(mockFragment, testShareLinkContent)

    val capturedAppCall = appCallCaptor.firstValue
    // verify DialogPresenter.present is called
    assertThat(isPresentCalled).isTrue
    // verify the request code is registered as a static callback
    assertThat(capturedAppCall.requestCode)
        .isEqualTo(staticShareCallbackRequestCodeCaptor.firstValue)
  }

  @Test
  fun `test show share dialog with a legacy fragment will call DialogPresenter with the activity result registry`() {
    val mockActivity = mock<Activity>()
    val mockFragment = mock<android.app.Fragment>()
    whenever(mockFragment.activity).thenReturn(mockActivity)
    val appCallCaptor = argumentCaptor<AppCall>()
    val fragmentWrapperCaptor = argumentCaptor<FragmentWrapper>()
    var isPresentCalled = false
    whenever(DialogPresenter.present(appCallCaptor.capture(), fragmentWrapperCaptor.capture()))
        .thenAnswer {
          isPresentCalled = true
          Unit
        }

    ShareDialog.show(mockFragment, testShareLinkContent)

    val capturedAppCall = appCallCaptor.firstValue
    // verify DialogPresenter.present is called
    assertThat(isPresentCalled).isTrue
    assertThat(fragmentWrapperCaptor.firstValue.nativeFragment).isEqualTo(mockFragment)
    // verify the request code is registered as a static callback
    assertThat(capturedAppCall.requestCode)
        .isEqualTo(staticShareCallbackRequestCodeCaptor.firstValue)
  }

  @Test
  fun `test canShow return true for link content type if the access token and the native app are not available`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(false)

    assertThat(ShareDialog.canShow(ShareLinkContent::class.java)).isTrue
  }

  @Test
  fun `test canShow return true for link content type if the access token is available and the native app are not available`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(true)

    assertThat(ShareDialog.canShow(ShareLinkContent::class.java)).isTrue
  }
  @Test
  fun `test canShow return false for photo content type if the access token and the native app are not available`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(false)

    assertThat(ShareDialog.canShow(SharePhotoContent::class.java)).isFalse
  }

  @Test
  fun `test canShow return true for photo content type if the access token is available and the native app are not available`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(true)

    assertThat(ShareDialog.canShow(SharePhotoContent::class.java)).isTrue
  }

  @Test
  fun `test canShow return false for video content type if native app are not available`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)

    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(true)
    assertThat(ShareDialog.canShow(ShareVideoContent::class.java)).isFalse
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(false)
    assertThat(ShareDialog.canShow(ShareVideoContent::class.java)).isFalse
  }

  @Test
  fun `test canShow return true for video content type if native app supports video feature`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(eq(ShareDialogFeature.VIDEO)))
        .thenReturn(true)

    assertThat(ShareDialog.canShow(ShareVideoContent::class.java)).isTrue
  }

  @Test
  fun `test canShow return false for media content type if native app does not multimedia feature`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(false)

    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(true)
    assertThat(ShareDialog.canShow(ShareMediaContent::class.java)).isFalse
    whenever(mockAccessTokenCompanion.isCurrentAccessTokenActive()).thenReturn(false)
    assertThat(ShareDialog.canShow(ShareMediaContent::class.java)).isFalse
  }

  @Test
  fun `test canShow return true for media content type if native app supports multimedia feature`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(ShareDialogFeature.MULTIMEDIA))
        .thenReturn(true)

    assertThat(ShareDialog.canShow(ShareMediaContent::class.java)).isTrue
  }

  @Test
  fun `test canShow return false for unknown content type`() {
    whenever(DialogPresenter.canPresentNativeDialogWithFeature(any())).thenReturn(true)

    assertThat(ShareDialog.canShow(UnknownContent::class.java)).isFalse
  }

  @Test
  fun `test canShow a link content with web mode is always true`() {
    val mockActivity = mock<Activity>()
    val shareDialog = ShareDialog(mockActivity)
    val shareLinkContent =
        ShareLinkContent.Builder().setContentUrl(Uri.parse("http://facebook.com")).build()

    whenever(AccessToken.isCurrentAccessTokenActive()).thenReturn(true)
    assertThat(shareDialog.canShow(shareLinkContent, ShareDialog.Mode.WEB)).isTrue
    whenever(AccessToken.isCurrentAccessTokenActive()).thenReturn(false)
    assertThat(shareDialog.canShow(shareLinkContent, ShareDialog.Mode.WEB)).isTrue
  }

  @Test
  fun `test canShow a photo content with web mode is true if access token is active`() {
    val mockActivity = mock<Activity>()
    val mockFragment = mock<android.app.Fragment>()
    whenever(mockFragment.activity).thenReturn(mockActivity)
    val shareDialog = ShareDialog(mockFragment)
    val sharePhotoContent =
        SharePhotoContent.Builder().setContentUrl(Uri.parse("http://facebook.com")).build()

    whenever(AccessToken.isCurrentAccessTokenActive()).thenReturn(true)
    assertThat(shareDialog.canShow(sharePhotoContent, ShareDialog.Mode.WEB)).isTrue
    whenever(AccessToken.isCurrentAccessTokenActive()).thenReturn(false)
    assertThat(shareDialog.canShow(sharePhotoContent, ShareDialog.Mode.WEB)).isFalse
  }

  private class UnknownContent : ShareContent<UnknownContent, UnknownContent.Builder> {
    constructor(builder: Builder) : super(builder)
    constructor(parcel: Parcel) : super(parcel)

    inner class Builder : ShareContent.Builder<UnknownContent, Builder>() {
      override fun build(): UnknownContent {
        return UnknownContent(this)
      }
    }
  }
}


/*
 * 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.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.text.TextUtils
import com.facebook.internal.Utility
import com.facebook.internal.Utility.awaitGetGraphMeRequestWithCache
import com.facebook.internal.Utility.getBundleLongAsDate
import com.facebook.internal.Utility.getGraphMeRequestWithCacheAsync
import com.facebook.internal.Utility.isNullOrEmpty
import com.facebook.internal.Utility.jsonArrayToStringList
import com.facebook.internal.Validate
import java.lang.Exception
import java.util.Collections
import java.util.Date
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

/**
 * This class represents an immutable access token for using Facebook APIs. It also includes
 * associated metadata such as expiration date and permissions.
 *
 * For more information on access tokens, see
 * [Access Tokens](https://developers.facebook.com/docs/facebook-login/access-tokens/).
 */
class AccessToken : Parcelable {
  /**
   * Gets the date at which the access token expires.
   *
   * @return the expiration date of the token
   */
  val expires: Date

  /**
   * Gets the list of permissions associated with this access token. Note that the most up-to-date
   * list of permissions is maintained by Facebook, so this list may be outdated if permissions have
   * been added or removed since the time the AccessToken object was created. For more information
   * on permissions, see https://developers.facebook.com/docs/reference/login/#permissions.
   *
   * @return a read-only list of strings representing the permissions granted via this access token
   */
  val permissions: Set<String?>

  /**
   * Gets the list of permissions declined by the user with this access token. It represents the
   * entire set of permissions that have been requested and declined. Note that the most up-to-date
   * list of permissions is maintained by Facebook, so this list may be outdated if permissions have
   * been granted or declined since the last time an AccessToken object was created.
   *
   * @return a read-only list of strings representing the permissions declined by the user
   */
  val declinedPermissions: Set<String?>

  /**
   * Gets the list of permissions that were expired with this access token.
   *
   * @return a read-only list of strings representing the expired permissions
   */
  val expiredPermissions: Set<String?>

  /**
   * Gets the string representing the access token.
   *
   * @return the string representing the access token
   */
  val token: String

  /**
   * Gets the [AccessTokenSource] indicating how this access token was obtained.
   *
   * @return the enum indicating how the access token was obtained
   */
  val source: AccessTokenSource

  /**
   * Gets the date at which the token was last refreshed. Since tokens expire, the Facebook SDK will
   * attempt to renew them periodically.
   *
   * @return the date at which this token was last refreshed
   */
  val lastRefresh: Date

  /**
   * Gets the ID of the Facebook Application associated with this access token.
   *
   * @return the application ID
   */
  val applicationId: String

  /**
   * Returns the user id for this access token.
   *
   * @return The user id for this access token.
   */
  val userId: String

  /**
   * Gets the date at which user data access expires.
   *
   * @return the expiration date of the token
   */
  val dataAccessExpirationTime: Date

  /**
   * Returns the graph domain for this access token.
   *
   * @return The graph domain for this access token.
   */
  val graphDomain: String?

  /**
   * Creates a new AccessToken using the supplied information from a previously-obtained access
   * token (for instance, from an already-cached access token obtained prior to integration with the
   * Facebook SDK). Note that the caller is asserting that all parameters provided are correct with
   * respect to the access token string; no validation is done to verify they are correct.
   *
   * @param accessToken the access token string obtained from Facebook
   * @param applicationId the ID of the Facebook Application associated with this access token
   * @param userId the id of the user
   * @param permissions the permissions that were requested when the token was obtained (or when it
   * was last reauthorized); may be null if permission set is unknown
   * @param declinedPermissions the permissions that were declined when the token was obtained; may
   * be null if permission set is unknown
   * @param expiredPermissions the permissions that were expired when the token was obtained; may be
   * null if permission set is unknown
   * @param accessTokenSource an enum indicating how the token was originally obtained (in most
   * cases, this will be either AccessTokenSource.FACEBOOK_APPLICATION or
   * AccessTokenSource.WEB_VIEW); if null, FACEBOOK_APPLICATION is assumed.
   * @param expirationTime the expiration date associated with the token; if null, an infinite
   * expiration time is assumed (but will become correct when the token is refreshed)
   * @param lastRefreshTime the last time the token was refreshed (or when it was first obtained);
   * if null, the current time is used.
   * @param dataAccessExpirationTime The time when user data access expires
   * @param graphDomain The Graph API domain that this token is valid for.
   */
  @JvmOverloads
  constructor(
      accessToken: String,
      applicationId: String,
      userId: String,
      permissions: Collection<String?>?,
      declinedPermissions: Collection<String?>?,
      expiredPermissions: Collection<String?>?,
      accessTokenSource: AccessTokenSource?,
      expirationTime: Date?,
      lastRefreshTime: Date?,
      dataAccessExpirationTime: Date?,
      graphDomain: String? = DEFAULT_GRAPH_DOMAIN
  ) {
    Validate.notEmpty(accessToken, "accessToken")
    Validate.notEmpty(applicationId, "applicationId")
    Validate.notEmpty(userId, "userId")

    expires = expirationTime ?: DEFAULT_EXPIRATION_TIME
    this.permissions =
        Collections.unmodifiableSet(if (permissions != null) HashSet(permissions) else HashSet())
    this.declinedPermissions =
        Collections.unmodifiableSet(
            if (declinedPermissions != null) HashSet(declinedPermissions) else HashSet())
    this.expiredPermissions =
        Collections.unmodifiableSet(
            if (expiredPermissions != null) HashSet(expiredPermissions) else HashSet())
    token = accessToken
    source =
        convertTokenSourceForGraphDomain(
            accessTokenSource ?: DEFAULT_ACCESS_TOKEN_SOURCE, graphDomain)
    lastRefresh = lastRefreshTime ?: DEFAULT_LAST_REFRESH_TIME
    this.applicationId = applicationId
    this.userId = userId
    this.dataAccessExpirationTime =
        if (dataAccessExpirationTime != null && dataAccessExpirationTime.time != 0L) {
          dataAccessExpirationTime
        } else {
          DEFAULT_EXPIRATION_TIME
        }
    this.graphDomain = graphDomain ?: DEFAULT_GRAPH_DOMAIN
  }

  interface AccessTokenRefreshCallback {
    /**
     * The method called on a successful refresh of an AccessToken.
     *
     * @param accessToken the access token created from the native link intent.
     */
    fun OnTokenRefreshed(accessToken: AccessToken?)
    /**
     * The method called on a failed refresh of an AccessToken.
     *
     * @param exception throw from AccessToken creation.
     */
    fun OnTokenRefreshFailed(exception: FacebookException?)
  }

  /** A callback for creating an access token from a NativeLinkingIntent */
  interface AccessTokenCreationCallback {
    /**
     * The method called on a successful creation of an AccessToken.
     *
     * @param token the access token created from the native link intent.
     */
    fun onSuccess(token: AccessToken?)
    /**
     * The method called on a failed creation of an AccessToken.
     *
     * @param error throw from AccessToken creation.
     */
    fun onError(error: FacebookException?)
  }

  override fun toString(): String {
    val builder = StringBuilder()
    builder.append("{AccessToken")
    builder.append(" token:").append(tokenToString())
    appendPermissions(builder)
    builder.append("}")
    return builder.toString()
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (other !is AccessToken) {
      return false
    }
    return expires == other.expires &&
        permissions == other.permissions &&
        declinedPermissions == other.declinedPermissions &&
        expiredPermissions == other.expiredPermissions &&
        token == other.token &&
        source === other.source &&
        lastRefresh == other.lastRefresh &&
        applicationId == other.applicationId &&
        userId == other.userId &&
        dataAccessExpirationTime == other.dataAccessExpirationTime &&
        if (graphDomain == null) other.graphDomain == null else graphDomain == other.graphDomain
  }

  override fun hashCode(): Int {
    var result = 17
    result = result * 31 + expires.hashCode()
    result = result * 31 + permissions.hashCode()
    result = result * 31 + declinedPermissions.hashCode()
    result = result * 31 + expiredPermissions.hashCode()
    result = result * 31 + token.hashCode()
    result = result * 31 + source.hashCode()
    result = result * 31 + lastRefresh.hashCode()
    result = result * 31 + applicationId.hashCode()
    result = result * 31 + userId.hashCode()
    result = result * 31 + dataAccessExpirationTime.hashCode()
    result = result * 31 + (graphDomain?.hashCode() ?: 0)
    return result
  }

  /**
   * Shows if the token is expired.
   *
   * @return true if the token is expired.
   */
  val isExpired: Boolean
    get() = Date().after(expires)

  /**
   * Shows if the user data access is expired.
   *
   * @return true if the data access is expired.
   */
  val isDataAccessExpired: Boolean
    get() = Date().after(dataAccessExpirationTime)

  /**
   * Determine if the token is an Instagram access token, based on the token's graphDomain
   * parameter.
   *
   * @return true if the token is an Instagram access token.
   */
  val isInstagramToken: Boolean
    get() = graphDomain != null && graphDomain.equals(FacebookSdk.INSTAGRAM)

  @Throws(JSONException::class)
  internal fun toJSONObject(): JSONObject {
    val jsonObject = JSONObject()
    jsonObject.put(VERSION_KEY, CURRENT_JSON_FORMAT)
    jsonObject.put(TOKEN_KEY, token)
    jsonObject.put(EXPIRES_AT_KEY, expires.time)
    val permissionsArray = JSONArray(permissions)
    jsonObject.put(PERMISSIONS_KEY, permissionsArray)
    val declinedPermissionsArray = JSONArray(declinedPermissions)
    jsonObject.put(DECLINED_PERMISSIONS_KEY, declinedPermissionsArray)
    val expiredPermissionsArray = JSONArray(expiredPermissions)
    jsonObject.put(EXPIRED_PERMISSIONS_KEY, expiredPermissionsArray)
    jsonObject.put(LAST_REFRESH_KEY, lastRefresh.time)
    jsonObject.put(SOURCE_KEY, source.name)
    jsonObject.put(APPLICATION_ID_KEY, applicationId)
    jsonObject.put(USER_ID_KEY, userId)
    jsonObject.put(DATA_ACCESS_EXPIRATION_TIME, dataAccessExpirationTime.time)
    if (graphDomain != null) {
      jsonObject.put(GRAPH_DOMAIN, graphDomain)
    }
    return jsonObject
  }

  private fun tokenToString(): String {
    return if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS)) {
      token
    } else {
      "ACCESS_TOKEN_REMOVED"
    }
  }

  private fun appendPermissions(builder: StringBuilder) {
    builder.append(" permissions:")
    builder.append("[")
    builder.append(TextUtils.join(", ", permissions))
    builder.append("]")
  }

  private fun convertTokenSourceForGraphDomain(
      tokenSource: AccessTokenSource,
      graphDomain: String?
  ): AccessTokenSource {
    if (graphDomain != null && graphDomain.equals(FacebookSdk.INSTAGRAM)) {
      return when (tokenSource) {
        AccessTokenSource.FACEBOOK_APPLICATION_WEB -> AccessTokenSource.INSTAGRAM_APPLICATION_WEB
        AccessTokenSource.CHROME_CUSTOM_TAB -> AccessTokenSource.INSTAGRAM_CUSTOM_CHROME_TAB
        AccessTokenSource.WEB_VIEW -> AccessTokenSource.INSTAGRAM_WEB_VIEW
        // Either already an Instagram token source or not supported source for Instagram
        else -> tokenSource
      }
    }
    return tokenSource
  }

  internal constructor(parcel: Parcel) {
    expires = Date(parcel.readLong())
    val permissionsList = ArrayList<String?>()
    parcel.readStringList(permissionsList)
    permissions = Collections.unmodifiableSet(HashSet(permissionsList))
    permissionsList.clear()
    parcel.readStringList(permissionsList)
    declinedPermissions = Collections.unmodifiableSet(HashSet(permissionsList))
    permissionsList.clear()
    parcel.readStringList(permissionsList)
    expiredPermissions = Collections.unmodifiableSet(HashSet(permissionsList))
    val token = parcel.readString()
    this.token = Validate.notNullOrEmpty(token, "token")
    val sourceString = parcel.readString()
    source =
        if (sourceString != null) AccessTokenSource.valueOf(sourceString)
        else DEFAULT_ACCESS_TOKEN_SOURCE
    lastRefresh = Date(parcel.readLong())
    val applicationId = parcel.readString()
    this.applicationId = Validate.notNullOrEmpty(applicationId, "applicationId")
    val userId = parcel.readString()
    this.userId = Validate.notNullOrEmpty(userId, "userId")
    dataAccessExpirationTime = Date(parcel.readLong())
    graphDomain = parcel.readString()
  }

  override fun describeContents(): Int = 0

  override fun writeToParcel(dest: Parcel, flags: Int) {
    dest.writeLong(expires.time)
    dest.writeStringList(ArrayList(permissions))
    dest.writeStringList(ArrayList(declinedPermissions))
    dest.writeStringList(ArrayList(expiredPermissions))
    dest.writeString(token)
    dest.writeString(source.name)
    dest.writeLong(lastRefresh.time)
    dest.writeString(applicationId)
    dest.writeString(userId)
    dest.writeLong(dataAccessExpirationTime.time)
    dest.writeString(graphDomain)
  }

  companion object {
    const val ACCESS_TOKEN_KEY = "access_token"
    const val EXPIRES_IN_KEY = "expires_in"
    const val USER_ID_KEY = "user_id"
    const val DATA_ACCESS_EXPIRATION_TIME = "data_access_expiration_time"
    const val GRAPH_DOMAIN = "graph_domain"
    const val DEFAULT_GRAPH_DOMAIN = "facebook"
    private val MAX_DATE = Date(Long.MAX_VALUE)
    private val DEFAULT_EXPIRATION_TIME = MAX_DATE
    private val DEFAULT_LAST_REFRESH_TIME = Date()
    private val DEFAULT_ACCESS_TOKEN_SOURCE = AccessTokenSource.FACEBOOK_APPLICATION_WEB

    // Constants related to JSON serialization.
    private const val CURRENT_JSON_FORMAT = 1
    private const val VERSION_KEY = "version"
    private const val EXPIRES_AT_KEY = "expires_at"
    private const val PERMISSIONS_KEY = "permissions"
    private const val DECLINED_PERMISSIONS_KEY = "declined_permissions"
    private const val EXPIRED_PERMISSIONS_KEY = "expired_permissions"
    private const val TOKEN_KEY = "token"
    private const val SOURCE_KEY = "source"
    private const val LAST_REFRESH_KEY = "last_refresh"
    private const val APPLICATION_ID_KEY = "application_id"
    /**
     * Getter for the access token that is current for the application.
     *
     * @return The access token that is current for the application.
     */
    @JvmStatic
    fun getCurrentAccessToken(): AccessToken? {
      return AccessTokenManager.getInstance().currentAccessToken
    }
    /**
     * Setter for the access token that is current for the application.
     *
     * @param accessToken The access token to set.
     */
    @JvmStatic
    fun setCurrentAccessToken(accessToken: AccessToken?) {
      AccessTokenManager.getInstance().currentAccessToken = accessToken
    }
    /**
     * Returns whether the current [AccessToken] is active or not.
     *
     * @return true if the current AccessToken exists and has not expired; false, otherwise.
     */
    @JvmStatic
    fun isCurrentAccessTokenActive(): Boolean {
      val accessToken = AccessTokenManager.getInstance().currentAccessToken
      return accessToken != null && !accessToken.isExpired
    }

    @JvmStatic
    fun isDataAccessActive(): Boolean {
      val accessToken = AccessTokenManager.getInstance().currentAccessToken
      return accessToken != null && !accessToken.isDataAccessExpired
    }

    /**
     * Indicates whether the current active access token is for an Instagram user.
     *
     * @return true if the current AccessToken exists, is active, and is for the Instagram domain.
     */
    @JvmStatic
    fun isLoggedInWithInstagram(): Boolean {
      val accessToken = AccessTokenManager.getInstance().currentAccessToken
      return accessToken != null && !accessToken.isExpired && accessToken.isInstagramToken
    }

    /**
     * Sets the current [AccessToken] with an expiration time of now. No action is taken if there is
     * no current AccessToken.
     */
    @JvmStatic
    fun expireCurrentAccessToken() {
      val accessToken = AccessTokenManager.getInstance().currentAccessToken
      if (accessToken != null) {
        setCurrentAccessToken(createExpired(accessToken))
      }
    }

    /**
     * Updates the current access token with up to date permissions, and extends the expiration
     * date, if extension is possible.
     */
    @JvmStatic
    fun refreshCurrentAccessTokenAsync() {
      AccessTokenManager.getInstance().refreshCurrentAccessToken(null)
    }

    /**
     * Updates the current access token with up to date permissions, and extends the expiration
     * date, if extension is possible.
     *
     * @param callback
     */
    @JvmStatic
    fun refreshCurrentAccessTokenAsync(callback: AccessTokenRefreshCallback?) {
      AccessTokenManager.getInstance().refreshCurrentAccessToken(callback)
    }

    /**
     * Creates a new AccessToken using the information contained in an Intent populated by the
     * Facebook application in order to launch a native link. For more information on native
     * linking, please see https://developers.facebook.com/docs/mobile/android/deep_linking/.
     *
     * @param intent the Intent that was used to start an Activity; must not be null
     * @param applicationId the ID of the Facebook Application associated with this access token
     */
    @JvmStatic
    fun createFromNativeLinkingIntent(
        intent: Intent,
        applicationId: String,
        accessTokenCallback: AccessTokenCreationCallback
    ) {
      if (intent.extras == null) {
        accessTokenCallback.onError(FacebookException("No extras found on intent"))
        return
      }
      val extras = Bundle(intent.extras)
      val accessToken = extras.getString(ACCESS_TOKEN_KEY)
      if (accessToken == null || accessToken.isEmpty()) {
        accessTokenCallback.onError(FacebookException("No access token found on intent"))
        return
      }
      val userId = extras.getString(USER_ID_KEY)
      // Old versions of facebook for android don't provide the UserId. Obtain the id if missing
      if (userId == null || userId.isEmpty()) {
        getGraphMeRequestWithCacheAsync(
            accessToken,
            object : Utility.GraphMeRequestWithCacheCallback {
              override fun onSuccess(userInfo: JSONObject?) {
                try {
                  val userId = checkNotNull(userInfo?.getString("id"))
                  extras.putString(USER_ID_KEY, userId)
                  accessTokenCallback.onSuccess(
                      createFromBundle(
                          null,
                          extras,
                          AccessTokenSource.FACEBOOK_APPLICATION_WEB,
                          Date(),
                          applicationId))
                } catch (ex: Exception) {
                  accessTokenCallback.onError(
                      FacebookException("Unable to generate access token due to missing user id"))
                }
              }

              override fun onFailure(error: FacebookException?) {
                accessTokenCallback.onError(error)
              }
            })
      } else {
        accessTokenCallback.onSuccess(
            createFromBundle(
                null, extras, AccessTokenSource.FACEBOOK_APPLICATION_WEB, Date(), applicationId))
      }
    }

    @SuppressLint("FieldGetter")
    @JvmStatic
    internal fun createFromRefresh(current: AccessToken, bundle: Bundle): AccessToken? {
      // Only tokens obtained via SSO support refresh. Token refresh returns the expiration date
      // in seconds from the epoch rather than seconds from now.
      if (current.source !== AccessTokenSource.FACEBOOK_APPLICATION_WEB &&
          current.source !== AccessTokenSource.FACEBOOK_APPLICATION_NATIVE &&
          current.source !== AccessTokenSource.FACEBOOK_APPLICATION_SERVICE) {
        throw FacebookException("Invalid token source: " + current.source)
      }
      val expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, Date(0))
      val token = bundle.getString(ACCESS_TOKEN_KEY) ?: return null
      val graphDomain = bundle.getString(GRAPH_DOMAIN)
      val dataAccessExpirationTime =
          getBundleLongAsDate(bundle, DATA_ACCESS_EXPIRATION_TIME, Date(0))
      return if (isNullOrEmpty(token)) {
        null
      } else {
        AccessToken(
            token,
            current.applicationId,
            current.userId,
            current.permissions,
            current.declinedPermissions,
            current.expiredPermissions,
            current.source,
            expires,
            Date(),
            dataAccessExpirationTime,
            graphDomain)
      }
    }

    internal fun createExpired(current: AccessToken): AccessToken {
      return AccessToken(
          current.token,
          current.applicationId,
          current.userId,
          current.permissions,
          current.declinedPermissions,
          current.expiredPermissions,
          current.source,
          Date(),
          Date(),
          current.dataAccessExpirationTime)
    }

    @JvmStatic
    internal fun createFromLegacyCache(bundle: Bundle): AccessToken? {
      val permissions = getPermissionsFromBundle(bundle, LegacyTokenHelper.PERMISSIONS_KEY)
      val declinedPermissions =
          getPermissionsFromBundle(bundle, LegacyTokenHelper.DECLINED_PERMISSIONS_KEY)
      val expiredPermissions =
          getPermissionsFromBundle(bundle, LegacyTokenHelper.EXPIRED_PERMISSIONS_KEY)
      var applicationId = LegacyTokenHelper.getApplicationId(bundle)
      if (isNullOrEmpty(applicationId)) {
        applicationId = FacebookSdk.getApplicationId()
      }
      val tokenString = LegacyTokenHelper.getToken(bundle) ?: return null
      val userInfo = awaitGetGraphMeRequestWithCache(tokenString)
      val userId =
          try {
            userInfo?.getString("id")
          } catch (ex: JSONException) {
            // This code is only used by AccessTokenCache. If we for any reason fail to get the
            // user id just return null.
            return null
          }
      return AccessToken(
          tokenString,
          applicationId ?: return null,
          userId ?: return null,
          permissions,
          declinedPermissions,
          expiredPermissions,
          LegacyTokenHelper.getSource(bundle),
          LegacyTokenHelper.getExpirationDate(bundle),
          LegacyTokenHelper.getLastRefreshDate(bundle),
          null)
    }

    @JvmStatic
    internal fun getPermissionsFromBundle(bundle: Bundle, key: String?): List<String?> {
      // Copy the list so we can guarantee immutable
      val originalPermissions: List<String?>? = bundle.getStringArrayList(key)
      val permissions: List<String?>
      permissions =
          if (originalPermissions == null) {
            emptyList<String>()
          } else {
            Collections.unmodifiableList(ArrayList(originalPermissions))
          }
      return permissions
    }

    @Throws(JSONException::class)
    @JvmStatic
    internal fun createFromJSONObject(jsonObject: JSONObject): AccessToken {
      val version = jsonObject.getInt(VERSION_KEY)
      if (version > CURRENT_JSON_FORMAT) {
        throw FacebookException("Unknown AccessToken serialization format.")
      }
      val token = jsonObject.getString(TOKEN_KEY)
      val expiresAt = Date(jsonObject.getLong(EXPIRES_AT_KEY))
      val permissionsArray = jsonObject.getJSONArray(PERMISSIONS_KEY)
      val declinedPermissionsArray = jsonObject.getJSONArray(DECLINED_PERMISSIONS_KEY)
      val expiredPermissionsArray = jsonObject.optJSONArray(EXPIRED_PERMISSIONS_KEY)
      val lastRefresh = Date(jsonObject.getLong(LAST_REFRESH_KEY))
      val source = AccessTokenSource.valueOf(jsonObject.getString(SOURCE_KEY))
      val applicationId = jsonObject.getString(APPLICATION_ID_KEY)
      val userId = jsonObject.getString(USER_ID_KEY)
      val dataAccessExpirationTime = Date(jsonObject.optLong(DATA_ACCESS_EXPIRATION_TIME, 0))
      val graphDomain = jsonObject.optString(GRAPH_DOMAIN, null)
      return AccessToken(
          token,
          applicationId,
          userId,
          jsonArrayToStringList(permissionsArray),
          jsonArrayToStringList(declinedPermissionsArray),
          if (expiredPermissionsArray == null) ArrayList()
          else jsonArrayToStringList(expiredPermissionsArray),
          source,
          expiresAt,
          lastRefresh,
          dataAccessExpirationTime,
          graphDomain)
    }

    private fun createFromBundle(
        requestedPermissions: List<String?>?,
        bundle: Bundle,
        source: AccessTokenSource,
        expirationBase: Date,
        applicationId: String
    ): AccessToken? {
      val token = bundle.getString(ACCESS_TOKEN_KEY) ?: return null
      val expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, expirationBase) ?: return null
      val userId = bundle.getString(USER_ID_KEY) ?: return null
      val dataAccessExpirationTime =
          getBundleLongAsDate(bundle, DATA_ACCESS_EXPIRATION_TIME, Date(0))
      return AccessToken(
          token,
          applicationId,
          userId,
          requestedPermissions,
          null,
          null,
          source,
          expires,
          Date(),
          dataAccessExpirationTime)
    }

    @JvmField
    val CREATOR: Parcelable.Creator<AccessToken> =
        object : Parcelable.Creator<AccessToken> {
          override fun createFromParcel(source: Parcel): AccessToken {
            return AccessToken(source)
          }

          override fun newArray(size: Int): Array<AccessToken?> {
            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

import android.content.Context
import android.content.SharedPreferences
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions
import org.json.JSONException
import org.json.JSONObject

internal class AccessTokenCache(
    private val sharedPreferences: SharedPreferences,
    private val tokenCachingStrategyFactory: SharedPreferencesTokenCachingStrategyFactory
) {
  private var tokenCachingStrategyField: LegacyTokenHelper? = null
  private val tokenCachingStrategy: LegacyTokenHelper
    @AutoHandleExceptions
    get() {
      if (tokenCachingStrategyField == null) {
        synchronized(this) {
          if (tokenCachingStrategyField == null) {
            tokenCachingStrategyField = tokenCachingStrategyFactory.create()
          }
        }
      }
      return checkNotNull(tokenCachingStrategyField)
    }

  constructor() :
      this(
          FacebookSdk.getApplicationContext()
              .getSharedPreferences(
                  AccessTokenManager.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE),
          SharedPreferencesTokenCachingStrategyFactory())

  fun load(): AccessToken? {
    var accessToken: AccessToken? = null
    if (hasCachedAccessToken()) {
      // If we have something cached, we try to use it; even if it is invalid, do not fall
      // back to a legacy caching strategy.
      accessToken = cachedAccessToken
    } else if (shouldCheckLegacyToken()) {
      accessToken = legacyAccessToken
      if (accessToken != null) {
        save(accessToken)
        tokenCachingStrategy.clear()
      }
    }
    return accessToken
  }

  fun save(accessToken: AccessToken) {
    try {
      val jsonObject = accessToken.toJSONObject()
      sharedPreferences.edit().putString(CACHED_ACCESS_TOKEN_KEY, jsonObject.toString()).apply()
    } catch (e: JSONException) {
      // Can't recover
    }
  }

  fun clear() {
    sharedPreferences.edit().remove(CACHED_ACCESS_TOKEN_KEY).apply()
    if (shouldCheckLegacyToken()) {
      tokenCachingStrategy.clear()
    }
  }

  private fun hasCachedAccessToken(): Boolean {
    return sharedPreferences.contains(CACHED_ACCESS_TOKEN_KEY)
  }

  private val cachedAccessToken: AccessToken?
    get() {
      val jsonString = sharedPreferences.getString(CACHED_ACCESS_TOKEN_KEY, null)
      return if (jsonString != null) {
        try {
          val jsonObject = JSONObject(jsonString)
          AccessToken.createFromJSONObject(jsonObject)
        } catch (e: JSONException) {
          null
        }
      } else null
    }

  private fun shouldCheckLegacyToken(): Boolean {
    return FacebookSdk.isLegacyTokenUpgradeSupported()
  }

  private val legacyAccessToken: AccessToken?
    get() {
      var accessToken: AccessToken? = null
      val bundle = tokenCachingStrategy.load()
      if (bundle != null && LegacyTokenHelper.hasTokenInformation(bundle)) {
        accessToken = AccessToken.createFromLegacyCache(bundle)
      }
      return accessToken
    }

  internal class SharedPreferencesTokenCachingStrategyFactory {
    fun create(): LegacyTokenHelper {
      return LegacyTokenHelper(FacebookSdk.getApplicationContext())
    }
  }

  companion object {
    const val CACHED_ACCESS_TOKEN_KEY = "com.facebook.AccessTokenManager.CachedAccessToken"
  }
}


/*
 * 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.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.facebook.AccessToken.Companion.isCurrentAccessTokenActive
import com.facebook.internal.Utility.areObjectsEqual
import com.facebook.internal.Utility.clearFacebookCookies
import com.facebook.internal.Utility.isNullOrEmpty
import java.util.Date
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean

class AccessTokenManager
internal constructor(
    private val localBroadcastManager: LocalBroadcastManager,
    private val accessTokenCache: AccessTokenCache
) {
  private var currentAccessTokenField: AccessToken? = null
  var currentAccessToken: AccessToken?
    get() = currentAccessTokenField
    set(value) = setCurrentAccessToken(value, true)
  private val tokenRefreshInProgress = AtomicBoolean(false)
  private var lastAttemptedTokenExtendDate = Date(0)

  interface RefreshTokenInfo {
    val graphPath: String
    val grantType: String
  }

  class FacebookRefreshTokenInfo : RefreshTokenInfo {
    override val graphPath: String = "oauth/access_token"
    override val grantType: String = "fb_extend_sso_token"
  }

  class InstagramRefreshTokenInfo : RefreshTokenInfo {
    override val graphPath: String = "refresh_access_token"
    override val grantType: String = "ig_refresh_token"
  }

  /**
   * Load access token from accessTokenCache and set to currentAccessToken
   *
   * @return if load access token success
   */
  fun loadCurrentAccessToken(): Boolean {
    val accessToken = accessTokenCache.load()
    if (accessToken != null) {
      setCurrentAccessToken(accessToken, false)
      return true
    }
    return false
  }

  /**
   * Build intent from currentAccessToken and broadcast the intent to
   * CurrentAccessTokenExpirationBroadcastReceiver.
   */
  fun currentAccessTokenChanged() {
    sendCurrentAccessTokenChangedBroadcastIntent(currentAccessToken, currentAccessToken)
  }

  private fun setCurrentAccessToken(currentAccessToken: AccessToken?, saveToCache: Boolean) {
    val oldAccessToken = this.currentAccessTokenField
    this.currentAccessTokenField = currentAccessToken
    tokenRefreshInProgress.set(false)
    lastAttemptedTokenExtendDate = Date(0)
    if (saveToCache) {
      if (currentAccessToken != null) {
        accessTokenCache.save(currentAccessToken)
      } else {
        accessTokenCache.clear()
        clearFacebookCookies(FacebookSdk.getApplicationContext())
      }
    }
    if (!areObjectsEqual(oldAccessToken, currentAccessToken)) {
      sendCurrentAccessTokenChangedBroadcastIntent(oldAccessToken, currentAccessToken)
      setTokenExpirationBroadcastAlarm()
    }
  }

  private fun sendCurrentAccessTokenChangedBroadcastIntent(
      oldAccessToken: AccessToken?,
      currentAccessToken: AccessToken?
  ) {
    val intent =
        Intent(
            FacebookSdk.getApplicationContext(),
            CurrentAccessTokenExpirationBroadcastReceiver::class.java)
    intent.action = ACTION_CURRENT_ACCESS_TOKEN_CHANGED
    intent.putExtra(EXTRA_OLD_ACCESS_TOKEN, oldAccessToken)
    intent.putExtra(EXTRA_NEW_ACCESS_TOKEN, currentAccessToken)
    localBroadcastManager.sendBroadcast(intent)
  }

  private fun setTokenExpirationBroadcastAlarm() {
    val context = FacebookSdk.getApplicationContext()
    val accessToken = AccessToken.getCurrentAccessToken()
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager?
    if (!isCurrentAccessTokenActive() || accessToken?.expires == null || alarmManager == null) {
      return
    }
    val intent = Intent(context, CurrentAccessTokenExpirationBroadcastReceiver::class.java)
    intent.action = ACTION_CURRENT_ACCESS_TOKEN_CHANGED
    val alarmIntent =
        if (android.os.Build.VERSION.SDK_INT >= 23) {
          PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
        } else {
          PendingIntent.getBroadcast(context, 0, intent, 0)
        }
    try {
      alarmManager[AlarmManager.RTC, accessToken.expires.time] = alarmIntent
    } catch (e: Exception) {
      /* no op */
    }
  }

  /** Refresh currentAccessToken if needed. */
  fun extendAccessTokenIfNeeded() {
    if (!shouldExtendAccessToken()) {
      return
    }
    refreshCurrentAccessToken(null)
  }

  private fun shouldExtendAccessToken(): Boolean {
    val currentAccessToken = this.currentAccessToken ?: return false
    val now = Date().time
    return currentAccessToken.source.canExtendToken() &&
        now - lastAttemptedTokenExtendDate.time > TOKEN_EXTEND_RETRY_SECONDS * 1000 &&
        now - currentAccessToken.lastRefresh.time > TOKEN_EXTEND_THRESHOLD_SECONDS * 1000
  }

  private class RefreshResult {
    var accessToken: String? = null
    var expiresAt = 0
    var expiresIn = 0
    var dataAccessExpirationTime: Long? = null
    var graphDomain: String? = null
  }

  /**
   * Refresh currentAccessToken.
   *
   * @param callback to be called after access token be refreshed.
   */
  fun refreshCurrentAccessToken(callback: AccessToken.AccessTokenRefreshCallback?) {
    if (Looper.getMainLooper() == Looper.myLooper()) {
      refreshCurrentAccessTokenImpl(callback)
    } else {
      val mainHandler = Handler(Looper.getMainLooper())
      mainHandler.post { refreshCurrentAccessTokenImpl(callback) }
    }
  }

  private fun refreshCurrentAccessTokenImpl(callback: AccessToken.AccessTokenRefreshCallback?) {
    val accessToken = currentAccessToken
    if (accessToken == null) {
      callback?.OnTokenRefreshFailed(FacebookException("No current access token to refresh"))
      return
    }
    if (!tokenRefreshInProgress.compareAndSet(false, true)) {
      callback?.OnTokenRefreshFailed(FacebookException("Refresh already in progress"))
      return
    }
    lastAttemptedTokenExtendDate = Date()
    val permissions: MutableSet<String?> = HashSet()
    val declinedPermissions: MutableSet<String?> = HashSet()
    val expiredPermissions: MutableSet<String?> = HashSet()
    val permissionsCallSucceeded = AtomicBoolean(false)
    val refreshResult = RefreshResult()
    val batch =
        GraphRequestBatch(
            createGrantedPermissionsRequest(
                accessToken,
                GraphRequest.Callback { response ->
                  val result = response.jsonObject ?: return@Callback
                  val permissionsArray = result.optJSONArray("data") ?: return@Callback
                  permissionsCallSucceeded.set(true)
                  for (i in 0 until permissionsArray.length()) {
                    val permissionEntry = permissionsArray.optJSONObject(i) ?: continue
                    val permission = permissionEntry.optString("permission")
                    var status = permissionEntry.optString("status")
                    if (!isNullOrEmpty(permission) && !isNullOrEmpty(status)) {
                      status = status.toLowerCase(Locale.US)
                      when (status) {
                        "granted" -> permissions.add(permission)
                        "declined" -> declinedPermissions.add(permission)
                        "expired" -> expiredPermissions.add(permission)
                        else -> Log.w(TAG, "Unexpected status: $status")
                      }
                    }
                  }
                }),
            createExtendAccessTokenRequest(
                accessToken,
                GraphRequest.Callback { response ->
                  val data = response.jsonObject ?: return@Callback
                  refreshResult.accessToken = data.optString("access_token")
                  refreshResult.expiresAt = data.optInt("expires_at")
                  refreshResult.expiresIn = data.optInt("expires_in")
                  refreshResult.dataAccessExpirationTime =
                      data.optLong("data_access_expiration_time")
                  refreshResult.graphDomain = data.optString("graph_domain", null)
                }))
    batch.addCallback(
        GraphRequestBatch.Callback {
          var newAccessToken: AccessToken? = null
          val returnAccessToken = refreshResult.accessToken
          val returnExpiresAt = refreshResult.expiresAt
          val returnDataAccessExpirationTime = refreshResult.dataAccessExpirationTime
          val returnGraphDomain = refreshResult.graphDomain

          try {
            if (getInstance().currentAccessToken == null ||
                getInstance().currentAccessToken?.userId !== accessToken.userId) {
              callback?.OnTokenRefreshFailed(
                  FacebookException("No current access token to refresh"))
              return@Callback
            }
            if (!permissionsCallSucceeded.get() &&
                returnAccessToken == null &&
                returnExpiresAt == 0) {
              callback?.OnTokenRefreshFailed(FacebookException("Failed to refresh access token"))
              return@Callback
            }
            var expirationTime = accessToken.expires
            if (refreshResult.expiresAt != 0) {
              expirationTime = Date(refreshResult.expiresAt * 1000L)
            } else if (refreshResult.expiresIn != 0) {
              val now = Date().time
              expirationTime = Date(refreshResult.expiresIn * 1000L + now)
            }
            newAccessToken =
                AccessToken(
                    returnAccessToken ?: accessToken.token,
                    accessToken.applicationId,
                    accessToken.userId,
                    if (permissionsCallSucceeded.get()) permissions else accessToken.permissions,
                    if (permissionsCallSucceeded.get()) declinedPermissions
                    else accessToken.declinedPermissions,
                    if (permissionsCallSucceeded.get()) expiredPermissions
                    else accessToken.expiredPermissions,
                    accessToken.source,
                    expirationTime,
                    Date(),
                    if (returnDataAccessExpirationTime != null) {
                      Date(returnDataAccessExpirationTime * 1000L)
                    } else accessToken.dataAccessExpirationTime,
                    returnGraphDomain ?: accessToken.graphDomain)
            getInstance().currentAccessToken = newAccessToken
          } finally {
            tokenRefreshInProgress.set(false)
            if (callback != null && newAccessToken != null) {
              callback.OnTokenRefreshed(newAccessToken)
            }
          }
        })
    batch.executeAsync()
  }

  companion object {
    const val TAG = "AccessTokenManager"
    const val ACTION_CURRENT_ACCESS_TOKEN_CHANGED =
        "com.facebook.sdk.ACTION_CURRENT_ACCESS_TOKEN_CHANGED"
    const val EXTRA_OLD_ACCESS_TOKEN = "com.facebook.sdk.EXTRA_OLD_ACCESS_TOKEN"
    const val EXTRA_NEW_ACCESS_TOKEN = "com.facebook.sdk.EXTRA_NEW_ACCESS_TOKEN"
    const val SHARED_PREFERENCES_NAME = "com.facebook.AccessTokenManager.SharedPreferences"

    // Token extension constants
    private const val TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60 // 1 day
    private const val TOKEN_EXTEND_RETRY_SECONDS = 60 * 60 // 1 hour
    private const val ME_PERMISSIONS_GRAPH_PATH = "me/permissions"

    private var instanceField: AccessTokenManager? = null

    @JvmStatic
    fun getInstance(): AccessTokenManager {
      val instance = instanceField
      if (instance == null) {
        synchronized(this) {
          val instance = instanceField
          return if (instance == null) {
            val applicationContext = FacebookSdk.getApplicationContext()
            val localBroadcastManager = LocalBroadcastManager.getInstance(applicationContext)
            val accessTokenCache = AccessTokenCache()
            val newInstance = AccessTokenManager(localBroadcastManager, accessTokenCache)
            instanceField = newInstance
            newInstance
          } else {
            instance
          }
        }
      } else {
        return instance
      }
    }

    private fun createGrantedPermissionsRequest(
        accessToken: AccessToken,
        callback: GraphRequest.Callback
    ): GraphRequest {
      val parameters = Bundle()
      parameters.putString(GraphRequest.FIELDS_PARAM, "permission,status")
      val request =
          GraphRequest.newGraphPathRequest(accessToken, ME_PERMISSIONS_GRAPH_PATH, callback)
      request.parameters = parameters
      request.httpMethod = HttpMethod.GET
      return request
    }

    private fun getRefreshTokenInfoForToken(accessToken: AccessToken): RefreshTokenInfo {
      val tokenGraphDomain = accessToken.graphDomain ?: AccessToken.DEFAULT_GRAPH_DOMAIN
      return when (tokenGraphDomain) {
        FacebookSdk.INSTAGRAM -> InstagramRefreshTokenInfo()
        else -> FacebookRefreshTokenInfo()
      }
    }

    private fun createExtendAccessTokenRequest(
        accessToken: AccessToken,
        callback: GraphRequest.Callback
    ): GraphRequest {
      val refreshInfo = getRefreshTokenInfoForToken(accessToken)
      val parameters = Bundle()
      parameters.putString("grant_type", refreshInfo.grantType)
      parameters.putString("client_id", accessToken.applicationId)
      parameters.putString(
          GraphRequest.FIELDS_PARAM,
          "access_token,expires_at,expires_in,data_access_expiration_time,graph_domain")
      val request = GraphRequest.newGraphPathRequest(accessToken, refreshInfo.graphPath, callback)
      request.parameters = parameters
      request.httpMethod = HttpMethod.GET
      return request
    }
  }
}


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

/** Indicates where a Facebook access token was obtained from. */
enum class AccessTokenSource(private val canExtendToken: Boolean) {
  /** Indicates an access token has not been obtained, or is otherwise invalid. */
  NONE(false),

  /**
   * Indicates an access token was obtained by the user logging in through the Facebook app for
   * Android using the web login dialog.
   */
  FACEBOOK_APPLICATION_WEB(true),

  /**
   * Indicates an access token was obtained by the user logging in through the Facebook app for
   * Android using the native login dialog.
   */
  FACEBOOK_APPLICATION_NATIVE(true),

  /**
   * Indicates an access token was obtained by asking the Facebook app for the current token based
   * on permissions the user has already granted to the app. No dialog was shown to the user in this
   * case.
   */
  FACEBOOK_APPLICATION_SERVICE(true),

  /** Indicates an access token was obtained by the user logging in through the Web-based dialog. */
  WEB_VIEW(true),

  /**
   * Indicates an access token was obtained by the user logging in through the Web-based dialog on a
   * Chrome Custom Tab.
   */
  CHROME_CUSTOM_TAB(true),

  /** Indicates an access token is for a test user rather than an actual Facebook user. */
  TEST_USER(true),

  /** Indicates an access token constructed with a Client Token. */
  CLIENT_TOKEN(true),

  /** Indicates an access token constructed from facebook.com/device */
  DEVICE_AUTH(true),

  /**
   * Indicates an access token was obtained by the user logging in through the Instagram app for
   * Android using the web login dialog.
   */
  INSTAGRAM_APPLICATION_WEB(true),

  /**
   * Indicates an access token was obtained by the user logging in to Instagram through the
   * Web-based dialog on a Chrome Custom Tab.
   */
  INSTAGRAM_CUSTOM_CHROME_TAB(true),

  /**
   * Indicates an access token was obtained by the user logging in to Instagram through the
   * Web-based dialog.
   */
  INSTAGRAM_WEB_VIEW(true);

  /** @return canExtendToken */
  fun canExtendToken(): Boolean = canExtendToken

  /** @return if this token is from instagram */
  fun fromInstagram(): Boolean {
    return when (this) {
      INSTAGRAM_APPLICATION_WEB,
      INSTAGRAM_CUSTOM_CHROME_TAB,
      INSTAGRAM_WEB_VIEW -> true
      else -> false
    }
  }
}


/*
 * 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.os.Parcel
import android.os.Parcelable
import com.facebook.internal.Validate
import com.facebook.internal.security.OidcSecurityUtil
import java.io.IOException
import java.security.spec.InvalidKeySpecException
import org.json.JSONException
import org.json.JSONObject

/**
 * This class represents an immutable Authentication Token (or so called "id token") for using
 * Facebook Login. It includes the three component that constructs the token
 * HEADER.PAYLOAD.SIGNATURE
 *
 * WARNING: This feature is currently in development and not intended for external usage.
 */
class AuthenticationToken : Parcelable {

  /**
   * Gets the raw string of the AuthenticationToken (or id_token)
   *
   * @return the raw string representing the Authentication Token in the following format
   * Header.Payload.Signature
   */
  val token: String

  /** Expected nonce - used in Login Configuration */
  val expectedNonce: String

  /** Header component of the AuthenticationToken */
  val header: AuthenticationTokenHeader

  /** Payload component of the AuthenticationToken */
  val claims: AuthenticationTokenClaims

  /** Signature component of the id_token TODO: create a new class for the signature part */
  val signature: String

  @JvmOverloads
  constructor(token: String, expectedNonce: String) {
    Validate.notEmpty(token, "token")
    Validate.notEmpty(expectedNonce, "expectedNonce")

    val tokenArray = token.split(".")
    require(tokenArray.size == 3) { "Invalid IdToken string" }
    val rawHeader = tokenArray[0]
    val rawClaims = tokenArray[1]
    val rawSignature = tokenArray[2]

    this.token = token
    this.expectedNonce = expectedNonce
    this.header = AuthenticationTokenHeader(rawHeader)
    this.claims = AuthenticationTokenClaims(rawClaims, expectedNonce)

    // validate signature
    require(isValidSignature(rawHeader, rawClaims, rawSignature, header.kid)) {
      "Invalid Signature"
    }
    this.signature = rawSignature
  }

  internal constructor(parcel: Parcel) {
    val token = parcel.readString()
    this.token = Validate.notNullOrEmpty(token, "token")

    val expectedNonce = parcel.readString()
    this.expectedNonce = Validate.notNullOrEmpty(expectedNonce, "expectedNonce")

    this.header =
        checkNotNull(parcel.readParcelable(AuthenticationTokenHeader::class.java.classLoader))
    this.claims =
        checkNotNull(parcel.readParcelable(AuthenticationTokenClaims::class.java.classLoader))

    val signature = parcel.readString()
    this.signature = Validate.notNullOrEmpty(signature, "signature")
  }

  /**
   * internal constructor for caching only NOTE: This constructor will skip the benefit of token
   * validation
   */
  @Throws(JSONException::class)
  internal constructor(jsonObject: JSONObject) {
    this.token = jsonObject.getString(TOKEN_STRING_KEY)
    this.expectedNonce = jsonObject.getString(EXPECTED_NONCE_KEY)
    this.signature = jsonObject.getString(SIGNATURE_KEY)

    val headerJSONObject = jsonObject.getJSONObject((HEADER_KEY))
    val claimsJSONObject = jsonObject.getJSONObject(CLAIMS_KEY)
    this.header = AuthenticationTokenHeader(headerJSONObject)
    this.claims = AuthenticationTokenClaims.createFromJSONObject(claimsJSONObject)
  }

  /** convert current AuthenticationToken object to JSON */
  @Throws(JSONException::class)
  internal fun toJSONObject(): JSONObject {
    val jsonObject = JSONObject()
    jsonObject.put(TOKEN_STRING_KEY, token)
    jsonObject.put(EXPECTED_NONCE_KEY, expectedNonce)
    jsonObject.put(HEADER_KEY, header.toJSONObject())
    jsonObject.put(CLAIMS_KEY, claims.toJSONObject())
    jsonObject.put(SIGNATURE_KEY, signature)
    return jsonObject
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (other !is AuthenticationToken) {
      return false
    }
    return token == other.token &&
        expectedNonce == other.expectedNonce &&
        header == other.header &&
        claims == other.claims &&
        signature == other.signature
  }

  override fun hashCode(): Int {
    var result = 17
    result = result * 31 + token.hashCode()
    result = result * 31 + expectedNonce.hashCode()
    result = result * 31 + header.hashCode()
    result = result * 31 + claims.hashCode()
    result = result * 31 + signature.hashCode()
    return result
  }

  override fun writeToParcel(dest: Parcel, flags: Int) {
    dest.writeString(token)
    dest.writeString(expectedNonce)
    dest.writeParcelable(header, flags)
    dest.writeParcelable(claims, flags)
    dest.writeString(signature)
  }

  override fun describeContents(): Int = 0

  private fun isValidSignature(
      headerString: String,
      claimsString: String,
      sigString: String,
      kid: String
  ): Boolean {
    try {
      val pubKeyString: String = OidcSecurityUtil.getRawKeyFromEndPoint(kid) ?: return false
      val pubKey = OidcSecurityUtil.getPublicKeyFromString(pubKeyString)
      return OidcSecurityUtil.verify(pubKey, "$headerString.$claimsString", sigString)
    } catch (ex: InvalidKeySpecException) {
      // invalid key
      return false
    } catch (_ex: IOException) {
      // exception happens on
      return false
    }
  }

  companion object {
    const val AUTHENTICATION_TOKEN_KEY = "id_token"

    // JSON serialization Key
    private const val TOKEN_STRING_KEY = "token_string"
    private const val EXPECTED_NONCE_KEY = "expected_nonce"
    private const val HEADER_KEY = "header"
    private const val CLAIMS_KEY = "claims"
    private const val SIGNATURE_KEY = "signature"

    /**
     * Getter for the authentication token that is current for the application.
     *
     * @return The authentication token that is current for the application.
     */
    @JvmStatic
    fun getCurrentAuthenticationToken(): AuthenticationToken? {
      return AuthenticationTokenManager.getInstance().currentAuthenticationToken
    }

    /**
     * Setter for the authentication token that is current for the application.
     *
     * @param authenticationToken The authentication token to set.
     */
    @JvmStatic
    fun setCurrentAuthenticationToken(authenticationToken: AuthenticationToken?) {
      AuthenticationTokenManager.getInstance().currentAuthenticationToken = authenticationToken
    }

    @JvmField
    val CREATOR: Parcelable.Creator<AuthenticationToken> =
        object : Parcelable.Creator<AuthenticationToken> {
          override fun createFromParcel(source: Parcel): AuthenticationToken {
            return AuthenticationToken(source)
          }

          override fun newArray(size: Int): Array<AuthenticationToken?> {
            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

import android.content.Context
import android.content.SharedPreferences
import org.json.JSONException
import org.json.JSONObject

/**
 * This class is an wrapper class which handles load/save/clear of the Authentication Token cache
 *
 * WARNING: This feature is currently in development and not intended for external usage.
 */
class AuthenticationTokenCache(private val sharedPreferences: SharedPreferences) {
  constructor() :
      this(
          FacebookSdk.getApplicationContext()
              .getSharedPreferences(
                  AuthenticationTokenManager.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE))

  fun load(): AuthenticationToken? {
    var authenticationToken: AuthenticationToken? = null
    if (hasCachedAuthenticationToken()) {
      // If we have something cached, we try to use it; even if it is invalid, do not fall
      authenticationToken = cachedAuthenticationToken
    }
    return authenticationToken
  }

  fun save(authenticationToken: AuthenticationToken) {
    try {
      val jsonObject = authenticationToken.toJSONObject()
      sharedPreferences
          .edit()
          .putString(CACHED_AUTHENTICATION_TOKEN_KEY, jsonObject.toString())
          .apply()
    } catch (e: JSONException) {
      // can't recover
    }
  }

  fun clear() {
    sharedPreferences.edit().remove(CACHED_AUTHENTICATION_TOKEN_KEY).apply()
  }

  private fun hasCachedAuthenticationToken(): Boolean {
    return sharedPreferences.contains(CACHED_AUTHENTICATION_TOKEN_KEY)
  }

  private val cachedAuthenticationToken: AuthenticationToken?
    get() {
      val jsonString = sharedPreferences.getString(CACHED_AUTHENTICATION_TOKEN_KEY, null)
      return if (jsonString != null) {
        try {
          val jsonObject = JSONObject(jsonString)
          AuthenticationToken(jsonObject)
        } catch (e: JSONException) {
          // in case if exception happens on reconstructing the id token
          null
        }
      } else null
    }

  companion object {
    const val CACHED_AUTHENTICATION_TOKEN_KEY =
        "com.facebook.AuthenticationManager.CachedAuthenticationToken"
  }
}


/*
 * 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.os.Parcel
import android.os.Parcelable
import android.text.format.DateUtils
import android.util.Base64
import androidx.annotation.VisibleForTesting
import com.facebook.internal.Utility.convertJSONObjectToHashMap
import com.facebook.internal.Utility.convertJSONObjectToStringMap
import com.facebook.internal.Utility.jsonArrayToSet
import com.facebook.internal.Utility.jsonArrayToStringList
import com.facebook.internal.Validate
import java.net.MalformedURLException
import java.net.URL
import java.util.Collections
import java.util.Date
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

/**
 * This class represents an immutable claims for using AuthenticationToken. It includes all metadata
 * or key values of the claims
 *
 * WARNING: This feature is currently in development and not intended for external usage.
 */
class AuthenticationTokenClaims : Parcelable {
  /** Get a unique identifier for the token. */
  val jti: String

  /** Get issuer Identifier for the Issuer of the response. */
  val iss: String

  /** Get audience(s) that this ID Token is intended for. */
  val aud: String

  /**
   * String value used to associate a Client session with an ID Token, and to mitigate replay
   * attacks.
   */
  val nonce: String

  /**
   * Expiration time (in seconds) on or after which the ID Token MUST NOT be accepted for
   * processing.
   */
  val exp: Long

  /** Time (in seconds) at which the JWT was issued. */
  val iat: Long

  /** Subject - Identifier for the End-User at the Issuer. */
  val sub: String

  /** End-User's full name in displayable form including all name parts. */
  val name: String?

  /** End-User's given name in displayable form */
  val givenName: String?

  /** End-User's middle name in displayable form */
  val middleName: String?

  /** End-User's family name in displayable form */
  val familyName: String?

  /**
   * End-User's preferred e-mail address.
   *
   * IMPORTANT: This field will only be populated if your user has granted your application the
   * 'email' permission.
   */
  val email: String?

  /** URL of the End-User's profile picture. */
  val picture: String?

  /**
   * End-User's friends.
   *
   * IMPORTANT: This field will only be populated if your user has granted your application the
   * 'user_friends' permission.
   */
  val userFriends: Set<String>?

  /** End-User's birthday */
  val userBirthday: String?

  /** End-User's age range */
  val userAgeRange: Map<String, Int>?

  /** End-User's hometown */
  val userHometown: Map<String, String>?

  /** End-User's location */
  val userLocation: Map<String, String>?

  /** End-User's gender */
  val userGender: String?

  /** End-User's link */
  val userLink: String?

  @JvmOverloads
  constructor(encodedClaims: String, expectedNonce: String) {
    Validate.notEmpty(encodedClaims, "encodedClaims")

    val decodedBytes = Base64.decode(encodedClaims, Base64.URL_SAFE)
    val claimsString = String(decodedBytes)
    val jsonObj = JSONObject(claimsString)

    // verify claims
    require(isValidClaims(jsonObj, expectedNonce)) { "Invalid claims" }

    this.jti = jsonObj.getString(JSON_KEY_JIT)
    this.iss = jsonObj.getString(JSON_KEY_ISS)
    this.aud = jsonObj.getString(JSON_KEY_AUD)
    this.nonce = jsonObj.getString(JSON_KEY_NONCE)
    this.exp = jsonObj.getLong(JSON_KEY_EXP)
    this.iat = jsonObj.getLong(JSON_KEY_IAT)
    this.sub = jsonObj.getString(JSON_KEY_SUB)
    this.name = jsonObj.getNullableString(JSON_KEY_NAME)
    this.givenName = jsonObj.getNullableString(JSON_KEY_GIVEN_NAME)
    this.middleName = jsonObj.getNullableString(JSON_KEY_MIDDLE_NAME)
    this.familyName = jsonObj.getNullableString(JSON_KEY_FAMILY_NAME)
    this.email = jsonObj.getNullableString(JSON_KEY_EMAIL)
    this.picture = jsonObj.getNullableString(JSON_KEY_PICTURE)

    val userFriendsList = jsonObj.optJSONArray(JSON_KEY_USER_FRIENDS)
    this.userFriends =
        if (userFriendsList == null) null
        else Collections.unmodifiableSet(jsonArrayToSet(userFriendsList))
    this.userBirthday = jsonObj.getNullableString(JSON_KEY_USER_BIRTHDAY)
    val userAgeRangeJson = jsonObj.optJSONObject(JSON_KEY_USER_AGE_RANGE)
    this.userAgeRange =
        if (userAgeRangeJson == null) null
        else {
          Collections.unmodifiableMap(
              convertJSONObjectToHashMap(userAgeRangeJson) as Map<String, Int>?)
        }
    val userHometownJson = jsonObj.optJSONObject(JSON_KEY_USER_HOMETOWN)
    this.userHometown =
        if (userHometownJson == null) null
        else Collections.unmodifiableMap(convertJSONObjectToStringMap(userHometownJson))
    val userLocationJson = jsonObj.optJSONObject(JSON_KEY_USER_LOCATION)
    this.userLocation =
        if (userLocationJson == null) null
        else Collections.unmodifiableMap(convertJSONObjectToStringMap(userLocationJson))
    this.userGender = jsonObj.getNullableString(JSON_KEY_USER_GENDER)
    this.userLink = jsonObj.getNullableString(JSON_KEY_USER_LINK)
  }

  /**
   * Creates a the claims component of
   *
   * @param jti A unique identifier for the token
   * @param iss Issuer Identifier for the Issuer of the response
   * @param aud Audience(s) that this ID Token is intended for
   * @param nonce String value used to associate a Client session with an ID Token
   * @param exp Expiration time on or after which the ID Token MUST NOT be accepted for processing
   * @param iat Time at which the JWT was issued
   * @param sub Subject - Identifier for the End-User at the Issuer
   * @param name End-User's full name in displayable form including all name parts
   * @param givenName End-User's given name in displayable form
   * @param middleName End-User's middle name in displayable form
   * @param familyName End-User's family name in displayable form
   * @param email End-User's preferred e-mail address
   * @param picture URL of the End-User's profile picture
   * @param userFriends End-User's friends
   * @param userBirthday End-User's birthday
   * @param userAgeRange End-User's age range
   * @param userHometown End-User's hometown
   * @param userLocation End-User's location
   * @param userGender End-User's gender
   * @param userLink End-User's link
   */
  @JvmOverloads
  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  constructor(
      jti: String,
      iss: String,
      aud: String,
      nonce: String,
      exp: Long,
      iat: Long,
      sub: String,
      name: String? = null,
      givenName: String? = null,
      middleName: String? = null,
      familyName: String? = null,
      email: String? = null,
      picture: String? = null,
      userFriends: Collection<String>? = null,
      userBirthday: String? = null,
      userAgeRange: Map<String, Int>? = null,
      userHometown: Map<String, String>? = null,
      userLocation: Map<String, String>? = null,
      userGender: String? = null,
      userLink: String? = null
  ) {
    Validate.notEmpty(jti, JSON_KEY_JIT)
    Validate.notEmpty(iss, JSON_KEY_ISS)
    Validate.notEmpty(aud, JSON_KEY_AUD)
    Validate.notEmpty(nonce, JSON_KEY_NONCE)
    Validate.notEmpty(sub, JSON_KEY_SUB)

    /**
     * TODO: We do not want developer to consume this constructor directly, need to move this class
     * to internal folder
     */
    this.jti = jti
    this.iss = iss
    this.aud = aud
    this.nonce = nonce
    this.exp = exp
    this.iat = iat
    this.sub = sub
    this.name = name
    this.givenName = givenName
    this.middleName = middleName
    this.familyName = familyName
    this.email = email
    this.picture = picture
    this.userFriends =
        if (userFriends != null) Collections.unmodifiableSet(HashSet(userFriends)) else null
    this.userBirthday = userBirthday
    this.userAgeRange =
        if (userAgeRange != null) Collections.unmodifiableMap(HashMap(userAgeRange)) else null
    this.userHometown =
        if (userHometown != null) Collections.unmodifiableMap(HashMap(userHometown)) else null
    this.userLocation =
        if (userLocation != null) Collections.unmodifiableMap(HashMap(userLocation)) else null
    this.userGender = userGender
    this.userLink = userLink
  }

  internal constructor(parcel: Parcel) {
    val jti = parcel.readString()
    this.jti = Validate.notNullOrEmpty(jti, JSON_KEY_JIT)

    val iss = parcel.readString()
    this.iss = Validate.notNullOrEmpty(iss, JSON_KEY_ISS)

    val aud = parcel.readString()
    this.aud = Validate.notNullOrEmpty(aud, JSON_KEY_AUD)

    val nonce = parcel.readString()
    this.nonce = Validate.notNullOrEmpty(nonce, JSON_KEY_NONCE)

    this.exp = parcel.readLong()
    this.iat = parcel.readLong()
    val sub = parcel.readString()
    this.sub = Validate.notNullOrEmpty(sub, JSON_KEY_SUB)
    this.name = parcel.readString()
    this.givenName = parcel.readString()
    this.middleName = parcel.readString()
    this.familyName = parcel.readString()
    this.email = parcel.readString()
    this.picture = parcel.readString()

    val userFriendsList = parcel.createStringArrayList()
    this.userFriends =
        if (userFriendsList != null) Collections.unmodifiableSet(HashSet(userFriendsList)) else null

    this.userBirthday = parcel.readString()

    val userAgeRangeMap = parcel.readHashMap(Int.javaClass.classLoader) as? (HashMap<String, Int>)
    this.userAgeRange =
        if (userAgeRangeMap != null) Collections.unmodifiableMap(userAgeRangeMap) else null

    val userHometownMap =
        parcel.readHashMap(String.javaClass.classLoader) as? (HashMap<String, String>)
    this.userHometown =
        if (userHometownMap != null) Collections.unmodifiableMap(userHometownMap) else null

    val userLocationMap =
        parcel.readHashMap(String.javaClass.classLoader) as? (HashMap<String, String>)
    this.userLocation =
        if (userLocationMap != null) Collections.unmodifiableMap(userLocationMap) else null

    this.userGender = parcel.readString()
    this.userLink = parcel.readString()
  }

  override fun writeToParcel(dest: Parcel, flags: Int) {
    dest.writeString(jti)
    dest.writeString(iss)
    dest.writeString(aud)
    dest.writeString(nonce)
    dest.writeLong(exp)
    dest.writeLong(iat)
    dest.writeString(sub)
    dest.writeString(name)
    dest.writeString(givenName)
    dest.writeString(middleName)
    dest.writeString(familyName)
    dest.writeString(email)
    dest.writeString(picture)
    if (userFriends == null) {
      dest.writeStringList(null)
    } else {
      dest.writeStringList(ArrayList(userFriends))
    }
    dest.writeString(userBirthday)
    dest.writeMap(userAgeRange)
    dest.writeMap(userHometown)
    dest.writeMap(userLocation)
    dest.writeString(userGender)
    dest.writeString(userLink)
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (other !is AuthenticationTokenClaims) {
      return false
    }
    return jti == other.jti &&
        iss == other.iss &&
        aud == other.aud &&
        nonce == other.nonce &&
        exp == other.exp &&
        iat == other.iat &&
        sub == other.sub &&
        name == other.name &&
        givenName == other.givenName &&
        middleName == other.middleName &&
        familyName == other.familyName &&
        email == other.email &&
        picture == other.picture &&
        userFriends == other.userFriends &&
        userBirthday == other.userBirthday &&
        userAgeRange == other.userAgeRange &&
        userHometown == other.userHometown &&
        userLocation == other.userLocation &&
        userGender == other.userGender &&
        userLink == other.userLink
  }

  override fun hashCode(): Int {
    var result = 17
    result = result * 31 + jti.hashCode()
    result = result * 31 + iss.hashCode()
    result = result * 31 + aud.hashCode()
    result = result * 31 + nonce.hashCode()
    result = result * 31 + exp.hashCode()
    result = result * 31 + iat.hashCode()
    result = result * 31 + sub.hashCode()
    result = result * 31 + (name?.hashCode() ?: 0)
    result = result * 31 + (givenName?.hashCode() ?: 0)
    result = result * 31 + (middleName?.hashCode() ?: 0)
    result = result * 31 + (familyName?.hashCode() ?: 0)
    result = result * 31 + (email?.hashCode() ?: 0)
    result = result * 31 + (picture?.hashCode() ?: 0)
    result = result * 31 + (userFriends?.hashCode() ?: 0)
    result = result * 31 + (userBirthday?.hashCode() ?: 0)
    result = result * 31 + (userAgeRange?.hashCode() ?: 0)
    result = result * 31 + (userHometown?.hashCode() ?: 0)
    result = result * 31 + (userLocation?.hashCode() ?: 0)
    result = result * 31 + (userGender?.hashCode() ?: 0)
    result = result * 31 + (userLink?.hashCode() ?: 0)
    return result
  }

  override fun toString(): String {
    val claimsJsonObject = toJSONObject()
    return claimsJsonObject.toString()
  }

  override fun describeContents(): Int = 0

  private fun isValidClaims(claimsJson: JSONObject, expectedNonce: String): Boolean {
    if (claimsJson == null) {
      return false
    }

    val jti = claimsJson.optString(JSON_KEY_JIT)
    if (jti.isEmpty()) {
      return false
    }

    try {
      val iss = claimsJson.optString(JSON_KEY_ISS)
      if (iss.isEmpty() ||
          ((URL(iss).host != "facebook.com") && (URL(iss).host != "www.facebook.com"))) {
        return false
      }
    } catch (ex: MalformedURLException) {
      // since its MalformedURLException, iss is invalid
      return false
    }

    val aud = claimsJson.optString(JSON_KEY_AUD)
    if (aud.isEmpty() || aud != FacebookSdk.getApplicationId()) { // aud matched
      return false
    }

    val expDate = Date(claimsJson.optLong(JSON_KEY_EXP) * 1000)
    if (Date().after(expDate)) { // is expired
      return false
    }

    val iatInSeconds = claimsJson.optLong(JSON_KEY_IAT)
    val iatExpireDate = Date(iatInSeconds * 1000 + MAX_TIME_SINCE_TOKEN_ISSUED)
    if (Date().after(iatExpireDate)) { // issued too far in the past
      return false
    }

    val sub = claimsJson.optString(JSON_KEY_SUB)
    if (sub.isEmpty()) { // user ID is not valid
      return false
    }

    val nonce = claimsJson.optString(JSON_KEY_NONCE)
    if (nonce.isEmpty() || nonce != expectedNonce) { // incorrect nonce
      return false
    }

    return true
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  fun toEnCodedString(): String {
    val claimsJsonString = toString()
    return Base64.encodeToString(claimsJsonString.toByteArray(), Base64.URL_SAFE)
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  internal fun toJSONObject(): JSONObject {
    val jsonObject = JSONObject()
    jsonObject.put(JSON_KEY_JIT, this.jti)
    jsonObject.put(JSON_KEY_ISS, this.iss)
    jsonObject.put(JSON_KEY_AUD, this.aud)
    jsonObject.put(JSON_KEY_NONCE, this.nonce)
    jsonObject.put(JSON_KEY_EXP, this.exp)
    jsonObject.put(JSON_KEY_IAT, this.iat)
    if (this.sub != null) {
      jsonObject.put(JSON_KEY_SUB, this.sub)
    }
    if (this.name != null) {
      jsonObject.put(JSON_KEY_NAME, this.name)
    }
    if (this.givenName != null) {
      jsonObject.put(JSON_KEY_GIVEN_NAME, this.givenName)
    }
    if (this.middleName != null) {
      jsonObject.put(JSON_KEY_MIDDLE_NAME, this.middleName)
    }
    if (this.familyName != null) {
      jsonObject.put(JSON_KEY_FAMILY_NAME, this.familyName)
    }
    if (this.email != null) {
      jsonObject.put(JSON_KEY_EMAIL, this.email)
    }
    if (this.picture != null) {
      jsonObject.put(JSON_KEY_PICTURE, this.picture)
    }
    if (this.userFriends != null) {
      jsonObject.put(JSON_KEY_USER_FRIENDS, JSONArray(this.userFriends))
    }
    if (this.userBirthday != null) {
      jsonObject.put(JSON_KEY_USER_BIRTHDAY, this.userBirthday)
    }
    if (this.userAgeRange != null) {
      jsonObject.put(JSON_KEY_USER_AGE_RANGE, JSONObject(this.userAgeRange))
    }
    if (this.userHometown != null) {
      jsonObject.put(JSON_KEY_USER_HOMETOWN, JSONObject(this.userHometown))
    }
    if (this.userLocation != null) {
      jsonObject.put(JSON_KEY_USER_LOCATION, JSONObject(this.userLocation))
    }
    if (this.userGender != null) {
      jsonObject.put(JSON_KEY_USER_GENDER, this.userGender)
    }
    if (this.userLink != null) {
      jsonObject.put(JSON_KEY_USER_LINK, this.userLink)
    }

    return jsonObject
  }

  companion object {
    const val MAX_TIME_SINCE_TOKEN_ISSUED = DateUtils.MINUTE_IN_MILLIS * 10 // 10 minutes
    const val JSON_KEY_JIT = "jti"
    const val JSON_KEY_ISS = "iss"
    const val JSON_KEY_AUD = "aud"
    const val JSON_KEY_NONCE = "nonce"
    const val JSON_KEY_EXP = "exp"
    const val JSON_KEY_IAT = "iat"
    const val JSON_KEY_SUB = "sub"
    const val JSON_KEY_NAME = "name"
    const val JSON_KEY_GIVEN_NAME = "given_name"
    const val JSON_KEY_MIDDLE_NAME = "middle_name"
    const val JSON_KEY_FAMILY_NAME = "family_name"
    const val JSON_KEY_EMAIL = "email"
    const val JSON_KEY_PICTURE = "picture"
    const val JSON_KEY_USER_FRIENDS = "user_friends"
    const val JSON_KEY_USER_BIRTHDAY = "user_birthday"
    const val JSON_KEY_USER_AGE_RANGE = "user_age_range"
    const val JSON_KEY_USER_HOMETOWN = "user_hometown"
    const val JSON_KEY_USER_GENDER = "user_gender"
    const val JSON_KEY_USER_LINK = "user_link"
    const val JSON_KEY_USER_LOCATION = "user_location"

    internal fun JSONObject.getNullableString(name: String): String? {
      if (has(name)) {
        return getString(name)
      }
      return null
    }

    @Throws(JSONException::class)
    @JvmStatic
    internal fun createFromJSONObject(jsonObject: JSONObject): AuthenticationTokenClaims {
      val jti = jsonObject.getString(JSON_KEY_JIT)
      val iss = jsonObject.getString(JSON_KEY_ISS)
      val aud = jsonObject.getString(JSON_KEY_AUD)
      val nonce = jsonObject.getString(JSON_KEY_NONCE)
      val exp = jsonObject.getLong(JSON_KEY_EXP)
      val iat = jsonObject.getLong(JSON_KEY_IAT)
      val sub = jsonObject.getString(JSON_KEY_SUB)
      val name = jsonObject.getNullableString(JSON_KEY_NAME)
      val givenName = jsonObject.getNullableString(JSON_KEY_GIVEN_NAME)
      val middleName = jsonObject.getNullableString(JSON_KEY_MIDDLE_NAME)
      val familyName = jsonObject.getNullableString(JSON_KEY_FAMILY_NAME)
      val email = jsonObject.getNullableString(JSON_KEY_EMAIL)
      val picture = jsonObject.getNullableString(JSON_KEY_PICTURE)
      val userFriends = jsonObject.optJSONArray(JSON_KEY_USER_FRIENDS)
      val userBirthday = jsonObject.getNullableString(JSON_KEY_USER_BIRTHDAY)
      val userAgeRange = jsonObject.optJSONObject(JSON_KEY_USER_AGE_RANGE)
      val userHometown = jsonObject.optJSONObject(JSON_KEY_USER_HOMETOWN)
      val userLocation = jsonObject.optJSONObject(JSON_KEY_USER_LOCATION)
      val userGender = jsonObject.getNullableString(JSON_KEY_USER_GENDER)
      val userLink = jsonObject.getNullableString(JSON_KEY_USER_LINK)
      return AuthenticationTokenClaims(
          jti,
          iss,
          aud,
          nonce,
          exp,
          iat,
          sub,
          name,
          givenName,
          middleName,
          familyName,
          email,
          picture,
          if (userFriends == null) null else jsonArrayToStringList(userFriends),
          userBirthday,
          if (userAgeRange == null) null
          else convertJSONObjectToHashMap(userAgeRange) as Map<String, Int>?,
          if (userHometown == null) null else convertJSONObjectToStringMap(userHometown),
          if (userLocation == null) null else convertJSONObjectToStringMap(userLocation),
          userGender,
          userLink)
    }

    @JvmField
    val CREATOR: Parcelable.Creator<AuthenticationTokenClaims> =
        object : Parcelable.Creator<AuthenticationTokenClaims> {
          override fun createFromParcel(source: Parcel): AuthenticationTokenClaims {
            return AuthenticationTokenClaims(source)
          }

          override fun newArray(size: Int): Array<AuthenticationTokenClaims?> {
            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

import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import androidx.annotation.VisibleForTesting
import com.facebook.internal.Validate
import org.json.JSONException
import org.json.JSONObject

/**
 * This class represents an immutable header for using AuthenticationToken. It includes all metadata
 * or key values of the header
 *
 * WARNING: This feature is currently in development and not intended for external usage.
 */
class AuthenticationTokenHeader : Parcelable {

  /** Value that represents the algorithm that was used to sign the JWT. */
  val alg: String

  /** The type of the JWT. */
  val typ: String

  /** Key identifier used in identifying the key to be used to verify the signature. */
  val kid: String

  constructor(encodedHeaderString: String) {
    // verify header
    require(isValidHeader(encodedHeaderString)) { "Invalid Header" }

    val decodedBytes = Base64.decode(encodedHeaderString, Base64.DEFAULT)
    val claimsString = String(decodedBytes)
    val jsonObj = JSONObject(claimsString)

    this.alg = jsonObj.getString("alg")
    this.typ = jsonObj.getString("typ")
    this.kid = jsonObj.getString("kid")
  }

  internal constructor(parcel: Parcel) {
    val alg = parcel.readString()
    this.alg = Validate.notNullOrEmpty(alg, "alg")

    val typ = parcel.readString()
    this.typ = Validate.notNullOrEmpty(typ, "typ")

    val kid = parcel.readString()
    this.kid = Validate.notNullOrEmpty(kid, "kid")
  }

  /**
   * This constructor is only for caching only. NOTE: Using the following constructor is strongly
   * discouraged, it will bypass any validation
   */
  @Throws(JSONException::class)
  internal constructor(jsonObject: JSONObject) {
    this.alg = jsonObject.getString("alg")
    this.typ = jsonObject.getString("typ")
    this.kid = jsonObject.getString("kid")
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  constructor(alg: String, typ: String, kid: String) {
    this.alg = alg
    this.typ = typ
    this.kid = kid
  }

  override fun writeToParcel(dest: Parcel, flags: Int) {
    dest.writeString(alg)
    dest.writeString(typ)
    dest.writeString(kid)
  }

  override fun describeContents(): Int = 0

  override fun toString(): String {
    val headerJsonObject = toJSONObject()
    return headerJsonObject.toString()
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (other !is AuthenticationTokenHeader) {
      return false
    }
    return alg == other.alg && typ == other.typ && kid == other.kid
  }

  override fun hashCode(): Int {
    var result = 17
    result = result * 31 + alg.hashCode()
    result = result * 31 + typ.hashCode()
    result = result * 31 + kid.hashCode()
    return result
  }

  /**
   * check if the input header string is a valid id token header
   *
   * @param headerString the header string
   */
  private fun isValidHeader(headerString: String): Boolean {
    Validate.notEmpty(headerString, "encodedHeaderString")

    val decodedBytes = Base64.decode(headerString, Base64.DEFAULT)
    val claimsString = String(decodedBytes)

    return try {
      val jsonObj = JSONObject(claimsString)

      val alg = jsonObj.optString("alg")
      val validAlg = alg.isNotEmpty() && alg == "RS256"

      val hasKid = jsonObj.optString("kid").isNotEmpty()
      val validTyp = jsonObj.optString("typ").isNotEmpty()

      validAlg && hasKid && validTyp
    } catch (_ex: JSONException) {
      // return false if there any problem parsing the JSON string
      false
    }
  }

  internal fun toJSONObject(): JSONObject {
    val jsonObject = JSONObject()
    jsonObject.put("alg", this.alg)
    jsonObject.put("typ", this.typ)
    jsonObject.put("kid", this.kid)

    return jsonObject
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  fun toEnCodedString(): String {
    val claimsJsonString = toString()
    return Base64.encodeToString(claimsJsonString.toByteArray(), Base64.DEFAULT)
  }

  companion object {
    @JvmField
    val CREATOR: Parcelable.Creator<AuthenticationTokenHeader> =
        object : Parcelable.Creator<AuthenticationTokenHeader> {
          override fun createFromParcel(source: Parcel): AuthenticationTokenHeader {
            return AuthenticationTokenHeader(source)
          }

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