<commit_before>package com.btcontract.wallet

import immortan._
import android.widget._
import fr.acinq.eclair._
import fr.acinq.bitcoin._
import immortan.crypto.Tools._
import fr.acinq.eclair.channel._
import scala.concurrent.duration._
import com.btcontract.wallet.Colors._
import com.btcontract.wallet.R.string._

import java.util.{Date, TimerTask}
import android.view.{View, ViewGroup}
import android.graphics.{Bitmap, BitmapFactory}
import immortan.utils.{BitcoinUri, InputParser, PaymentRequestExt, Rx}
import com.chauthai.swipereveallayout.{SwipeRevealLayout, ViewBinderHelper}
import com.btcontract.wallet.BaseActivity.StringOps
import fr.acinq.eclair.wire.HostedChannelBranding
import androidx.recyclerview.widget.RecyclerView
import immortan.ChannelListener.Malfunction
import com.google.common.cache.LoadingCache
import com.indicator.ChannelIndicatorLine
import androidx.appcompat.app.AlertDialog
import androidx.cardview.widget.CardView
import com.ornach.nobobutton.NoboButton
import rx.lang.scala.Subscription
import immortan.wire.HostedState
import android.text.Spanned
import android.os.Bundle


object ChanActivity {
  def getHcState(hc: HostedCommits): String = {
    val preimages = hc.revealedFulfills.map(_.ourPreimage.toHex).mkString("\n")
    val hostedState = HostedState(hc.remoteInfo.nodeId, hc.remoteInfo.nodeSpecificPubKey, hc.lastCrossSignedState)
    val serializedHostedState = immortan.wire.ExtCodecs.hostedStateCodec.encode(value = hostedState).require.toHex
    WalletApp.app.getString(ln_hosted_chan_state).format(getDetails(hc), serializedHostedState, preimages)
  }

  def getDetails(cs: Commitments): String = {
    val remoteId = cs.remoteInfo.nodeId.toString
    val localId = cs.remoteInfo.nodeSpecificPubKey.toString
    val shortId = cs.updateOpt.map(_.shortChannelId.toString).getOrElse("unknown")
    val stamp = WalletApp.app.when(new Date(cs.startedAt), WalletApp.app.dateFormat)
    WalletApp.app.getString(ln_chan_details).format(remoteId, localId, shortId, stamp)
  }
}

class ChanActivity extends ChanErrorHandlerActivity with ChoiceReceiver with HasTypicalChainFee with ChannelListener { me =>
  private[this] lazy val chanContainer = findViewById(R.id.chanContainer).asInstanceOf[LinearLayout]
  private[this] lazy val chanList = findViewById(R.id.chanList).asInstanceOf[ListView]

  private[this] lazy val brandingInfos = WalletApp.txDataBag.db.txWrap(getBrandingInfos.toMap)
  private[this] lazy val normalChanActions = getResources.getStringArray(R.array.ln_normal_chan_actions).map(_.html)
  private[this] lazy val hostedChanActions = getResources.getStringArray(R.array.ln_hosted_chan_actions).map(_.html)
  private[this] var updateSubscription = Option.empty[Subscription]
  private[this] var csToDisplay = Seq.empty[ChanAndCommits]

  val hcImageMemo: LoadingCache[Bytes, Bitmap] = memoize {
    bytes => BitmapFactory.decodeByteArray(bytes, 0, bytes.length)
  }

