mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-21 21:29:00 +00:00
Fix various crashes
This commit is contained in:
parent
e1406f3a43
commit
bb01dc9b7d
18 changed files with 90 additions and 62 deletions
|
|
@ -155,7 +155,7 @@ android {
|
|||
buildConfigField "String", "TESTING_LEVEL", "\"production\""
|
||||
resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW"
|
||||
|
||||
versionCode 3070
|
||||
versionCode 3077
|
||||
versionName "3.4.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
|||
|
|
@ -900,7 +900,7 @@
|
|||
<string name="renew_subscription">有料プランを再開する</string>
|
||||
<string name="thanks_for_subscribing">有料プランにご加入いただきありがとうございます</string>
|
||||
<string name="subscribe_options_title">有料プランの期間をあなたのご都合に合わせて選びましょう</string>
|
||||
<string name="ending_on">%@に止める</string>
|
||||
<string name="ending_on">%sに止める</string>
|
||||
<string name="not_recurring">くり返さない</string>
|
||||
<string name="cancelled">キャンセルしました</string>
|
||||
<string name="send_invites">招待状を送る</string>
|
||||
|
|
|
|||
|
|
@ -1096,6 +1096,6 @@
|
|||
<string name="sent_card">%s gönderdin</string>
|
||||
<string name="take_me_back">Beni geri al</string>
|
||||
<string name="not_now">Şimdi olmaz</string>
|
||||
<string name="need_help_header_description">Sorularınızın bir başka oyuncu tarafından cevaplanması için% s içinde bir mesaj gönderin.</string>
|
||||
<string name="need_help_header_description">Sorularınızın bir başka oyuncu tarafından cevaplanması için%s içinde bir mesaj gönderin.</string>
|
||||
<string name="setup_task_habit_1_notes">Bir Alışkanlık, Günlük İş veya Yapılacak İş</string>
|
||||
</resources>
|
||||
|
|
@ -103,6 +103,7 @@ abstract class HabiticaBaseApplication : Application() {
|
|||
}
|
||||
var builder = ImageLoader.Builder(this)
|
||||
.transition(CrossfadeTransition())
|
||||
.allowHardware(false)
|
||||
.componentRegistry {
|
||||
if (SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder(this@HabiticaBaseApplication))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import org.solovyev.android.checkout.RequestListener
|
|||
import org.solovyev.android.checkout.ResponseCodes
|
||||
import retrofit2.HttpException
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePurchaseVerifier() {
|
||||
private val apiClient: ApiClient
|
||||
|
|
@ -40,8 +41,8 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
|
|||
purchasedOrderList.add(purchase.orderId)
|
||||
verifiedPurchases.add(purchase)
|
||||
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
|
||||
val giftedID = removeGift(purchase.sku)
|
||||
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID))
|
||||
val gift = removeGift(purchase.sku)
|
||||
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, gift?.second))
|
||||
}) { throwable: Throwable ->
|
||||
handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases)
|
||||
}
|
||||
|
|
@ -52,8 +53,8 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
|
|||
purchasedOrderList.add(purchase.orderId)
|
||||
verifiedPurchases.add(purchase)
|
||||
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
|
||||
val giftedID = removeGift(purchase.sku)
|
||||
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID))
|
||||
val gift = removeGift(purchase.sku)
|
||||
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, gift?.second))
|
||||
}) { throwable: Throwable ->
|
||||
handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases)
|
||||
}
|
||||
|
|
@ -104,9 +105,14 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
|
|||
validationRequest.transaction = Transaction()
|
||||
validationRequest.transaction?.receipt = purchase.data
|
||||
validationRequest.transaction?.signature = purchase.signature
|
||||
if (pendingGifts.containsKey(purchase.sku)) {
|
||||
validationRequest.gift = IAPGift()
|
||||
validationRequest.gift?.uuid = pendingGifts[purchase.sku]
|
||||
pendingGifts[purchase.sku]?.let { gift ->
|
||||
// If the gift and the purchase happened within 5 minutes, we consider them to match.
|
||||
// Otherwise the gift is probably an old one that wasn't cleared out correctly
|
||||
if (abs(gift.first.time - purchase.time) < 300000) {
|
||||
validationRequest.gift = IAPGift(gift.second)
|
||||
} else {
|
||||
removeGift(purchase.sku)
|
||||
}
|
||||
}
|
||||
return validationRequest
|
||||
}
|
||||
|
|
@ -132,16 +138,16 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
|
|||
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
|
||||
}
|
||||
|
||||
private fun loadPendingGifts(): MutableMap<String?, String?> {
|
||||
val outputMap: MutableMap<String?, String?> = HashMap()
|
||||
private fun loadPendingGifts(): MutableMap<String, Pair<Date, String>> {
|
||||
val outputMap: MutableMap<String, Pair<Date, String>> = HashMap()
|
||||
try {
|
||||
val jsonString = preferences?.getString(PENDING_GIFTS_KEY, JSONObject().toString()) ?: ""
|
||||
val jsonObject = JSONObject(jsonString)
|
||||
val keysItr = jsonObject.keys()
|
||||
while (keysItr.hasNext()) {
|
||||
val key = keysItr.next()
|
||||
val value = jsonObject[key] as String
|
||||
outputMap[key] = value
|
||||
val value = jsonObject.getJSONArray(key)
|
||||
outputMap[key] = Pair(value[0] as Date, value[1] as String)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
RxErrorHandler.reportError(e)
|
||||
|
|
@ -151,19 +157,19 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
|
|||
|
||||
companion object {
|
||||
private const val PURCHASED_PRODUCTS_KEY = "PURCHASED_PRODUCTS"
|
||||
private const val PENDING_GIFTS_KEY = "PENDING_GIFTS"
|
||||
private var pendingGifts: MutableMap<String?, String?> = HashMap()
|
||||
private const val PENDING_GIFTS_KEY = "PENDING_GIFTS_DATED"
|
||||
private var pendingGifts: MutableMap<String, Pair<Date, String>> = HashMap()
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
fun addGift(sku: String?, userID: String?) {
|
||||
pendingGifts[sku] = userID
|
||||
fun addGift(sku: String, userID: String) {
|
||||
pendingGifts[sku] = Pair(Date(), userID)
|
||||
savePendingGifts()
|
||||
}
|
||||
|
||||
private fun removeGift(sku: String): String? {
|
||||
val giftedID = pendingGifts.remove(sku)
|
||||
private fun removeGift(sku: String): Pair<Date, String>? {
|
||||
val gift = pendingGifts.remove(sku)
|
||||
savePendingGifts()
|
||||
return giftedID
|
||||
return gift
|
||||
}
|
||||
|
||||
private fun savePendingGifts() {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import javax.security.cert.CertificateException
|
|||
|
||||
// https://stackoverflow.com/a/42716982
|
||||
class KeyHelper @Throws(NoSuchPaddingException::class, NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class)
|
||||
constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore: KeyStore) {
|
||||
constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore: KeyStore?) {
|
||||
|
||||
private val aesKeyFromKS: Key?
|
||||
@Throws(NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class, UnrecoverableKeyException::class)
|
||||
get() {
|
||||
return keyStore.getKey(KEY_ALIAS, null) as? SecretKey
|
||||
return keyStore?.getKey(KEY_ALIAS, null) as? SecretKey
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -48,10 +48,10 @@ constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore
|
|||
|
||||
@Throws(NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class)
|
||||
private fun generateEncryptKey(ctx: Context) {
|
||||
keyStore.load(null)
|
||||
keyStore?.load(null)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!keyStore.containsAlias(KEY_ALIAS)) {
|
||||
if (keyStore?.containsAlias(KEY_ALIAS) == false) {
|
||||
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore)
|
||||
keyGenerator.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
|
|
@ -66,7 +66,7 @@ constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore
|
|||
keyGenerator.generateKey()
|
||||
}
|
||||
} else {
|
||||
if (!keyStore.containsAlias(KEY_ALIAS)) {
|
||||
if (keyStore?.containsAlias(KEY_ALIAS) == false) {
|
||||
// Generate a key pair for encryption
|
||||
val start = Calendar.getInstance()
|
||||
val end = Calendar.getInstance()
|
||||
|
|
@ -87,7 +87,7 @@ constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore
|
|||
|
||||
@Throws(Exception::class)
|
||||
private fun rsaEncrypt(secret: ByteArray): ByteArray {
|
||||
val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
|
||||
val privateKeyEntry = keyStore?.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
|
||||
// Encrypt the text
|
||||
val inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL")
|
||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry?.certificate?.publicKey)
|
||||
|
|
@ -102,7 +102,7 @@ constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore
|
|||
|
||||
@Throws(Exception::class)
|
||||
private fun rsaDecrypt(encrypted: ByteArray): ByteArray {
|
||||
val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
|
||||
val privateKeyEntry = keyStore?.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
|
||||
val output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL")
|
||||
output.init(Cipher.DECRYPT_MODE, privateKeyEntry?.privateKey)
|
||||
val cipherInputStream = CipherInputStream(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ open class PurchaseHandler(activity: Activity, val analyticsManager: AnalyticsMa
|
|||
cont.resume(it)
|
||||
}
|
||||
} catch (e: NullPointerException) {
|
||||
cont.resumeWithException(e)
|
||||
cont.resume(null)
|
||||
}
|
||||
if (inventory == null) cont.resume(null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import android.media.AudioManager
|
|||
import android.media.MediaPlayer
|
||||
import java.io.File
|
||||
|
||||
class SoundFile(val theme: String, private val fileName: String) : MediaPlayer.OnCompletionListener {
|
||||
class SoundFile(val theme: String, private val fileName: String) {
|
||||
private var player: MediaPlayer? = null
|
||||
var file: File? = null
|
||||
private var playerPrepared: Boolean = false
|
||||
private var isPlaying: Boolean = false
|
||||
|
||||
val webUrl: String
|
||||
get() = "https://s3.amazonaws.com/habitica-assets/mobileApp/sounds/$theme/$fileName.mp3"
|
||||
|
|
@ -17,37 +17,37 @@ class SoundFile(val theme: String, private val fileName: String) : MediaPlayer.O
|
|||
get() = theme + "_" + fileName + ".mp3"
|
||||
|
||||
fun play() {
|
||||
if (isPlaying || file?.path == null) {
|
||||
if (player?.isPlaying == true || file?.path == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val m = MediaPlayer()
|
||||
if (player?.isPlaying == false) {
|
||||
player?.release()
|
||||
player = null
|
||||
}
|
||||
|
||||
m.setOnCompletionListener { mp ->
|
||||
isPlaying = false
|
||||
player = MediaPlayer()
|
||||
|
||||
player?.setOnCompletionListener { mp ->
|
||||
mp.release()
|
||||
player = null
|
||||
}
|
||||
|
||||
try {
|
||||
m.setDataSource(file?.path)
|
||||
player?.setDataSource(file?.path)
|
||||
val attributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION)
|
||||
.build()
|
||||
m.setAudioAttributes(attributes)
|
||||
m.prepare()
|
||||
player?.setAudioAttributes(attributes)
|
||||
player?.prepare()
|
||||
|
||||
playerPrepared = true
|
||||
m.setVolume(100f, 100f)
|
||||
m.isLooping = false
|
||||
isPlaying = true
|
||||
m.start()
|
||||
player?.setVolume(100f, 100f)
|
||||
player?.isLooping = false
|
||||
player?.start()
|
||||
} catch (e: Exception) {
|
||||
RxErrorHandler.reportError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompletion(mediaPlayer: MediaPlayer) {
|
||||
isPlaying = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ constructor(
|
|||
container.addView(createTextView(context, xp, HabiticaIconsHelper.imageOfExperience()))
|
||||
}
|
||||
if (hp != null && hp != 0.0) {
|
||||
displayType = SnackbarDisplayType.FAILURE
|
||||
if (hp < 0) {
|
||||
displayType = SnackbarDisplayType.FAILURE
|
||||
}
|
||||
container.addView(createTextView(context, hp, HabiticaIconsHelper.imageOfHeartDarkBg()))
|
||||
}
|
||||
if (gold != null && gold != 0.0) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
package com.habitrpg.android.habitica.models
|
||||
|
||||
class IAPGift {
|
||||
var uuid: String? = null
|
||||
}
|
||||
class IAPGift(var uuid: String? = null)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.habitrpg.android.habitica.extensions.addOkButton
|
|||
import com.habitrpg.android.habitica.helpers.AppConfigManager
|
||||
import com.habitrpg.android.habitica.helpers.PurchaseHandler
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.models.members.Member
|
||||
import com.habitrpg.android.habitica.proxy.AnalyticsManager
|
||||
import com.habitrpg.android.habitica.ui.fragments.purchases.GiftBalanceGemsFragment
|
||||
import com.habitrpg.android.habitica.ui.fragments.purchases.GiftPurchaseGemsFragment
|
||||
|
|
@ -48,6 +49,7 @@ class GiftGemsActivity : BaseActivity() {
|
|||
|
||||
private var giftedUsername: String? = null
|
||||
private var giftedUserID: String? = null
|
||||
private var giftedMember: Member? = null
|
||||
|
||||
private var purchaseFragment: GiftPurchaseGemsFragment? = null
|
||||
private var balanceFragment: GiftBalanceGemsFragment? = null
|
||||
|
|
@ -89,6 +91,7 @@ class GiftGemsActivity : BaseActivity() {
|
|||
compositeSubscription.add(
|
||||
socialRepository.getMember(giftedUsername ?: giftedUserID).firstElement().subscribe(
|
||||
{
|
||||
giftedMember = it
|
||||
giftedUserID = it.id
|
||||
giftedUsername = it.username
|
||||
purchaseFragment?.giftedMember = it
|
||||
|
|
@ -139,6 +142,7 @@ class GiftGemsActivity : BaseActivity() {
|
|||
fragment.setPurchaseHandler(purchaseHandler)
|
||||
fragment.setupCheckout()
|
||||
purchaseFragment = fragment
|
||||
purchaseFragment?.giftedMember = giftedMember
|
||||
fragment
|
||||
} else {
|
||||
val fragment = GiftBalanceGemsFragment()
|
||||
|
|
@ -146,6 +150,7 @@ class GiftGemsActivity : BaseActivity() {
|
|||
displayConfirmationDialog()
|
||||
}
|
||||
balanceFragment = fragment
|
||||
balanceFragment?.giftedMember = giftedMember
|
||||
fragment
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,11 +185,13 @@ class GiftSubscriptionActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun purchaseSubscription(sku: Sku) {
|
||||
if (giftedUserID?.isNotEmpty() != true) {
|
||||
return
|
||||
giftedUserID?.let { id ->
|
||||
if (id.isEmpty()) {
|
||||
return
|
||||
}
|
||||
HabiticaPurchaseVerifier.addGift(sku.id.code, id)
|
||||
purchaseHandler?.purchaseNoRenewSubscription(sku)
|
||||
}
|
||||
HabiticaPurchaseVerifier.addGift(sku.id.code, giftedUserID)
|
||||
purchaseHandler?.purchaseNoRenewSubscription(sku)
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
|
|||
field = value
|
||||
for (item in items) {
|
||||
if (item.isHeader) {
|
||||
notifyItemChanged(getVisibleItemPosition(item.identifier))
|
||||
val visiblePosition = getVisibleItemPosition(item.identifier)
|
||||
if (visiblePosition >= 0) {
|
||||
notifyItemChanged(visiblePosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +73,10 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
|
|||
notifyItemInserted(items.size - 1)
|
||||
} else {
|
||||
items[position] = item
|
||||
notifyItemChanged(getVisibleItemPosition(item.identifier))
|
||||
val visiblePosition = getVisibleItemPosition(item.identifier)
|
||||
if (visiblePosition in 0 until itemCount) {
|
||||
notifyItemChanged(visiblePosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ItemsFragment : BaseMainFragment<FragmentViewpagerBinding>() {
|
|||
}
|
||||
fragment.user = this@ItemsFragment.user
|
||||
fragment.itemTypeText =
|
||||
if (position == 4) getString(R.string.special_items)
|
||||
if (position == 4 && isAdded) getString(R.string.special_items)
|
||||
else getPageTitle(position)
|
||||
|
||||
return fragment
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TimePreferenceDialogFragment : PreferenceDialogFragmentCompat() {
|
|||
}
|
||||
|
||||
override fun onCreateDialogView(context: Context?): View {
|
||||
picker = TimePicker(getContext())
|
||||
picker = TimePicker(context)
|
||||
return picker
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ class GiftPurchaseGemsFragment : BaseFragment<FragmentGiftGemPurchaseBinding>()
|
|||
}
|
||||
|
||||
private fun purchaseGems(identifier: String) {
|
||||
HabiticaPurchaseVerifier.addGift(identifier, giftedMember?.id)
|
||||
purchaseHandler?.purchaseGems(identifier)
|
||||
giftedMember?.id?.let {
|
||||
HabiticaPurchaseVerifier.addGift(identifier, it)
|
||||
purchaseHandler?.purchaseGems(identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.habitrpg.android.habitica.helpers.MainNavigationController
|
|||
import com.habitrpg.android.habitica.modules.AppModule
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
|
@ -57,7 +58,9 @@ class BugFixFragment : BaseMainFragment<FragmentSupportBugFixBinding>() {
|
|||
compositeSubscription.add(
|
||||
Completable.fromAction {
|
||||
deviceInfo = context?.let { DeviceName.getDeviceInfo(it) }
|
||||
}.subscribe()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
)
|
||||
|
||||
binding?.reportBugButton?.setOnClickListener {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.habitrpg.android.habitica.helpers.MainNavigationController
|
|||
import com.habitrpg.android.habitica.modules.AppModule
|
||||
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
|
@ -63,7 +64,9 @@ class SupportMainFragment : BaseMainFragment<FragmentSupportMainBinding>() {
|
|||
compositeSubscription.add(
|
||||
Completable.fromAction {
|
||||
deviceInfo = context?.let { DeviceName.getDeviceInfo(it) }
|
||||
}.subscribe()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
)
|
||||
|
||||
binding?.resetTutorialButton?.setOnClickListener {
|
||||
|
|
|
|||
Loading…
Reference in a new issue