Updated on 2026-08-14

This commit is contained in:
Tangem 2018-09-20 10:58:42 +03:00
parent 31e7fa6885
commit 3416aa25e5
7 changed files with 208 additions and 177 deletions

View file

@ -11,7 +11,7 @@ public class Server {
public static class Method {
public static final String VERIFY = URL_TANGEM + "verify";
public static final String VERIFY_AND_GET_ARTWORK = URL_TANGEM + "card/verify-and-get-artwork";
public static final String VERIFY_AND_GET_INFO = URL_TANGEM + "card/verify-and-get-info";
public static final String ARTWORK = URL_TANGEM + "card/artwork";
}
}

View file

@ -5,10 +5,7 @@ import android.support.annotation.NonNull;
import android.util.Log;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import com.tangem.data.network.model.CardVerify;
import com.tangem.data.network.model.CardVerifyAndGetArtwork;
import com.tangem.data.network.model.CardVerifyBody;
import com.tangem.data.network.model.CardVerifyResponse;
import com.tangem.data.network.model.CardVerifyAndGetInfo;
import com.tangem.data.network.model.InfuraBody;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.data.network.model.RateInfoResponse;
@ -242,17 +239,17 @@ public class ServerApiHelper {
* HTTP
* Card verify
*/
private CardVerifyAndGetArtworkListener cardVerifyAndGetArtworkListener;
private CardVerifyAndGetInfoListener cardVerifyAndGetInfoListener;
public interface CardVerifyAndGetArtworkListener {
void onCardVerifyAndGetArtwork(CardVerifyAndGetArtwork.Response cardVerifyAndGetArtworkResponse);
public interface CardVerifyAndGetInfoListener {
void onCardVerifyAndGetInfo(CardVerifyAndGetInfo.Response cardVerifyAndGetArtworkResponse);
}
public void setCardVerifyAndGetArtworkListener(CardVerifyAndGetArtworkListener listener) {
cardVerifyAndGetArtworkListener = listener;
public void setCardVerifyAndGetInfoListener(CardVerifyAndGetInfoListener listener) {
cardVerifyAndGetInfoListener = listener;
}
public void cardVerifyAndGetArtwork(TangemCard card) {
public void cardVerifyAndGetInfo(TangemCard card) {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
@ -269,26 +266,26 @@ public class ServerApiHelper {
TangemApi tangemApi = retrofit.create(TangemApi.class);
List<CardVerifyAndGetArtwork.Request.Item> requests = new ArrayList<>();
requests.add(new CardVerifyAndGetArtwork.Request.Item(Util.bytesToHex(card.getCID()), Util.bytesToHex(card.getCardPublicKey())));
List<CardVerifyAndGetInfo.Request.Item> requests = new ArrayList<>();
requests.add(new CardVerifyAndGetInfo.Request.Item(Util.bytesToHex(card.getCID()), Util.bytesToHex(card.getCardPublicKey())));
CardVerifyAndGetArtwork.Request requestBody = new CardVerifyAndGetArtwork.Request(requests);
CardVerifyAndGetInfo.Request requestBody = new CardVerifyAndGetInfo.Request(requests);
Call<CardVerifyAndGetArtwork.Response> call = tangemApi.getCardVerifyAndGetArtwork(requestBody);
call.enqueue(new Callback<CardVerifyAndGetArtwork.Response>() {
Call<CardVerifyAndGetInfo.Response> call = tangemApi.getCardVerifyAndGetInfo(requestBody);
call.enqueue(new Callback<CardVerifyAndGetInfo.Response>() {
@Override
public void onResponse(@NonNull Call<CardVerifyAndGetArtwork.Response> call, @NonNull Response<CardVerifyAndGetArtwork.Response> response) {
public void onResponse(@NonNull Call<CardVerifyAndGetInfo.Response> call, @NonNull Response<CardVerifyAndGetInfo.Response> response) {
if (response.code() == 200) {
CardVerifyAndGetArtwork.Response cardVerifyAndGetArtworkResponse = response.body();
cardVerifyAndGetArtworkListener.onCardVerifyAndGetArtwork(cardVerifyAndGetArtworkResponse);
Log.i(TAG, "cardVerifyAndGetArtwork onResponse " + response.code());
CardVerifyAndGetInfo.Response cardVerifyAndGetArtworkResponse = response.body();
cardVerifyAndGetInfoListener.onCardVerifyAndGetInfo(cardVerifyAndGetArtworkResponse);
Log.i(TAG, "cardVerifyAndGeInfo onResponse " + response.code());
} else
Log.e(TAG, "cardVerifyAndGetArtwork onResponse " + response.code());
Log.e(TAG, "cardVerifyAndGetInfo onResponse " + response.code());
}
@Override
public void onFailure(@NonNull Call<CardVerifyAndGetArtwork.Response> call, @NonNull Throwable t) {
Log.e(TAG, "cardVerifyAndGetArtwork onFailure " + t.getMessage());
public void onFailure(@NonNull Call<CardVerifyAndGetInfo.Response> call, @NonNull Throwable t) {
Log.e(TAG, "cardVerifyAndGetInfo onFailure " + t.getMessage());
}
});
}

View file

@ -1,6 +1,6 @@
package com.tangem.data.network;
import com.tangem.data.network.model.CardVerifyAndGetArtwork;
import com.tangem.data.network.model.CardVerifyAndGetInfo;
import com.tangem.data.network.model.CardVerifyBody;
import com.tangem.data.network.model.CardVerifyResponse;
@ -18,8 +18,8 @@ public interface TangemApi {
Call<CardVerifyResponse> getCardVerify(@Body CardVerifyBody body);
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_ARTWORK)
Call<CardVerifyAndGetArtwork.Response> getCardVerifyAndGetArtwork(@Body CardVerifyAndGetArtwork.Request requestBody);
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
Call<CardVerifyAndGetInfo.Response> getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody);
@GET(Server.ApiTangem.Method.ARTWORK)
Call<ResponseBody> getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey);

View file

@ -1,56 +0,0 @@
package com.tangem.data.network.model
import android.os.Build
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
class CardVerifyAndGetArtwork {
data class Request(
var requests: List<Item>? = null
) {
data class Item(
var CID: String = "",
var publicKey: String = ""
)
}
data class Response(
var results: List<Item>? = null
) {
data class Item(
var error: String? = null,
var CID: String = "",
var passed: Boolean = false,
var batch: String = "",
var artworkId: String = "",
var artworkHash: String = "",
var updateDate: String = ""
) {
fun getUpdateDate(): Instant? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return try {
LocalDateTime.parse(updateDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZoneOffset.UTC)
} catch (e: Exception) {
e.printStackTrace()
null
}
} else {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).parse(updateDate).toInstant()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.data.network.model
import android.os.Build
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
class CardVerifyAndGetInfo {
data class Request(
var requests: List<Item>? = null
) {
data class Item(
var CID: String = "",
var publicKey: String = ""
)
}
data class Response(
var results: List<Item>? = null
) {
data class Item(
var error: String? = null,
var CID: String = "",
var passed: Boolean = false,
var batch: String = "",
var artwork: ArtworkInfo? = null,
var substitution: SubstitutionInfo? = null
) {
data class ArtworkInfo(
var id: String = "",
var hash: String = "",
var date: String = ""
) {
fun getUpdateDate(): Instant? {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return LocalDateTime.parse(date, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZoneOffset.UTC)
} else {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).parse(date).toInstant()
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
data class SubstitutionInfo(
var data: String? = null,
var signature: String? = null
)
}
}
}

View file

@ -8,23 +8,31 @@ import kotlin.collections.HashMap
import android.graphics.BitmapFactory
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.tangem.data.network.model.CardVerifyAndGetInfo
import com.tangem.domain.cardReader.CardCrypto
import com.tangem.util.Util
import com.tangem.wallet.R
import java.io.InputStream
import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.time.Instant
data class ArtworksStorage(
data class LocalStorage(
val context: Context
) {
private val cache: FileCache = FileCache(context)
private lateinit var artworks: HashMap<String, ArtworkInfo>
private lateinit var batches: HashMap<String, BatchInfo>
private var artworksFile: File = File(context.filesDir, "artworks.json")
private var batchesFile: File = File(context.filesDir, "batches.json")
private val artworksFile: File = File(context.filesDir, "artworks.json")
private val batchesFile: File = File(context.filesDir, "batches.json")
private var cacheDir: File? = null
init {
cacheDir = File(context.filesDir, "artworks")
if (!cacheDir!!.exists())
cacheDir!!.mkdirs()
if (artworksFile.exists()) {
artworksFile.bufferedReader().use { artworks = Gson().fromJson(it, object : TypeToken<HashMap<String, ArtworkInfo>>() {}.type) }
} else {
@ -62,24 +70,44 @@ data class ArtworksStorage(
}
}
fun checkNeedUpdateArtwork(artworkId: String, artworkHash: String, updateDate: Instant?): Boolean {
val artwork = artworks[artworkId] ?: return true
if (artwork.hash == artworkHash) return false
if (updateDate == null) return false
return artwork.updateDate == null || artwork.updateDate < updateDate
private fun getArtworkFile(artworkId: String): File {
return File(cacheDir, "$artworkId.png")
}
fun checkBatchArtworkChanged(batch: String, artworkId: String): Boolean {
val batchInfo = batches[batch]
if (batchInfo == null) {
putBatchToCatalog(batch, artworkId)
return true
private fun loadArtworkBitmapFromFile(artworkId: String): Bitmap? {
return try {
BitmapFactory.decodeFile(getArtworkFile(artworkId).absolutePath)
} catch (E: Exception) {
E.printStackTrace()
null
}
if (batchInfo.artworkId != artworkId) {
putBatchToCatalog(batch, artworkId)
}
private fun saveArtworkBitmapToFile(artworkId: String, data: ByteArray) {
val file = getArtworkFile(artworkId)
file.writeBytes(data)
}
fun checkNeedUpdateArtwork(artwork: CardVerifyAndGetInfo.Response.Item.ArtworkInfo?): Boolean {
if (artwork == null) return false
val localArtwork = artworks[artwork.id] ?: return true
if (localArtwork.hash == artwork.hash) return false
return localArtwork.updateDate == null || localArtwork.updateDate < artwork.getUpdateDate()
}
fun checkBatchInfoChanged(result: CardVerifyAndGetInfo.Response.Item): Boolean {
var sData: String? = result.substitution?.data
var sSignature: String? = result.substitution?.signature
if (!BatchInfo.CardDataSubstitution.verifySignature(result.batch, sData, sSignature)) {
sData = null
sSignature = null
}
val batchInfo = batches[result.batch]
if (batchInfo == null || batchInfo.artworkId != result.artwork?.id || batchInfo.dataSubstitution != sData) {
putBatchToCatalog(result.batch, result.artwork?.id, sData, sSignature)
return true
}
return false
@ -87,12 +115,12 @@ data class ArtworksStorage(
fun updateArtwork(artworkId: String, inputStream: InputStream, updateDate: Instant) {
val data = inputStream.readBytes()
cache.saveBitmap(artworkId, data)
saveArtworkBitmapToFile(artworkId, data)
putArtworkToCatalog(artworkId, false, data, updateDate, true)
}
private fun putBatchToCatalog(batch: String, artworkId: String, forceSave: Boolean = true) {
batches[batch] = BatchInfo(artworkId)
private fun putBatchToCatalog(batch: String, artworkId: String?, substitution: String?, substitutionSignature: String?, forceSave: Boolean = true) {
batches[batch] = BatchInfo(artworkId, substitution, substitutionSignature)
if (forceSave) {
val sBatches = Gson().toJson(batches)
batchesFile.bufferedWriter().use { it.write(sBatches) }
@ -122,15 +150,16 @@ data class ArtworksStorage(
if (resID == 0) return null
BitmapFactory.decodeResource(context.resources, resID)
} else {
cache.getBitmap(artworkId)
loadArtworkBitmapFromFile(artworkId)
}
}
private fun getDefaultArtworkBitmap(): Bitmap {
val defaultArtworkId = context.resources.getResourceEntryName(R.drawable.card_default)
val info = artworks[defaultArtworkId]
var bitmap: Bitmap? = null
if (info != null && !info.isResource) {
bitmap = cache.getBitmap(defaultArtworkId)
bitmap = loadArtworkBitmapFromFile(defaultArtworkId)
}
if (bitmap == null) return BitmapFactory.decodeResource(context.resources, R.drawable.card_default)
return bitmap
@ -138,57 +167,63 @@ data class ArtworksStorage(
fun getCardArtworkBitmap(card: TangemCard): Bitmap {
// special cases (first series of cards, hardcode CID->artwork), on new series batch<->artwork
var artworkId =
val artworkId =
when (card.cidDescription) {
in "AA01 0000 0000 0000".."AA01 0000 0000 4999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
in "AA01 0000 0000 5000".."AA01 0000 0000 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_005)
context.resources.getResourceEntryName(R.drawable.card_btc_005)
}
in "AE01 0000 0000 0000".."AE01 0000 0000 4999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
in "AE01 0000 0000 5000".."AE01 0000 0000 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_005)
context.resources.getResourceEntryName(R.drawable.card_btc_005)
}
in "CB01 0000 0000 0000".."CB01 0000 0000 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
in "CB01 0000 0001 0000".."CB01 0000 0001 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_005)
context.resources.getResourceEntryName(R.drawable.card_btc_005)
}
in "CB01 0000 0002 0000".."CB01 0000 0003 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
in "CB01 0000 0004 0000".."CB01 0000 0005 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_005)
context.resources.getResourceEntryName(R.drawable.card_btc_005)
}
in "CB02 0000 0000 0000".."CB02 0000 0002 4999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
in "CB02 0000 0002 5000".."CB02 0000 0004 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_005)
context.resources.getResourceEntryName(R.drawable.card_btc_005)
}
in "CB05 0000 1000 0000".."CB05 0000 1000 9999" -> {
context.resources.getResourceName(R.drawable.card_btc_001)
context.resources.getResourceEntryName(R.drawable.card_btc_001)
}
else -> {
val batchInfo = batches[card.batch] ?: return getDefaultArtworkBitmap()
batchInfo.artworkId
}
}
if( artworkId.contains('/'))
artworkId=artworkId.split('/').last()
} ?: return getDefaultArtworkBitmap()
// if (artworkId.contains('/'))
// artworkId = artworkId.split('/').last()
return getArtworkBitmap(artworkId) ?: return getDefaultArtworkBitmap()
}
fun applySubstitution(card: TangemCard) {
val batchInfo = batches[card.batch] ?: return
val substitution = batchInfo.getDataSubstitution(card.batch) ?: return
substitution.applyToCard(card)
}
data class ArtworkInfo(
val isResource: Boolean,
val hash: String,
@ -196,51 +231,40 @@ data class ArtworksStorage(
)
data class BatchInfo(
val artworkId: String
)
val artworkId: String?,
val dataSubstitution: String?,
val dataSubstitutionSignature: String?
) {
inner class FileCache(context: Context) {
private var cacheDir: File? = null
init {
cacheDir = File(context.filesDir, "artworks")
//Find the dir to save cached images
// if (android.os.Environment.getExternalStorageState() == android.os.Environment.MEDIA_MOUNTED)
// cacheDir = File(android.os.Environment.getExternalStorageDirectory(), "artworks_cache")
// else
// cacheDir = context.getCacheDir()
if (!cacheDir!!.exists())
cacheDir!!.mkdirs()
}
private fun getFile(artworkId: String): File {
return File(cacheDir, "$artworkId.png")
}
fun getBitmap(artworkId: String): Bitmap? {
return try {
BitmapFactory.decodeFile(getFile(artworkId).absolutePath)
} catch (E: Exception) {
E.printStackTrace()
fun getDataSubstitution(batch: String): CardDataSubstitution? {
return if (CardDataSubstitution.verifySignature(batch, dataSubstitution, dataSubstitutionSignature)) {
Gson().fromJson(dataSubstitution, CardDataSubstitution::class.java)
} else {
null
}
}
fun saveBitmap(artworkId: String, data: ByteArray) {
val file = getFile(artworkId)
file.writeBytes(data)
class CardDataSubstitution(
val tokenSymbol: String?,
val tokenDecimal: Int?,
val contractAddress: String?
) {
fun applyToCard(card: TangemCard) {
if (card.tokenSymbol.isNullOrBlank()) card.tokenSymbol = tokenSymbol
if (tokenDecimal != null) card.tokensDecimal = tokenDecimal
if (card.contractAddress.isNullOrBlank()) card.contractAddress = contractAddress
}
companion object {
private val substitutionPublicKey: ByteArray = byteArrayOf(0x00, 0x00)
fun verifySignature(batch: String, substitutionData: String?, substitutionSignature: String?): Boolean {
if (substitutionData == null && substitutionSignature == null) return true
if (substitutionData == null || substitutionSignature == null) return false
val dataToSign = batch.toByteArray(StandardCharsets.UTF_8) + substitutionData.toByteArray(StandardCharsets.UTF_8)
return CardCrypto.VerifySignature(substitutionPublicKey, dataToSign, Util.hexToBytes(substitutionSignature))
}
}
}
fun clear() {
val files = cacheDir!!.listFiles() ?: return
for (f in files)
f.delete()
}
}
companion object {
const val defaultArtworkId = ""
}
}

View file

@ -76,7 +76,7 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
private var timerRepeatRefresh: Timer? = null
private lateinit var artworksStorage: ArtworksStorage
private lateinit var localStorage: LocalStorage
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -92,7 +92,9 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
lastTag = activity!!.intent.getParcelableExtra(MainActivity.EXTRA_LAST_DISCOVERED_TAG)
artworksStorage = ArtworksStorage(context!!)
localStorage = LocalStorage(context!!)
localStorage.applySubstitution(card!!)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -108,7 +110,7 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
tvBalance.setSingleLine(false)
//ivTangemCard.setImageResource(card!!.cardImageResource)
ivTangemCard.setImageBitmap(artworksStorage.getCardArtworkBitmap(card!!))
ivTangemCard.setImageBitmap(localStorage.getCardArtworkBitmap(card!!))
val engine = CoinEngineFactory.create(card!!.blockchain)
val visibleFlag = engine?.inOutPutVisible() ?: true
@ -224,34 +226,34 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
//
//// Log.i(TAG, "setCardVerify " + it.results!![0].passed)
// }
serverApiHelper!!.setCardVerifyAndGetArtworkListener {
serverApiHelper!!.setCardVerifyAndGetInfoListener {
srlLoadedWallet!!.isRefreshing = false
val result = it.results!![0]
if (result.error != null) {
Log.e(TAG, "Can't verify card: ${result.error}")
card!!.isOnlineVerified = false
return@setCardVerifyAndGetArtworkListener
return@setCardVerifyAndGetInfoListener
}
card!!.isOnlineVerified = result.passed
if (!result.passed) return@setCardVerifyAndGetArtworkListener
if (!result.passed) return@setCardVerifyAndGetInfoListener
if (artworksStorage.checkBatchArtworkChanged(result.batch, result.artworkId)) {
Log.w(TAG, "Batch ${result.batch} artwork changed to '${result.artworkId}'")
ivTangemCard.setImageBitmap(artworksStorage.getCardArtworkBitmap(card!!))
if (localStorage.checkBatchInfoChanged(result)) {
Log.w(TAG, "Batch ${result.batch} info changed to '$result'")
ivTangemCard.setImageBitmap(localStorage.getCardArtworkBitmap(card!!))
updateViews()
}
if (artworksStorage.checkNeedUpdateArtwork(result.artworkId, result.artworkHash, result.getUpdateDate())) {
Log.w(TAG, "Artwork '${result.artworkId}' updated, need download")
serverApiHelper!!.requestArtwork(result.artworkId, result.getUpdateDate(), card!!)
if (result.artwork!=null && localStorage.checkNeedUpdateArtwork(result.artwork)) {
Log.w(TAG, "Artwork '${result.artwork!!.id}' updated, need download")
serverApiHelper!!.requestArtwork(result.artwork!!.id, result.artwork!!.getUpdateDate(), card!!)
}
// Log.i(TAG, "setCardVerify " + it.results!![0].passed)
}
serverApiHelper!!.setArtworkListener { artworkId, inputStream, updateDate ->
Log.w(TAG, "Artwork '$artworkId' downloaded")
artworksStorage.updateArtwork(artworkId, inputStream, updateDate)
ivTangemCard.setImageBitmap(artworksStorage.getCardArtworkBitmap(card!!))
localStorage.updateArtwork(artworkId, inputStream, updateDate)
ivTangemCard.setImageBitmap(localStorage.getCardArtworkBitmap(card!!))
}
// request rate info listener
@ -380,7 +382,7 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
private fun requestCardVerify() {
if ((card!!.isOnlineVerified == null || !card!!.isOnlineVerified))
serverApiHelper!!.cardVerifyAndGetArtwork(card) //serverApiHelper!!.cardVerify(card)
serverApiHelper!!.cardVerifyAndGetInfo(card) //serverApiHelper!!.cardVerify(card)
}
private fun requestRateInfo(cryptoId: String) {