  val chanAdapter: BaseAdapter = new BaseAdapter {
    private[this] val viewBinderHelper = new ViewBinderHelper
    override def getItem(pos: Int): ChanAndCommits = csToDisplay(pos)
    override def getItemId(position: Int): Long = position
    override def getCount: Int = csToDisplay.size

    def getView(position: Int, savedView: View, parent: ViewGroup): View = {
      val card = if (null == savedView) getLayoutInflater.inflate(R.layout.frag_chan_card, null) else savedView

      val cardView = (getItem(position), card.getTag) match {
        case (ChanAndCommits(chan: ChannelHosted, hc: HostedCommits), view: HostedViewHolder) => view.fill(chan, hc)
        case (ChanAndCommits(chan: ChannelHosted, hc: HostedCommits), _) => new HostedViewHolder(card).fill(chan, hc)
        case (ChanAndCommits(chan: ChannelNormal, commits: NormalCommits), view: NormalViewHolder) => view.fill(chan, commits)
        case (ChanAndCommits(chan: ChannelNormal, commits: NormalCommits), _) => new NormalViewHolder(card).fill(chan, commits)
        case _ => throw new RuntimeException
      }

      viewBinderHelper.bind(cardView.swipeWrap, position.toString)
      card.setTag(cardView)
      card
    }
  }

  abstract class ChanCardViewHolder(view: View) extends RecyclerView.ViewHolder(view) {
    val swipeWrap: SwipeRevealLayout = itemView.asInstanceOf[SwipeRevealLayout]

    val removeItem: NoboButton = swipeWrap.findViewById(R.id.removeItem).asInstanceOf[NoboButton]
    val channelCard: CardView = swipeWrap.findViewById(R.id.channelCard).asInstanceOf[CardView]

    val hcBranding: RelativeLayout = swipeWrap.findViewById(R.id.hcBranding).asInstanceOf[RelativeLayout]
    val hcSupportInfo: TextView = swipeWrap.findViewById(R.id.hcSupportInfo).asInstanceOf[TextView]
    val hcImage: ImageView = swipeWrap.findViewById(R.id.hcImage).asInstanceOf[ImageView]

    val baseBar: ProgressBar = swipeWrap.findViewById(R.id.baseBar).asInstanceOf[ProgressBar]
    val overBar: ProgressBar = swipeWrap.findViewById(R.id.overBar).asInstanceOf[ProgressBar]
    val peerAddress: TextView = swipeWrap.findViewById(R.id.peerAddress).asInstanceOf[TextView]
    val chanState: View = swipeWrap.findViewById(R.id.chanState).asInstanceOf[View]

    val canSendText: TextView = swipeWrap.findViewById(R.id.canSendText).asInstanceOf[TextView]
    val canReceiveText: TextView = swipeWrap.findViewById(R.id.canReceiveText).asInstanceOf[TextView]
    val refundableAmountText: TextView = swipeWrap.findViewById(R.id.refundableAmountText).asInstanceOf[TextView]
    val paymentsInFlightText: TextView = swipeWrap.findViewById(R.id.paymentsInFlightText).asInstanceOf[TextView]
    val totalCapacityText: TextView = swipeWrap.findViewById(R.id.totalCapacityText).asInstanceOf[TextView]
    val overrideProposal: TextView = swipeWrap.findViewById(R.id.overrideProposal).asInstanceOf[TextView]
    val extraInfoText: TextView = swipeWrap.findViewById(R.id.extraInfoText).asInstanceOf[TextView]

    val wrappers: Seq[View] =
      swipeWrap.findViewById(R.id.progressBars).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.totalCapacity).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.refundableAmount).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.paymentsInFlight).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.canReceive).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.canSend).asInstanceOf[View] ::
        Nil

    def visibleExcept(goneRes: Int*): Unit = for (wrap <- wrappers) {
      val hideView = goneRes.contains(wrap.getId)
      setVis(!hideView, wrap)
    }

    baseBar.setMax(1000)
    overBar.setMax(1000)
  }

  class NormalViewHolder(view: View) extends ChanCardViewHolder(view) {
    def fill(chan: ChannelNormal, cs: NormalCommits): NormalViewHolder = {

      val capacity: Satoshi = cs.commitInput.txOut.amount
      val barCanReceive = (cs.availableForReceive.toLong / capacity.toLong).toInt
      val barCanSend = (cs.latestReducedRemoteSpec.toRemote.toLong / capacity.toLong).toInt
      val barLocalReserve = (cs.latestReducedRemoteSpec.toRemote - cs.availableForSend).toLong / capacity.toLong
      val tempFeeMismatch = chan.data match { case norm: DATA_NORMAL => norm.feeUpdateRequired case _ => false }
      val inFlight: MilliSatoshi = cs.latestReducedRemoteSpec.htlcs.foldLeft(0L.msat)(_ + _.add.amountMsat)
      val refundable: MilliSatoshi = cs.latestReducedRemoteSpec.toRemote + inFlight

      if (Channel isWaiting chan) {
        setVis(isVisible = true, extraInfoText)
        extraInfoText.setText(getString(ln_info_opening).html)
        channelCard setOnClickListener bringChanOptions(normalChanActions.take(2), cs)
        visibleExcept(R.id.progressBars, R.id.paymentsInFlight, R.id.canReceive, R.id.canSend)
      } else if (Channel isOperational chan) {
        channelCard setOnClickListener bringChanOptions(normalChanActions, cs)
        setVis(isVisible = cs.updateOpt.isEmpty || tempFeeMismatch, extraInfoText)
        if (cs.updateOpt.isEmpty) extraInfoText.setText(ln_info_no_update)
        if (tempFeeMismatch) extraInfoText.setText(ln_info_fee_mismatch)
        visibleExcept(goneRes = -1)
      } else {
        val closeInfoRes = chan.data match { case c: DATA_CLOSING => closedBy(c) case _ => ln_info_shutdown }
        channelCard setOnClickListener bringChanOptions(normalChanActions.take(2), cs)
        visibleExcept(R.id.progressBars, R.id.canReceive, R.id.canSend)
        extraInfoText.setText(getString(closeInfoRes).html)
        setVis(isVisible = true, extraInfoText)
      }

      removeItem setOnClickListener onButtonTap {
        def proceed: Unit = chan process CMD_CLOSE(None, force = true)
        val builder = confirmationBuilder(cs, getString(confirm_ln_normal_chan_force_close).html)
        mkCheckForm(alert => runAnd(alert.dismiss)(proceed), none, builder, dialog_ok, dialog_cancel)
      }

      setVis(isVisible = false, overrideProposal)
      setVis(isVisible = false, hcBranding)

      ChannelIndicatorLine.setView(chanState, chan)
      peerAddress.setText(cs.remoteInfo.address.toString)
      overBar.setProgress(barCanSend min barLocalReserve.toInt)
      baseBar.setSecondaryProgress(barCanSend + barCanReceive)
      baseBar.setProgress(barCanSend)

      totalCapacityText.setText(sumOrNothing(capacity.toMilliSatoshi, cardIn).html)
      canReceiveText.setText(sumOrNothing(cs.availableForReceive, cardOut).html)
      canSendText.setText(sumOrNothing(cs.availableForSend, cardIn).html)
      refundableAmountText.setText(sumOrNothing(refundable, cardIn).html)
      paymentsInFlightText.setText(sumOrNothing(inFlight, cardIn).html)
      this
    }
  }

  class HostedViewHolder(view: View) extends ChanCardViewHolder(view) {
    def fill(chan: ChannelHosted, hc: HostedCommits): HostedViewHolder = {
      val capacity = hc.lastCrossSignedState.initHostedChannel.channelCapacityMsat
      val inFlight = hc.nextLocalSpec.htlcs.foldLeft(0L.msat)(_ + _.add.amountMsat)
      val barCanReceive = (hc.availableForReceive.toLong / capacity.toLong).toInt
      val barCanSend = (hc.availableForSend.toLong / capacity.toLong).toInt

      val errorText = (hc.localError, hc.remoteError) match {
        case Some(error) ~ _ => s"LOCAL: ${ErrorExt extractDescription error}"
        case _ ~ Some(error) => s"REMOTE: ${ErrorExt extractDescription error}"
        case _ => new String
      }

      val brandOpt = brandingInfos.get(hc.remoteInfo.nodeId)
      setVis(isVisible = hc.overrideProposal.isDefined, overrideProposal)
      setVis(isVisible = brandOpt.isDefined, hcBranding)

      for (HostedChannelBranding(_, pngIcon, contactInfo) <- brandOpt) {
        pngIcon.map(_.toArray).map(hcImageMemo.get).foreach(hcImage.setImageBitmap)
        hcSupportInfo.setText(contactInfo)
      }

      removeItem setOnClickListener onButtonTap {
        if (hc.localSpec.htlcs.nonEmpty) snack(chanContainer, getString(ln_hosted_chan_remove_impossible).html, R.string.dialog_ok, _.dismiss)
        else mkCheckForm(alert => runAnd(alert.dismiss)(me removeHc hc), none, confirmationBuilder(hc, getString(confirm_ln_hosted_chan_remove).html), dialog_ok, dialog_cancel)
        swipeWrap.close(true)
      }

      overrideProposal setOnClickListener onButtonTap {
        val newBalance = hc.lastCrossSignedState.initHostedChannel.channelCapacityMsat - hc.overrideProposal.get.localBalanceMsat
        val current = WalletApp.denom.parsedWithSign(hc.availableForReceive, cardIn, cardZero)
        val overridden = WalletApp.denom.parsedWithSign(newBalance, cardIn, cardZero)

        def proceed: Unit = chan process CMD_HOSTED_STATE_OVERRIDE(hc.overrideProposal.get)
        val builder = confirmationBuilder(hc, getString(ln_hc_override_warn).format(current, overridden).html)
        mkCheckForm(alert => runAnd(alert.dismiss)(proceed), none, builder, dialog_ok, dialog_cancel)
      }<commit_msg>UI fix<commit_after>package com.btcontract.wallet

import immortan._
import android.widget._
import fr.acinq.eclair._
import fr.acinq.bitcoin._
import immortan.crypto.Tools._
import fr.acinq.eclair.channel._
import scala.concurrent.duration._
import com.btcontract.wallet.Colors._
import com.btcontract.wallet.R.string._

import java.util.{Date, TimerTask}
import android.view.{View, ViewGroup}
import android.graphics.{Bitmap, BitmapFactory}
import immortan.utils.{BitcoinUri, InputParser, PaymentRequestExt, Rx}
import com.chauthai.swipereveallayout.{SwipeRevealLayout, ViewBinderHelper}
import com.btcontract.wallet.BaseActivity.StringOps
import fr.acinq.eclair.wire.HostedChannelBranding
import androidx.recyclerview.widget.RecyclerView
import immortan.ChannelListener.Malfunction
import com.google.common.cache.LoadingCache
import com.indicator.ChannelIndicatorLine
import androidx.appcompat.app.AlertDialog
import androidx.cardview.widget.CardView
import com.ornach.nobobutton.NoboButton
import rx.lang.scala.Subscription
import immortan.wire.HostedState
import android.text.Spanned
import android.os.Bundle


object ChanActivity {
  def getHcState(hc: HostedCommits): String = {
    val preimages = hc.revealedFulfills.map(_.ourPreimage.toHex).mkString("\n")
    val hostedState = HostedState(hc.remoteInfo.nodeId, hc.remoteInfo.nodeSpecificPubKey, hc.lastCrossSignedState)
    val serializedHostedState = immortan.wire.ExtCodecs.hostedStateCodec.encode(value = hostedState).require.toHex
    WalletApp.app.getString(ln_hosted_chan_state).format(getDetails(hc), serializedHostedState, preimages)
  }

  def getDetails(cs: Commitments): String = {
    val remoteId = cs.remoteInfo.nodeId.toString
    val localId = cs.remoteInfo.nodeSpecificPubKey.toString
    val shortId = cs.updateOpt.map(_.shortChannelId.toString).getOrElse("unknown")
    val stamp = WalletApp.app.when(new Date(cs.startedAt), WalletApp.app.dateFormat)
    WalletApp.app.getString(ln_chan_details).format(remoteId, localId, shortId, stamp)
  }
}

class ChanActivity extends ChanErrorHandlerActivity with ChoiceReceiver with HasTypicalChainFee with ChannelListener { me =>
  private[this] lazy val chanContainer = findViewById(R.id.chanContainer).asInstanceOf[LinearLayout]
  private[this] lazy val chanList = findViewById(R.id.chanList).asInstanceOf[ListView]

  private[this] lazy val brandingInfos = WalletApp.txDataBag.db.txWrap(getBrandingInfos.toMap)
  private[this] lazy val normalChanActions = getResources.getStringArray(R.array.ln_normal_chan_actions).map(_.html)
  private[this] lazy val hostedChanActions = getResources.getStringArray(R.array.ln_hosted_chan_actions).map(_.html)
  private[this] var updateSubscription = Option.empty[Subscription]
  private[this] var csToDisplay = Seq.empty[ChanAndCommits]

  val hcImageMemo: LoadingCache[Bytes, Bitmap] = memoize {
    bytes => BitmapFactory.decodeByteArray(bytes, 0, bytes.length)
  }

  val chanAdapter: BaseAdapter = new BaseAdapter {
    private[this] val viewBinderHelper = new ViewBinderHelper
    override def getItem(pos: Int): ChanAndCommits = csToDisplay(pos)
    override def getItemId(position: Int): Long = position
    override def getCount: Int = csToDisplay.size

    def getView(position: Int, savedView: View, parent: ViewGroup): View = {
      val card = if (null == savedView) getLayoutInflater.inflate(R.layout.frag_chan_card, null) else savedView

      val cardView = (getItem(position), card.getTag) match {
        case (ChanAndCommits(chan: ChannelHosted, hc: HostedCommits), view: HostedViewHolder) => view.fill(chan, hc)
        case (ChanAndCommits(chan: ChannelHosted, hc: HostedCommits), _) => new HostedViewHolder(card).fill(chan, hc)
        case (ChanAndCommits(chan: ChannelNormal, commits: NormalCommits), view: NormalViewHolder) => view.fill(chan, commits)
        case (ChanAndCommits(chan: ChannelNormal, commits: NormalCommits), _) => new NormalViewHolder(card).fill(chan, commits)
        case _ => throw new RuntimeException
      }

      viewBinderHelper.bind(cardView.swipeWrap, position.toString)
      card.setTag(cardView)
      card
    }
  }

  abstract class ChanCardViewHolder(view: View) extends RecyclerView.ViewHolder(view) {
    val swipeWrap: SwipeRevealLayout = itemView.asInstanceOf[SwipeRevealLayout]

    val removeItem: NoboButton = swipeWrap.findViewById(R.id.removeItem).asInstanceOf[NoboButton]
    val channelCard: CardView = swipeWrap.findViewById(R.id.channelCard).asInstanceOf[CardView]

    val hcBranding: RelativeLayout = swipeWrap.findViewById(R.id.hcBranding).asInstanceOf[RelativeLayout]
    val hcSupportInfo: TextView = swipeWrap.findViewById(R.id.hcSupportInfo).asInstanceOf[TextView]
    val hcImage: ImageView = swipeWrap.findViewById(R.id.hcImage).asInstanceOf[ImageView]

    val baseBar: ProgressBar = swipeWrap.findViewById(R.id.baseBar).asInstanceOf[ProgressBar]
    val overBar: ProgressBar = swipeWrap.findViewById(R.id.overBar).asInstanceOf[ProgressBar]
    val peerAddress: TextView = swipeWrap.findViewById(R.id.peerAddress).asInstanceOf[TextView]
    val chanState: View = swipeWrap.findViewById(R.id.chanState).asInstanceOf[View]

    val canSendText: TextView = swipeWrap.findViewById(R.id.canSendText).asInstanceOf[TextView]
    val canReceiveText: TextView = swipeWrap.findViewById(R.id.canReceiveText).asInstanceOf[TextView]
    val refundableAmountText: TextView = swipeWrap.findViewById(R.id.refundableAmountText).asInstanceOf[TextView]
    val paymentsInFlightText: TextView = swipeWrap.findViewById(R.id.paymentsInFlightText).asInstanceOf[TextView]
    val totalCapacityText: TextView = swipeWrap.findViewById(R.id.totalCapacityText).asInstanceOf[TextView]
    val overrideProposal: TextView = swipeWrap.findViewById(R.id.overrideProposal).asInstanceOf[TextView]
    val extraInfoText: TextView = swipeWrap.findViewById(R.id.extraInfoText).asInstanceOf[TextView]

    val wrappers: Seq[View] =
      swipeWrap.findViewById(R.id.progressBars).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.totalCapacity).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.refundableAmount).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.paymentsInFlight).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.canReceive).asInstanceOf[View] ::
        swipeWrap.findViewById(R.id.canSend).asInstanceOf[View] ::
        Nil

    def visibleExcept(goneRes: Int*): Unit = for (wrap <- wrappers) {
      val hideView = goneRes.contains(wrap.getId)
      setVis(!hideView, wrap)
    }

    baseBar.setMax(1000)
    overBar.setMax(1000)
  }

  class NormalViewHolder(view: View) extends ChanCardViewHolder(view) {
    def fill(chan: ChannelNormal, cs: NormalCommits): NormalViewHolder = {

      val capacity: Satoshi = cs.commitInput.txOut.amount
      val barCanReceive = (cs.availableForReceive.toLong / capacity.toLong).toInt
      val barCanSend = (cs.latestReducedRemoteSpec.toRemote.toLong / capacity.toLong).toInt
      val barLocalReserve = (cs.latestReducedRemoteSpec.toRemote - cs.availableForSend).toLong / capacity.toLong
      val tempFeeMismatch = chan.data match { case norm: DATA_NORMAL => norm.feeUpdateRequired case _ => false }
      val inFlight: MilliSatoshi = cs.latestReducedRemoteSpec.htlcs.foldLeft(0L.msat)(_ + _.add.amountMsat)
      val refundable: MilliSatoshi = cs.latestReducedRemoteSpec.toRemote + inFlight

      if (Channel isWaiting chan) {
        setVis(isVisible = true, extraInfoText)
        extraInfoText.setText(getString(ln_info_opening).html)
        channelCard setOnClickListener bringChanOptions(normalChanActions.take(2), cs)
        visibleExcept(R.id.progressBars, R.id.paymentsInFlight, R.id.canReceive, R.id.canSend)
      } else if (Channel isOperational chan) {
        channelCard setOnClickListener bringChanOptions(normalChanActions, cs)
        setVis(isVisible = cs.updateOpt.isEmpty || tempFeeMismatch, extraInfoText)
        if (cs.updateOpt.isEmpty) extraInfoText.setText(ln_info_no_update)
        if (tempFeeMismatch) extraInfoText.setText(ln_info_fee_mismatch)
        visibleExcept(goneRes = -1)
      } else {
        val closeInfoRes = chan.data match { case c: DATA_CLOSING => closedBy(c) case _ => ln_info_shutdown }
        channelCard setOnClickListener bringChanOptions(normalChanActions.take(2), cs)
        visibleExcept(R.id.progressBars, R.id.canReceive, R.id.canSend)
        extraInfoText.setText(getString(closeInfoRes).html)
        setVis(isVisible = true, extraInfoText)
      }

      removeItem setOnClickListener onButtonTap {
        def proceed: Unit = chan process CMD_CLOSE(None, force = true)
        val builder = confirmationBuilder(cs, getString(confirm_ln_normal_chan_force_close).html)
        mkCheckForm(alert => runAnd(alert.dismiss)(proceed), none, builder, dialog_ok, dialog_cancel)
      }

      setVis(isVisible = false, overrideProposal)
      setVis(isVisible = false, hcBranding)

      ChannelIndicatorLine.setView(chanState, chan)
      peerAddress.setText(cs.remoteInfo.address.toString)
      overBar.setProgress(barCanSend min barLocalReserve.toInt)
      baseBar.setSecondaryProgress(barCanSend + barCanReceive)
      baseBar.setProgress(barCanSend)

      totalCapacityText.setText(sumOrNothing(capacity.toMilliSatoshi, cardIn).html)
      canReceiveText.setText(sumOrNothing(cs.availableForReceive, cardOut).html)
      canSendText.setText(sumOrNothing(cs.availableForSend, cardIn).html)
      refundableAmountText.setText(sumOrNothing(refundable, cardIn).html)
      paymentsInFlightText.setText(sumOrNothing(inFlight, cardIn).html)
      this
    }
  }

  class HostedViewHolder(view: View) extends ChanCardViewHolder(view) {
    def fill(chan: ChannelHosted, hc: HostedCommits): HostedViewHolder = {
      val capacity = hc.lastCrossSignedState.initHostedChannel.channelCapacityMsat
      val inFlight = hc.nextLocalSpec.htlcs.foldLeft(0L.msat)(_ + _.add.amountMsat)
      val barCanReceive = (hc.availableForReceive.toLong / capacity.truncateToSatoshi.toLong).toInt
      val barCanSend = (hc.availableForSend.toLong / capacity.truncateToSatoshi.toLong).toInt

      val errorText = (hc.localError, hc.remoteError) match {
        case Some(error) ~ _ => s"LOCAL: ${ErrorExt extractDescription error}"
        case _ ~ Some(error) => s"REMOTE: ${ErrorExt extractDescription error}"
        case _ => new String
      }

      val brandOpt = brandingInfos.get(hc.remoteInfo.nodeId)
      setVis(isVisible = hc.overrideProposal.isDefined, overrideProposal)
      setVis(isVisible = brandOpt.isDefined, hcBranding)

      for (HostedChannelBranding(_, pngIcon, contactInfo) <- brandOpt) {
        pngIcon.map(_.toArray).map(hcImageMemo.get).foreach(hcImage.setImageBitmap)
        hcSupportInfo.setText(contactInfo)
      }

      removeItem setOnClickListener onButtonTap {
        if (hc.localSpec.htlcs.nonEmpty) snack(chanContainer, getString(ln_hosted_chan_remove_impossible).html, R.string.dialog_ok, _.dismiss)
        else mkCheckForm(alert => runAnd(alert.dismiss)(me removeHc hc), none, confirmationBuilder(hc, getString(confirm_ln_hosted_chan_remove).html), dialog_ok, dialog_cancel)
        swipeWrap.close(true)
      }

      overrideProposal setOnClickListener onButtonTap {
        val newBalance = hc.lastCrossSignedState.initHostedChannel.channelCapacityMsat - hc.overrideProposal.get.localBalanceMsat
        val current = WalletApp.denom.parsedWithSign(hc.availableForReceive, cardIn, cardZero)
        val overridden = WalletApp.denom.parsedWithSign(newBalance, cardIn, cardZero)

        def proceed: Unit = chan process CMD_HOSTED_STATE_OVERRIDE(hc.overrideProposal.get)
        val builder = confirmationBuilder(hc, getString(ln_hc_override_warn).format(current, overridden).html)
        mkCheckForm(alert => runAnd(alert.dismiss)(proceed), none, builder, dialog_ok, dialog_cancel)
      }
{'fraction_non_alphanumeric': 0.0979227236337589, 'fraction_numerical': 0.0011797918510091435, 'mean_word_length': 4.766277939747328, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '134774', 'n_tokens_mistral': 7592, 'n_tokens_neox': 7264, 'n_words': 1237}