将jcfw部分代码改为kotlin

This commit is contained in:
CounterFire2023 2023-11-09 16:33:34 +08:00
parent b09c735324
commit ed0f5f42da
30 changed files with 1107 additions and 1148 deletions

2
.idea/kotlinc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.0-release" />
<option name="version" value="1.7.20" />
</component>
</project>

View File

@ -17,7 +17,8 @@
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#ifndef COM_JC_JCFW_CLASS_NAME
#define COM_JC_JCFW_CLASS_NAME com_jc_jcfw_JcSDK
// Java_com_jc_jcfw_JcSDK_00024Companion_runJS
#define COM_JC_JCFW_CLASS_NAME com_jc_jcfw_JcSDK_00024Companion
#endif
#define JNI_JCFW(FUNC) JNI_METHOD1(COM_JC_JCFW_CLASS_NAME, FUNC)
@ -259,7 +260,7 @@ extern "C"
return result == 0 ? 1 : 0;
}
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
JNIEXPORT jint JNICALL JNI_JCFW(runJS)(JNIEnv *env, jclass clazz, jstring j_fun_id, jstring j_method_name, jobjectArray j_params)
JNIEXPORT jint JNICALL JNI_JCFW(runJS)(JNIEnv *env, jobject clazz, jstring j_fun_id, jstring j_method_name, jobjectArray j_params)
{
if (!_isStarted) {
return 0;
@ -285,7 +286,7 @@ extern "C"
return result == 0 ? 1 : 0;
}
JNIEXPORT jstring JNICALL JNI_JCFW(decryptPass)(JNIEnv *env, jclass clazz, jstring jaccount, jstring jpass)
JNIEXPORT jstring JNICALL JNI_JCFW(decryptPass)(JNIEnv *env, jobject clazz, jstring jaccount, jstring jpass)
{
std::string pass_encrypted = JniHelper::jstring2string(jpass);
std::string account = JniHelper::jstring2string(jaccount);
@ -293,7 +294,7 @@ extern "C"
std::string passDecrypt = decrypt_aes(pass_encrypted, keyStr);
return env->NewStringUTF(passDecrypt.c_str());
}
JNIEXPORT void JNICALL JNI_JCFW(tick)(JNIEnv *env, jclass clazz, jfloat dt)
JNIEXPORT void JNICALL JNI_JCFW(tick)(JNIEnv *env, jobject clazz, jfloat dt)
{
tick2(dt);
}

View File

@ -7,6 +7,7 @@ plugins {
id 'com.google.gms.google-services'
// Add the Crashlytics Gradle plugin
id 'com.google.firebase.crashlytics'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger()

View File

@ -100,14 +100,14 @@ class MainActivity : UnityPlayerActivity(), Cocos2dxHelperListener {
initFacebookSDK()
// end of facebook login
tiktokOpenApi = TikTokOpenApiFactory.create(this)
val payClient = PayClient.getInstance()
payClient.init(this)
val payClient = PayClient.instance
payClient?.init(this)
mWalletUtil = WalletUtil(this)
}
override fun onResume() {
super.onResume()
val paramArr = arrayOf<String>()
val paramArr = arrayOfNulls<String>(0)
JcSDK.callNativeJS("", "onGameResume", paramArr)
}
@ -483,19 +483,19 @@ class MainActivity : UnityPlayerActivity(), Cocos2dxHelperListener {
Log.d(TAG, "need refresh accessToken")
mAppAuthSvr!!.refreshToken(funID) {
mWalletUtil!!.downloadCfgWithApi(
state.accessToken
state.accessToken!!
) { mWalletUtil!!.passLocal }
}
} else {
Log.d(TAG, "access token no need refresh")
mWalletUtil!!.downloadCfgWithApi(state.accessToken) { mWalletUtil!!.passLocal }
mWalletUtil!!.downloadCfgWithApi(state.accessToken!!) { mWalletUtil!!.passLocal }
}
} else {
Log.w(TAG, "not login")
mExecutor!!.submit {
mAppAuthSvr!!.doAuth(funID) {
mWalletUtil!!.downloadCfgWithApi(
state.accessToken
state.accessToken!!
) { mWalletUtil!!.passLocal }
}
}

View File

@ -109,15 +109,15 @@ class WalletUtil(private val mContext: Context) {
fileId = DriveUtils.uploadAppFile(service, file, "application/json")
}
Log.i(TAG, "File ID: $fileId")
func.accept(NativeResult(funID, null, fileId))
func.accept(NativeResult(funID!!, null, fileId))
successCB(fileId)
} catch (e: GoogleJsonResponseException) {
Log.i(TAG, "Unable to create file: " + e.details)
func.accept(NativeResult(funID, e.message, null))
func.accept(NativeResult(funID!!, e.message, null))
errorCB("Unable to create file: " + e.message)
} catch (e: IOException) {
Log.i(TAG, e.message!!)
func.accept(NativeResult(funID, e.message, null))
func.accept(NativeResult(funID!!, e.message, null))
errorCB("Unable to create file: " + e.message)
}
}
@ -155,7 +155,7 @@ class WalletUtil(private val mContext: Context) {
}
}
fun downloadCfgWithApi(accessToken: String?, func: Consumer<File?>) {
fun downloadCfgWithApi(accessToken: String, func: Consumer<File?>) {
val api = DriverApiUtils(accessToken)
val fileName = generateFileName(account)
val fileLocal = getWalletCfgFile(account)
@ -186,7 +186,7 @@ class WalletUtil(private val mContext: Context) {
}
fun uploadCfgWithApi(accessToken: String?, func: Consumer<NativeResult?>) {
val api = DriverApiUtils(accessToken)
val api = DriverApiUtils(accessToken!!)
val fileLocal = getWalletCfgFile(account)
try {
val fileName = generateFileName(account)
@ -197,13 +197,13 @@ class WalletUtil(private val mContext: Context) {
fileId = repData.getString("id")
}
Log.i(TAG, "success upload file with api, ID: $fileId")
func.accept(NativeResult(funID, null, fileId))
func.accept(NativeResult(funID!!, null, fileId))
successCB(fileId)
} catch (e: IOException) {
func.accept(NativeResult(funID, e.message, null))
func.accept(NativeResult(funID!!, e.message, null))
errorCB("error upload file with api: " + e.message)
} catch (e: JSONException) {
func.accept(NativeResult(funID, e.message, null))
func.accept(NativeResult(funID!!, e.message, null))
errorCB("error upload file with api: " + e.message)
}
}

View File

@ -10,6 +10,6 @@ class WalletInterface
// sign
@JavascriptInterface
fun pageCall(dataStr: String?) {
EventBus.getDefault().post(WebPageEvent(dataStr))
EventBus.getDefault().post(WebPageEvent(dataStr!!))
}
}

View File

@ -1,232 +0,0 @@
package com.jc.jcfw;
import static com.ctf.games.release.Constants.FUNID_PREFIX;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.ctf.games.release.MainActivity;
import com.ctf.games.release.MainApplication;
import com.ctf.games.release.ui.UIManager;
import com.ctf.games.release.webpage.events.CallJSEvent;
import com.ctf.games.release.webpage.events.ProxyCBEvent;
import com.google.common.base.Strings;
import com.jc.jcfw.google.PayClient;
import com.jc.jcfw.util.ThreadUtils;
import com.jc.jcfw.util.UIUtils;
import org.cocos2dx.lib.CocosJSHelper;
import org.greenrobot.eventbus.EventBus;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class JcSDK {
private static final String TAG = JcSDK.class.getSimpleName();
private static UnityCallback commonCB;
@SuppressLint("StaticFieldLeak")
private static PayClient payClient;
private static native int runJS(final String funId, final String methodName, final String[] params);
public static native String decryptPass(final String account, final String pass);
public static native void tick(float dt);
public static void initCommonCB(UnityCallback callBack) {
Log.i(TAG, "call init common callback from unity");
commonCB = callBack;
}
/**
* @Deprecated
* 不使用该方法, 直接由unity调用cpp方法
* @param password
*/
public static void initWallet(String password) {
Log.i(TAG, "call init wallet from unity with password: " + password);
CocosJSHelper.initWallet(password);
commonCB.nativeCallback("", "wallet init success", 0);
}
/**
* 回调至c#
*/
public static void csCallback(String funId, String msg) {
if (!Objects.equals(funId, "") && funId.indexOf("js_") == 0) {
commonCB.nativeCallback(funId, msg, 1);
} else {
commonCB.nativeCallback(funId, msg, 0);
}
}
/**
* check if metamask installed and jump to metamask
*
* @param url
* sample:
* "https://metamask.app.link/wc?uri="+ExampleApplication.config.toWCUri();
*/
public static void toWallet(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Log.i(TAG, url);
try {
intent.setData(Uri.parse(url));
MainActivity.app.startActivity(intent);
} catch (ActivityNotFoundException e) {
Intent i = new Intent(Intent.ACTION_VIEW);
String downloadUrl = "https://metamask.io/download/";
if (url.startsWith("imtokenv2")) {
downloadUrl = "https://token.im/download";
} else if (url.startsWith("okx://")) {
downloadUrl = "https://www.okx.com/download";
}
i.setData(Uri.parse(downloadUrl));
MainActivity.app.startActivity(i);
}
}
public static void showQRCode(String funid, String content) {
UIUtils.showQRCode(MainActivity.app, content, "");
}
public static void showWebPage(String funid, String url) {
MainActivity.app.showPage(funid, url);
}
public static void scanQRCode(String funid, String title) {
// MainActivity.app.showQRScan(funid, title);
UIManager.getSingleton().showQRScan(funid, title);
}
public static void signWithTiktok(String funid) {
MainActivity.app.signWithTiktok(funid);
}
public static void signWithFacebook(String funid) {
MainActivity.app.signWithFacebook(funid);
}
public static void shareWithFacebook(String content) {
MainActivity.app.shareWithFacebook(content);
}
public static void signWithTwitter(String funid) {
MainActivity.app.signWithTwitter(funid);
}
public static void signWithGoogle(String funid) {
MainActivity.app.signWithGoogle(funid);
}
public static void signWithApple(String funid) {
MainActivity.app.signWithApple(funid);
}
public static void signOutGoogle(String funid) {
MainActivity.app.signOutGoogle(funid);
}
public static void logEvent(String content) {
MainActivity.app.logEvent(content);
}
public static void queryProducts(String funid, String skuListStr) {
Log.i(TAG, "queryProducts with: " + skuListStr);
if (payClient == null) {
payClient = PayClient.getInstance();
}
List<String> skuList = new ArrayList<>();
if (skuListStr.contains(",")) {
String[] skuArr = skuListStr.split(",");
skuList.addAll(Arrays.asList(skuArr));
} else {
skuList.add(skuListStr);
}
payClient.queryProductList(funid, skuList);
}
public static void buyProduct(String funid, String productId, String orderId) {
Log.i(TAG, "buyProduct with: " + productId);
if (payClient == null) {
payClient = PayClient.getInstance();
}
payClient.buyOne(funid, productId, orderId);
}
public static void queryPurchase(String funid) {
Log.i(TAG, "queryPurchase");
if (payClient == null) {
payClient = PayClient.getInstance();
}
payClient.queryPurchase(funid);
}
public static void passStorageState(String funid, String account) {
Log.i(TAG, "passStorageState with: " + account);
MainActivity.app.passStorageState(funid, account);
}
public static void storagePass(String funid, String account, String password) {
MainActivity.app.storagePass(funid, account, password);
}
public static void authGetStoragePass(String funid, String account) {
MainActivity.app.authGetStoragePass(funid, account);
}
public static void storageGameData(String data) {
MainApplication.application.setGameData(data);
}
public static void getClientId(String funid) {
Log.i(TAG, "getClientId ");
MainActivity.app.getClientId(funid);
}
public static void onProxyCB(String funId, String data) {
EventBus.getDefault().post(new ProxyCBEvent(funId, data));
}
public static void nativeCb(String funId, String error, String dataStr) {
JSONObject result = new JSONObject();
try {
if (Strings.isNullOrEmpty(error)) {
result.put("errcode", 0);
result.put("data", dataStr);
} else {
result.put("errcode", 1);
result.put("errmessage", error);
}
} catch (JSONException e) {
Log.e(TAG, "JSONException: " + e.getMessage());
}
if (funId == null || funId.isEmpty()) {
funId = MainActivity.app.getFunId();
}
Log.i(TAG, String.format("%s native cb, error: %s, data: %s", funId, error, dataStr));
if (funId.startsWith(FUNID_PREFIX)) {
EventBus.getDefault().post(new CallJSEvent(funId, result.toString()));
} else {
String finalFunId = funId;
ThreadUtils.runInMain(() -> JcSDK.runJS(finalFunId, "jniCallback", new String[]{result.toString()}));
}
}
public static void callNativeJS(String funId, String methodName, String[] params) {
ThreadUtils.runInMain(() -> JcSDK.runJS(funId, methodName, params));
}
public static void nativeCb(NativeResult result) {
nativeCb(result.getFunid(), result.getError(), result.getDataStr());
}
}

View File

@ -0,0 +1,237 @@
package com.jc.jcfw
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.ctf.games.release.MainActivity
import com.ctf.games.release.MainApplication
import com.ctf.games.release.ui.UIManager.Companion.singleton
import com.ctf.games.release.webpage.events.CallJSEvent
import com.ctf.games.release.webpage.events.ProxyCBEvent
import com.google.common.base.Strings
import com.jc.jcfw.google.PayClient
import com.jc.jcfw.util.ThreadUtils
import com.jc.jcfw.util.UIUtils
import org.cocos2dx.lib.CocosJSHelper
import org.greenrobot.eventbus.EventBus
import org.json.JSONException
import org.json.JSONObject
import java.util.Arrays
const val FUNID_PREFIX = "webpage_"
class JcSDK {
companion object {
external fun runJS(funId: String?, methodName: String, params: Array<String?>): Int
external fun decryptPass(account: String?, pass: String?): String?
external fun tick(dt: Float)
private val TAG = JcSDK::class.java.simpleName
private var commonCB: UnityCallback? = null
@SuppressLint("StaticFieldLeak")
private var payClient: PayClient? = null
@JvmStatic
fun initCommonCB(callBack: UnityCallback?) {
Log.i(TAG, "call init common callback from unity")
commonCB = callBack
}
/**
* @Deprecated
* 不使用该方法, 直接由unity调用cpp方法
* @param password
*/
@JvmStatic
fun initWallet(password: String) {
Log.i(TAG, "call init wallet from unity with password: $password")
CocosJSHelper.initWallet(password)
commonCB!!.nativeCallback("", "wallet init success", 0)
}
/**
* 回调至c#
*/
@JvmStatic
fun csCallback(funId: String, msg: String?) {
if (funId != "" && funId.indexOf("js_") == 0) {
commonCB!!.nativeCallback(funId, msg, 1)
} else {
commonCB!!.nativeCallback(funId, msg, 0)
}
}
/**
* check if metamask installed and jump to metamask
*
* @param url
* sample:
* "https://metamask.app.link/wc?uri="+ExampleApplication.config.toWCUri();
*/
@JvmStatic
fun toWallet(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
Log.i(TAG, url)
try {
intent.data = Uri.parse(url)
MainActivity.app!!.startActivity(intent)
} catch (e: ActivityNotFoundException) {
val i = Intent(Intent.ACTION_VIEW)
var downloadUrl = "https://metamask.io/download/"
if (url.startsWith("imtokenv2")) {
downloadUrl = "https://token.im/download"
} else if (url.startsWith("okx://")) {
downloadUrl = "https://www.okx.com/download"
}
i.data = Uri.parse(downloadUrl)
MainActivity.app!!.startActivity(i)
}
}
@JvmStatic
fun showQRCode(funid: String?, content: String?) {
UIUtils.showQRCode(MainActivity.app, content, "")
}
@JvmStatic
fun showWebPage(funid: String?, url: String?) {
MainActivity.app!!.showPage(funid, url)
}
@JvmStatic
fun scanQRCode(funid: String?, title: String?) {
// MainActivity.app.showQRScan(funid, title);
singleton!!.showQRScan(funid, title)
}
@JvmStatic
fun signWithTiktok(funid: String?) {
MainActivity.app!!.signWithTiktok(funid!!)
}
@JvmStatic
fun signWithFacebook(funid: String?) {
MainActivity.app!!.signWithFacebook(funid!!)
}
@JvmStatic
fun shareWithFacebook(content: String?) {
MainActivity.app!!.shareWithFacebook(content)
}
@JvmStatic
fun signWithTwitter(funid: String?) {
MainActivity.app!!.signWithTwitter(funid!!)
}
@JvmStatic
fun signWithGoogle(funid: String?) {
MainActivity.app!!.signWithGoogle(funid)
}
@JvmStatic
fun signWithApple(funid: String?) {
MainActivity.app!!.signWithApple(funid!!)
}
@JvmStatic
fun signOutGoogle(funid: String?) {
MainActivity.app!!.signOutGoogle(funid)
}
@JvmStatic
fun logEvent(content: String?) {
MainActivity.app!!.logEvent(content)
}
@JvmStatic
fun queryProducts(funid: String?, skuListStr: String) {
Log.i(TAG, "queryProducts with: $skuListStr")
if (payClient == null) {
payClient = PayClient.instance
}
val skuList: MutableList<String> = ArrayList()
if (skuListStr.contains(",")) {
val skuArr = skuListStr.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
skuList.addAll(Arrays.asList(*skuArr))
} else {
skuList.add(skuListStr)
}
payClient!!.queryProductList(funid, skuList)
}
@JvmStatic
fun buyProduct(funid: String?, productId: String, orderId: String?) {
Log.i(TAG, "buyProduct with: $productId")
if (payClient == null) {
payClient = PayClient.instance
}
payClient!!.buyOne(funid, productId, orderId)
}
@JvmStatic
fun queryPurchase(funid: String?) {
Log.i(TAG, "queryPurchase")
if (payClient == null) {
payClient = PayClient.instance
}
payClient!!.queryPurchase(funid)
}
@JvmStatic
fun passStorageState(funid: String?, account: String) {
Log.i(TAG, "passStorageState with: $account")
MainActivity.app!!.passStorageState(funid, account)
}
@JvmStatic
fun storagePass(funid: String?, account: String?, password: String?) {
MainActivity.app!!.storagePass(funid, account, password)
}
@JvmStatic
fun authGetStoragePass(funid: String?, account: String?) {
MainActivity.app!!.authGetStoragePass(funid, account!!)
}
@JvmStatic
fun storageGameData(data: String?) {
MainApplication.application!!.gameData = data
}
@JvmStatic
fun getClientId(funid: String?) {
Log.i(TAG, "getClientId ")
MainActivity.app!!.getClientId(funid)
}
@JvmStatic
fun onProxyCB(funId: String?, data: String?) {
EventBus.getDefault().post(ProxyCBEvent(funId!!, data!!))
}
fun nativeCb(funId: String?, error: String?, dataStr: String?) {
var funId = funId
val result = JSONObject()
try {
if (Strings.isNullOrEmpty(error)) {
result.put("errcode", 0)
result.put("data", dataStr)
} else {
result.put("errcode", 1)
result.put("errmessage", error)
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException: " + e.message)
}
if (funId == null || funId.isEmpty()) {
funId = MainActivity.app!!.funId
}
Log.i(TAG, String.format("%s native cb, error: %s, data: %s", funId, error, dataStr))
if (funId != null && funId.startsWith(FUNID_PREFIX)) {
EventBus.getDefault().post(CallJSEvent(funId, result.toString()))
} else {
val finalFunId = funId
ThreadUtils.runInMain { runJS(finalFunId, "jniCallback", arrayOf(result.toString())) }
}
}
fun callNativeJS(funId: String?, methodName: String, params: Array<String?>) {
ThreadUtils.runInMain { runJS(funId, methodName, params) }
}
fun nativeCb(result: NativeResult) {
nativeCb(result.funid, result.error, result.dataStr)
}
}
}

View File

@ -1,37 +0,0 @@
package com.jc.jcfw;
public class NativeResult {
private String funid;
private String error;
private String dataStr;
public NativeResult(String funid, String error, String dataStr) {
this.funid = funid;
this.error = error;
this.dataStr = dataStr;
}
public String getFunid() {
return funid;
}
public void setFunid(String funid) {
this.funid = funid;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getDataStr() {
return dataStr;
}
public void setDataStr(String dataStr) {
this.dataStr = dataStr;
}
}

View File

@ -0,0 +1,3 @@
package com.jc.jcfw
class NativeResult(var funid: String, var error: String?, var dataStr: String?)

View File

@ -1,5 +0,0 @@
package com.jc.jcfw;
public interface UnityCallback {
public void nativeCallback(String funId, String str, int type);
}

View File

@ -0,0 +1,5 @@
package com.jc.jcfw
interface UnityCallback {
fun nativeCallback(funId: String?, str: String?, type: Int)
}

View File

@ -1,251 +0,0 @@
package com.jc.jcfw.google;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.ctf.games.release.MainActivity;
import com.jc.jcfw.JcSDK;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PayClient extends Activity implements PurchasesUpdatedListener {
private static final String TAG = "GooglePayClient";
private static volatile PayClient mInstance = null;
private static BillingClient billingClient;
private static ConcurrentHashMap<String, ProductDetails> skuDetailsMap;
private Context mContext = null;
private String mFunId;
public static PayClient getInstance() {
if (null == mInstance) {
synchronized (PayClient.class) {
if (null == mInstance) {
mInstance = new PayClient();
}
}
}
return mInstance;
}
public void init(Context context) {
this.mContext = context;
skuDetailsMap = new ConcurrentHashMap<>();
billingClient = BillingClient.newBuilder(context).enablePendingPurchases().setListener(this).build();
connectToPlay();
}
private void connectToPlay() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
Log.i(TAG, "onBillingSetupFinished, response code: " + billingResult.getResponseCode());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
Log.i(TAG, "BillingClient is ready");
} else {
Log.i(TAG, "error init BillingClient with error:: " +billingResult.getResponseCode());
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
connectToPlay();
}
});
}
JSONArray handlePurchase(JSONArray dataArr, Purchase purchase) throws JSONException {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
// || purchase.getPurchaseState() == Purchase.PurchaseState.PENDING
) {
// Acknowledge purchase and grant the item to the user
// Grant entitlement to the user.
Log.i(TAG, "handlePurchase:" + purchase.getOriginalJson());
Log.i(TAG, "purchase sign:" + purchase.getSignature());
if (!purchase.isAcknowledged() && purchase.getProducts().size() > 0) {
// consumables 消耗型
JSONObject data = new JSONObject();
data.put("id", purchase.getProducts().get(0));
data.put("token", purchase.getPurchaseToken());
dataArr.put(data);
}
}
return dataArr;
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
Log.i(TAG, "onPurchasesUpdated with status: " + billingResult.getResponseCode());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
final JSONArray dataArr = new JSONArray();
boolean hasErr = false;
for (Purchase purchase : purchases) {
try {
handlePurchase(dataArr, purchase);
} catch (JSONException e) {
hasErr = true;
break;
}
}
if (hasErr) {
purchaseUpdateCb(null,"error parse purchase data", null);
} else {
purchaseUpdateCb(null,null, dataArr.toString());
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
purchaseUpdateCb(null,"user cancel buy", null);
} else {
String errmsg = billingResult.getDebugMessage();
if (errmsg.isEmpty()) {
errmsg = "other error";
}
purchaseUpdateCb(null, errmsg, null);
}
}
private void purchaseUpdateCb(String funId, String error, String dataStr) {
if (funId == null || funId.isEmpty()) {
if (mFunId != null && !mFunId.isEmpty()) {
final String _funId = mFunId;
runOnUiThread(() -> JcSDK.nativeCb(_funId, error, dataStr));
mFunId = null;
}
} else {
runOnUiThread(() -> JcSDK.nativeCb(funId, error, dataStr));
}
}
private boolean parseProductDetails(JSONArray dataArr, ProductDetails skuDetails) {
JSONObject data = new JSONObject();
try {
data.put("name", skuDetails.getTitle());
data.put("description", skuDetails.getDescription());
data.put("id", skuDetails.getProductId());
data.put("type", skuDetails.getProductType());
if (skuDetails.getProductType().equals(BillingClient.ProductType.INAPP)) {
data.put("currencyCode",
skuDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
data.put("priceValue", skuDetails.getOneTimePurchaseOfferDetails().getPriceAmountMicros());
data.put("priceShow", skuDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
}
dataArr.put(data);
return true;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
}
public void queryProductList(String funId, List<String> productIds) {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
for (String productId : productIds) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP)
.build());
}
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(
params,
(billingResult, productDetailsList) -> {
// Process the result
Map<String, ProductDetails> pMap = new HashMap<>();
for (ProductDetails details : productDetailsList) {
skuDetailsMap.put(details.getProductId(), details);
pMap.put(details.getProductId(), details);
}
final JSONArray dataArr = new JSONArray();
boolean hasErr = false;
for (Map.Entry<String, ProductDetails> entry : pMap.entrySet()) {
ProductDetails skuDetails = entry.getValue();
if (!parseProductDetails(dataArr, skuDetails)) {
hasErr = true;
break;
}
}
if (hasErr) {
purchaseUpdateCb(funId, "parse product detail json error", null);
} else {
purchaseUpdateCb(funId, null, dataArr.toString());
}
});
}
public void queryPurchase(String funId) {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build(),
(billingResult, purchases) -> {
// Process the result
final JSONArray dataArr = new JSONArray();
boolean hasErr = false;
for (Purchase purchase : purchases) {
try {
handlePurchase(dataArr, purchase);
} catch (JSONException e) {
hasErr = true;
break;
}
}
if (hasErr) {
purchaseUpdateCb(funId, "error parse purchase data", null);
} else {
purchaseUpdateCb(funId, null, dataArr.toString());
}
});
}
public void buyOne(String funId, String productId, String orderId) {
if (mFunId != null && !mFunId.isEmpty()) {
purchaseUpdateCb(funId, "another purchase is in progress", null);
return;
}
if (!skuDetailsMap.containsKey(productId)) {
purchaseUpdateCb(funId, "product with : "+productId+ " not found", null);
return;
}
ProductDetails productDetails = skuDetailsMap.get(productId);
// Set the parameters for the offer that will be presented
// in the billing flow creating separate productDetailsParamsList variable
List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = Collections
.singletonList(BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build());
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setObfuscatedAccountId(orderId)
.setObfuscatedProfileId(orderId)
.build();
// Launch the billing flow
this.mFunId = funId;
MainActivity.app.runOnUiThread(() -> {
billingClient.launchBillingFlow((Activity) mContext, billingFlowParams);
});
}
}

View File

@ -0,0 +1,250 @@
package com.jc.jcfw.google
import android.app.Activity
import android.content.Context
import android.util.Log
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.ctf.games.release.MainActivity
import com.jc.jcfw.JcSDK
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.ConcurrentHashMap
class PayClient : Activity(), PurchasesUpdatedListener {
private var mContext: Context? = null
private var mFunId: String? = null
fun init(context: Context?) {
mContext = context
skuDetailsMap = ConcurrentHashMap()
billingClient =
BillingClient.newBuilder(context!!).enablePendingPurchases().setListener(this).build()
connectToPlay()
}
private fun connectToPlay() {
billingClient!!.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.i(TAG, "onBillingSetupFinished, response code: " + billingResult.responseCode)
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
Log.i(TAG, "BillingClient is ready")
} else {
Log.i(TAG, "error init BillingClient with error:: " + billingResult.responseCode)
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
connectToPlay()
}
})
}
@Throws(JSONException::class)
fun handlePurchase(dataArr: JSONArray, purchase: Purchase): JSONArray {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED // || purchase.getPurchaseState() == Purchase.PurchaseState.PENDING
) {
// Acknowledge purchase and grant the item to the user
// Grant entitlement to the user.
Log.i(TAG, "handlePurchase:" + purchase.originalJson)
Log.i(TAG, "purchase sign:" + purchase.signature)
if (!purchase.isAcknowledged && purchase.products.size > 0) {
// consumables 消耗型
val data = JSONObject()
data.put("id", purchase.products[0])
data.put("token", purchase.purchaseToken)
dataArr.put(data)
}
}
return dataArr
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
Log.i(TAG, "onPurchasesUpdated with status: " + billingResult.responseCode)
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
&& purchases != null
) {
val dataArr = JSONArray()
var hasErr = false
for (purchase in purchases) {
try {
handlePurchase(dataArr, purchase)
} catch (e: JSONException) {
hasErr = true
break
}
}
if (hasErr) {
purchaseUpdateCb(null, "error parse purchase data", null)
} else {
purchaseUpdateCb(null, null, dataArr.toString())
}
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
purchaseUpdateCb(null, "user cancel buy", null)
} else {
var errmsg = billingResult.debugMessage
if (errmsg.isEmpty()) {
errmsg = "other error"
}
purchaseUpdateCb(null, errmsg, null)
}
}
private fun purchaseUpdateCb(funId: String?, error: String?, dataStr: String?) {
if (funId == null || funId.isEmpty()) {
if (mFunId != null && !mFunId!!.isEmpty()) {
runOnUiThread { JcSDK.nativeCb(mFunId, error, dataStr) }
mFunId = null
}
} else {
runOnUiThread { JcSDK.nativeCb(funId, error, dataStr) }
}
}
private fun parseProductDetails(dataArr: JSONArray, skuDetails: ProductDetails): Boolean {
val data = JSONObject()
return try {
data.put("name", skuDetails.title)
data.put("description", skuDetails.description)
data.put("id", skuDetails.productId)
data.put("type", skuDetails.productType)
if (skuDetails.productType == BillingClient.ProductType.INAPP) {
data.put(
"currencyCode",
skuDetails.oneTimePurchaseOfferDetails!!.priceCurrencyCode
)
data.put("priceValue", skuDetails.oneTimePurchaseOfferDetails!!.priceAmountMicros)
data.put("priceShow", skuDetails.oneTimePurchaseOfferDetails!!.formattedPrice)
}
dataArr.put(data)
true
} catch (e: JSONException) {
e.printStackTrace()
false
}
}
fun queryProductList(funId: String?, productIds: List<String?>) {
val productList: MutableList<QueryProductDetailsParams.Product> = ArrayList()
for (productId in productIds) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId!!)
.setProductType(BillingClient.ProductType.INAPP)
.build()
)
}
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
billingClient!!.queryProductDetailsAsync(
params
) { billingResult: BillingResult?, productDetailsList: List<ProductDetails> ->
// Process the result
val pMap: MutableMap<String, ProductDetails> = HashMap()
for (details in productDetailsList) {
skuDetailsMap!![details.productId] = details
pMap[details.productId] = details
}
val dataArr = JSONArray()
var hasErr = false
for ((_, skuDetails) in pMap) {
if (!parseProductDetails(dataArr, skuDetails)) {
hasErr = true
break
}
}
if (hasErr) {
purchaseUpdateCb(funId, "parse product detail json error", null)
} else {
purchaseUpdateCb(funId, null, dataArr.toString())
}
}
}
fun queryPurchase(funId: String?) {
billingClient!!.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build()
) { billingResult: BillingResult?, purchases: List<Purchase> ->
// Process the result
val dataArr = JSONArray()
var hasErr = false
for (purchase in purchases) {
try {
handlePurchase(dataArr, purchase)
} catch (e: JSONException) {
hasErr = true
break
}
}
if (hasErr) {
purchaseUpdateCb(funId, "error parse purchase data", null)
} else {
purchaseUpdateCb(funId, null, dataArr.toString())
}
}
}
fun buyOne(funId: String?, productId: String, orderId: String?) {
if (mFunId != null && !mFunId!!.isEmpty()) {
purchaseUpdateCb(funId, "another purchase is in progress", null)
return
}
if (!skuDetailsMap!!.containsKey(productId)) {
purchaseUpdateCb(funId, "product with : $productId not found", null)
return
}
val productDetails = skuDetailsMap!![productId]
// Set the parameters for the offer that will be presented
// in the billing flow creating separate productDetailsParamsList variable
val productDetailsParamsList = listOf(
ProductDetailsParams.newBuilder()
.setProductDetails(productDetails!!)
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setObfuscatedAccountId(orderId!!)
.setObfuscatedProfileId(orderId)
.build()
// Launch the billing flow
mFunId = funId
MainActivity.app!!.runOnUiThread {
billingClient!!.launchBillingFlow(
(mContext as Activity?)!!, billingFlowParams
)
}
}
companion object {
private const val TAG = "GooglePayClient"
@Volatile
private var mInstance: PayClient? = null
private var billingClient: BillingClient? = null
private var skuDetailsMap: ConcurrentHashMap<String, ProductDetails>? = null
val instance: PayClient?
get() {
if (null == mInstance) {
synchronized(PayClient::class.java) {
if (null == mInstance) {
mInstance = PayClient()
}
}
}
return mInstance
}
}
}

View File

@ -1,101 +0,0 @@
package com.jc.jcfw.util;
import android.util.Log;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.extensions.android.json.AndroidJsonFactory;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.FileContent;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.function.Consumer;
public class DriveUtils {
private static final String TAG = DriveUtils.class.getSimpleName();
public static Drive generateService(GoogleAccountCredential credential, String appName) {
return new Drive.Builder(
AndroidHttp.newCompatibleTransport(),
AndroidJsonFactory.getDefaultInstance(),
credential)
.setApplicationName(appName)
.build();
}
/**
* query app file by filename
*
* @param service
* @param fileName
* @throws IOException
*/
public static String queryOneAppFile(Drive service, String fileName) {
boolean querySuccess = false;
String fileId = "";
while (!querySuccess) {
try {
FileList files = service.files().list()
.setSpaces("appDataFolder")
.setFields("nextPageToken, files(id, name)")
.setPageSize(100)
.execute();
querySuccess = true;
for (File file : files.getFiles()) {
Log.i(TAG, String.format("Found file: %s (%s)\n", file.getName(), file.getId()));
if (file.getName().equals(fileName)) {
fileId = file.getId();
break;
}
}
} catch (IOException e) {
Log.i(TAG, "error query file from drive");
}
}
return fileId;
}
/**
* download one file from drive
*
* @param service
* @param fileId
* @throws IOException
*/
public static String downloadFile(Drive service, String fileId) throws IOException {
OutputStream outputStream = new ByteArrayOutputStream();
service.files().get(fileId).executeMediaAndDownloadTo(outputStream);
// convert outputStream to JSON string
// String json = outputStream.toString();
return outputStream.toString();
}
/**
* upload one file to Drive
*
* @param service
* @param filePath file absolute path
* @param fileType application/json
* @throws IOException
*/
public static String uploadAppFile(Drive service, java.io.File filePath, String fileType) throws IOException {
String fileName = filePath.getName();
File fileMetadata = new File();
fileMetadata.setName(fileName);
fileMetadata.setParents(Collections.singletonList("appDataFolder"));
// "application/json"
FileContent mediaContent = new FileContent(fileType, filePath);
File file = service.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute();
Log.i(TAG, String.format("%s upload success, File ID: %s", fileName, file.getId()));
return file.getId();
}
}

View File

@ -0,0 +1,96 @@
package com.jc.jcfw.util
import android.util.Log
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.extensions.android.json.AndroidJsonFactory
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.http.FileContent
import com.google.api.services.drive.Drive
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.OutputStream
object DriveUtils {
private val TAG = DriveUtils::class.java.simpleName
fun generateService(credential: GoogleAccountCredential?, appName: String?): Drive {
return Drive.Builder(
AndroidHttp.newCompatibleTransport(),
AndroidJsonFactory.getDefaultInstance(),
credential
)
.setApplicationName(appName)
.build()
}
/**
* query app file by filename
*
* @param service
* @param fileName
* @throws IOException
*/
fun queryOneAppFile(service: Drive, fileName: String): String {
var querySuccess = false
var fileId = ""
while (!querySuccess) {
try {
val files = service.files().list()
.setSpaces("appDataFolder")
.setFields("nextPageToken, files(id, name)")
.setPageSize(100)
.execute()
querySuccess = true
for (file in files.files) {
Log.i(TAG, String.format("Found file: %s (%s)\n", file.name, file.id))
if (file.name == fileName) {
fileId = file.id
break
}
}
} catch (e: IOException) {
Log.i(TAG, "error query file from drive")
}
}
return fileId
}
/**
* download one file from drive
*
* @param service
* @param fileId
* @throws IOException
*/
@Throws(IOException::class)
fun downloadFile(service: Drive, fileId: String?): String {
val outputStream: OutputStream = ByteArrayOutputStream()
service.files()[fileId].executeMediaAndDownloadTo(outputStream)
// convert outputStream to JSON string
// String json = outputStream.toString();
return outputStream.toString()
}
/**
* upload one file to Drive
*
* @param service
* @param filePath file absolute path
* @param fileType application/json
* @throws IOException
*/
@Throws(IOException::class)
fun uploadAppFile(service: Drive, filePath: File, fileType: String?): String {
val fileName = filePath.name
val fileMetadata = com.google.api.services.drive.model.File()
fileMetadata.name = fileName
fileMetadata.parents = listOf("appDataFolder")
// "application/json"
val mediaContent = FileContent(fileType, filePath)
val file = service.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute()
Log.i(TAG, String.format("%s upload success, File ID: %s", fileName, file.id))
return file.id
}
}

View File

@ -1,97 +0,0 @@
package com.jc.jcfw.util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DriverApiUtils {
private static final String driveApiBase = "https://www.googleapis.com/drive/v3/files";
private static final String driveApiUploadBase = "https://www.googleapis.com/upload/drive/v3/files";
private final String TAG = getClass().getSimpleName();
private String token = "";
public DriverApiUtils() {
}
public DriverApiUtils(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public JSONArray fileList() throws IOException, JSONException {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url(driveApiBase + "?spaces=appDataFolder&fields=files(id, name, modifiedTime)")
.get()
.addHeader("Authorization", "Bearer " + token)
.build();
try (Response response = client.newCall(request).execute()) {
String resStr = response.body().string();
JSONObject resJson = new JSONObject(resStr);
return resJson.getJSONArray("files");
}
}
public String queryOneAppFile(String fileName) throws IOException, JSONException {
JSONArray fileList = fileList();
for (int i = 0; i < fileList.length(); i++) {
JSONObject file = fileList.getJSONObject(i);
if (file.getString("name").equals(fileName)) {
return file.getString("id");
}
}
return "";
}
public String fileInfo(String fileId) throws IOException {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url(String.format("%s/%s?alt=media", driveApiBase, fileId))
.get()
.addHeader("Authorization", "Bearer " + token)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
public String uploadFile(java.io.File filePath, String fileType) throws IOException {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
String fileName = filePath.getName();
String optionStr = "{\"name\":\""+fileName+"\",\"parents\":[\"appDataFolder\"]}";
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("", "upload-options.json",
RequestBody.create(optionStr, MediaType.parse("application/json")))
.addFormDataPart("", fileName,
RequestBody.create(filePath, MediaType.parse("application/octet-stream")))
.build();
Request request = new Request.Builder()
.url(driveApiUploadBase + "?uploadType=multipart")
.post(body)
.addHeader("Content-Type", fileType)
.addHeader("Authorization", "Bearer " + token)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
}

View File

@ -0,0 +1,98 @@
package com.jc.jcfw.util
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Request.Builder
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.IOException
class DriverApiUtils {
private val TAG = javaClass.simpleName
var token = ""
constructor() {}
constructor(token: String) {
this.token = token
}
@Throws(IOException::class, JSONException::class)
fun fileList(): JSONArray {
val client = OkHttpClient().newBuilder()
.build()
val request: Request = Builder()
.url(driveApiBase + "?spaces=appDataFolder&fields=files(id, name, modifiedTime)")
.get()
.addHeader("Authorization", "Bearer $token")
.build()
client.newCall(request).execute().use { response ->
val resStr = response.body!!.string()
val resJson = JSONObject(resStr)
return resJson.getJSONArray("files")
}
}
@Throws(IOException::class, JSONException::class)
fun queryOneAppFile(fileName: String): String {
val fileList = fileList()
for (i in 0 until fileList.length()) {
val file = fileList.getJSONObject(i)
if (file.getString("name") == fileName) {
return file.getString("id")
}
}
return ""
}
@Throws(IOException::class)
fun fileInfo(fileId: String?): String {
val client = OkHttpClient().newBuilder()
.build()
val request: Request = Builder()
.url(String.format("%s/%s?alt=media", driveApiBase, fileId))
.get()
.addHeader("Authorization", "Bearer $token")
.build()
client.newCall(request).execute().use { response -> return response.body!!.string() }
}
@Throws(IOException::class)
fun uploadFile(filePath: File, fileType: String): String {
val client = OkHttpClient().newBuilder()
.build()
val fileName = filePath.name
val optionStr = "{\"name\":\"$fileName\",\"parents\":[\"appDataFolder\"]}"
val body: RequestBody = MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart(
"", "upload-options.json",
optionStr.toRequestBody("application/json".toMediaTypeOrNull())
)
.addFormDataPart(
"", fileName,
filePath.asRequestBody("application/octet-stream".toMediaTypeOrNull())
)
.build()
val request: Request = Builder()
.url("$driveApiUploadBase?uploadType=multipart")
.post(body)
.addHeader("Content-Type", fileType)
.addHeader("Authorization", "Bearer $token")
.build()
val response = client.newCall(request).execute()
return response.body!!.string()
}
companion object {
private const val driveApiBase = "https://www.googleapis.com/drive/v3/files"
private const val driveApiUploadBase = "https://www.googleapis.com/upload/drive/v3/files"
}
}

View File

@ -1,240 +0,0 @@
package com.jc.jcfw.util;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
public class FileUtils {
private static final String TAG = FileUtils.class.getSimpleName();
/**
* check if path specificed exists, create it if not exists
*
* @param fileName path
* @return TRUE or FALSE
*/
public static boolean fileIsExist(String fileName) {
File file = new File(fileName);
if (file.exists())
return true;
else {
return file.mkdirs();
}
}
public static void writeFile(File filePath, String content) throws IOException {
FileOutputStream out = new FileOutputStream(filePath);
out.write(content.getBytes());
out.close();
}
public static JSONObject readJsonFromFile(File filePath) throws IOException, JSONException {
RandomAccessFile f = new RandomAccessFile(filePath, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new JSONObject(new String(bytes));
}
/**
* get image base path of external storage
*/
public static String getPath(Context context) {
String fileName = "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
fileName = context.getExternalFilesDir("").getAbsolutePath() + "/current/";
} else {
if ("Xiaomi".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else if ("HUAWEI".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else if ("HONOR".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else if ("OPPO".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else if ("vivo".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else if ("samsung".equalsIgnoreCase(Build.BRAND)) {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
} else {
fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/";
}
}
File file = new File(fileName);
if (file.mkdirs()) {
return fileName;
}
return fileName;
}
public static String insertImageIntoGallery(Context activity, Bitmap source, String filename, String title) {
ContentResolver cr = activity.getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, title);
values.put(MediaStore.Images.Media.DISPLAY_NAME, title);
values.put(MediaStore.Images.Media.DESCRIPTION, title);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
// Add the date meta data to ensure the image is added at the front of the gallery
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Video.Media.IS_PENDING, 1);
}
Uri url = null;
String stringUrl = null; /* value to be returned */
try {
url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (source != null) {
OutputStream imageOut = cr.openOutputStream(url);
try {
source.compress(Bitmap.CompressFormat.JPEG, 100, imageOut);
} finally {
imageOut.close();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Video.Media.IS_PENDING, 0);
cr.update(url, values, null, null);
}
}
} else {
cr.delete(url, null, null);
return storeToAlternateSd(activity, source, filename);
// url = null;
}
} catch (Exception e) {
if (url != null) {
cr.delete(url, null, null);
// url = null;
}
return storeToAlternateSd(activity, source, filename);
}
if (url != null) {
stringUrl = url.toString();
}
return stringUrl;
}
/**
* If we have issues saving into our MediaStore, save it directly to our SD card. We can then interact with this file
* directly, opposed to pulling from the MediaStore. Again, this is a backup method if things don't work out as we
* would expect (seeing as most devices will have a MediaStore).
*
* @param src
* @return - the file's path
*/
private static String storeToAlternateSd(Context activity, Bitmap src, String filename){
if(src == null)
return null;
String sdCardDirectory = getPath(activity);
File image = new File(sdCardDirectory, filename + ".jpg");
try {
FileOutputStream imageOut = new FileOutputStream(image);
src.compress(Bitmap.CompressFormat.JPEG, 100, imageOut);
imageOut.close();
return image.getAbsolutePath();
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/*
* save bitmap to system gallery
*/
public static String saveBitmap(Context activity, String oid, Bitmap bitmap) {
String title = "wallet_key_" + oid;
String imageName = "wallet_key_" + oid;
String uri = insertImageIntoGallery(activity, bitmap, imageName, title);
Log.i(TAG, "save image success: " + uri);
return uri;
}
public static Bitmap loadImgData(Context activity, String oid) {
Uri uri = readImageFromGallery(activity, oid);
Bitmap data;
if (uri != null) {
try {
data = MediaStore.Images.Media.getBitmap(activity.getContentResolver(),uri);
} catch (IOException e) {
data = readImageFromExt(activity, oid);
}
} else {
data = readImageFromExt(activity, oid);
}
return data;
}
public static Bitmap readImageFromExt(Context activity, String oid) {
String sdCardDirectory = getPath(activity);
String imageName = "wallet_key_" + oid;
File file = new File(sdCardDirectory, imageName + ".jpg");
if (!file.exists()) {
return null;
}
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeFile(file.getPath());
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
public static Uri readImageFromGallery(Context activity, String oid) {
String filename = "wallet_key_" + oid;
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
Uri result = null;
String[] projection = {MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME};
final String orderBy = MediaStore.Images.Media.DATE_ADDED;
Cursor cursor = activity.getContentResolver().query(uri, projection, null, null, orderBy + " DESC");
String imageName = "wallet_key_" + oid;
if (cursor != null) {
int nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME);
int idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID);
int dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String name = cursor.getString(nameColumn);
long id = cursor.getLong(idColumn);
Log.i(TAG, "img name: " + name + " id: " + id);
if (name.contains(imageName)) {
String data = cursor.getString(dataColumn);
result = ContentUris.withAppendedId(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), id);
Log.i(TAG, "img name: " + name + " id: " + id + " data:" + data);
break;
}
}
cursor.close();
}
return result;
}
}

View File

@ -0,0 +1,237 @@
package com.jc.jcfw.util
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
object FileUtils {
private val TAG = FileUtils::class.java.simpleName
/**
* check if path specificed exists, create it if not exists
*
* @param fileName path
* @return TRUE or FALSE
*/
fun fileIsExist(fileName: String?): Boolean {
val file = File(fileName)
return if (file.exists()) true else {
file.mkdirs()
}
}
@Throws(IOException::class)
fun writeFile(filePath: File?, content: String) {
val out = FileOutputStream(filePath)
out.write(content.toByteArray())
out.close()
}
@Throws(IOException::class, JSONException::class)
fun readJsonFromFile(filePath: File?): JSONObject {
val f = RandomAccessFile(filePath, "r")
val bytes = ByteArray(f.length().toInt())
f.readFully(bytes)
f.close()
return JSONObject(String(bytes))
}
/**
* get image base path of external storage
*/
fun getPath(context: Context): String {
var fileName = ""
fileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.getExternalFilesDir("")!!.absolutePath + "/current/"
} else {
if ("Xiaomi".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else if ("HUAWEI".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else if ("HONOR".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else if ("OPPO".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else if ("vivo".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else if ("samsung".equals(Build.BRAND, ignoreCase = true)) {
Environment.getExternalStorageDirectory().path + "/DCIM/Camera/"
} else {
Environment.getExternalStorageDirectory().path + "/DCIM/"
}
}
val file = File(fileName)
return if (file.mkdirs()) {
fileName
} else fileName
}
fun insertImageIntoGallery(
activity: Context,
source: Bitmap?,
filename: String,
title: String?
): String? {
val cr = activity.contentResolver
val values = ContentValues()
values.put(MediaStore.Images.Media.TITLE, title)
values.put(MediaStore.Images.Media.DISPLAY_NAME, title)
values.put(MediaStore.Images.Media.DESCRIPTION, title)
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
// Add the date meta data to ensure the image is added at the front of the gallery
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Video.Media.IS_PENDING, 1)
}
var url: Uri? = null
var stringUrl: String? = null /* value to be returned */
try {
url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (source != null) {
val imageOut = cr.openOutputStream(url!!)
try {
source.compress(Bitmap.CompressFormat.JPEG, 100, imageOut)
} finally {
imageOut!!.close()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Video.Media.IS_PENDING, 0)
cr.update(url, values, null, null)
}
}
} else {
cr.delete(url!!, null, null)
return storeToAlternateSd(activity, source, filename)
// url = null;
}
} catch (e: Exception) {
if (url != null) {
cr.delete(url, null, null)
// url = null;
}
return storeToAlternateSd(activity, source, filename)
}
if (url != null) {
stringUrl = url.toString()
}
return stringUrl
}
/**
* If we have issues saving into our MediaStore, save it directly to our SD card. We can then interact with this file
* directly, opposed to pulling from the MediaStore. Again, this is a backup method if things don't work out as we
* would expect (seeing as most devices will have a MediaStore).
*
* @param src
* @return - the file's path
*/
private fun storeToAlternateSd(activity: Context, src: Bitmap?, filename: String): String? {
if (src == null) return null
val sdCardDirectory = getPath(activity)
val image = File(sdCardDirectory, "$filename.jpg")
return try {
val imageOut = FileOutputStream(image)
src.compress(Bitmap.CompressFormat.JPEG, 100, imageOut)
imageOut.close()
image.absolutePath
} catch (ex: IOException) {
ex.printStackTrace()
null
}
}
/*
* save bitmap to system gallery
*/
fun saveBitmap(activity: Context, oid: String, bitmap: Bitmap?): String? {
val title = "wallet_key_$oid"
val imageName = "wallet_key_$oid"
val uri = insertImageIntoGallery(activity, bitmap, imageName, title)
Log.i(TAG, "save image success: $uri")
return uri
}
fun loadImgData(activity: Context, oid: String): Bitmap? {
val uri = readImageFromGallery(activity, oid)
val data: Bitmap?
data = if (uri != null) {
try {
MediaStore.Images.Media.getBitmap(activity.contentResolver, uri)
} catch (e: IOException) {
readImageFromExt(activity, oid)
}
} else {
readImageFromExt(activity, oid)
}
return data
}
fun readImageFromExt(activity: Context, oid: String): Bitmap? {
val sdCardDirectory = getPath(activity)
val imageName = "wallet_key_$oid"
val file = File(sdCardDirectory, "$imageName.jpg")
if (!file.exists()) {
return null
}
var bitmap: Bitmap? = null
try {
bitmap = BitmapFactory.decodeFile(file.path)
} catch (e: Exception) {
e.printStackTrace()
}
return bitmap
}
fun readImageFromGallery(activity: Context, oid: String): Uri? {
val filename = "wallet_key_$oid"
val uri: Uri
uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
var result: Uri? = null
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME
)
val orderBy = MediaStore.Images.Media.DATE_ADDED
val cursor = activity.contentResolver.query(uri, projection, null, null, "$orderBy DESC")
val imageName = "wallet_key_$oid"
if (cursor != null) {
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val name = cursor.getString(nameColumn)
val id = cursor.getLong(idColumn)
Log.i(TAG, "img name: $name id: $id")
if (name.contains(imageName)) {
val data = cursor.getString(dataColumn)
result = ContentUris.withAppendedId(
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL),
id
)
Log.i(TAG, "img name: $name id: $id data:$data")
break
}
}
cursor.close()
}
return result
}
}

View File

@ -1,31 +0,0 @@
package com.jc.jcfw.util;
import android.os.Bundle;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
public class JsonUtils {
public static Bundle convertJsonToBundle(String jsonString) throws JSONException {
Bundle bundle = new Bundle();
JSONObject jsonObject = new JSONObject(jsonString);
traverseJsonObject(jsonObject, bundle);
return bundle;
}
public static void traverseJsonObject(JSONObject jsonObject, Bundle bundle) throws JSONException {
Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.get(key);
// if (value instanceof JSONObject) {
// traverseJsonObject((JSONObject) value, bundle);
// } else {
// bundle.putString(key, value.toString());
// }
bundle.putString(key, value.toString());
}
}
}

View File

@ -0,0 +1,30 @@
package com.jc.jcfw.util
import android.os.Bundle
import org.json.JSONException
import org.json.JSONObject
object JsonUtils {
@Throws(JSONException::class)
fun convertJsonToBundle(jsonString: String?): Bundle {
val bundle = Bundle()
val jsonObject = JSONObject(jsonString)
traverseJsonObject(jsonObject, bundle)
return bundle
}
@Throws(JSONException::class)
fun traverseJsonObject(jsonObject: JSONObject, bundle: Bundle) {
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = jsonObject[key]
// if (value instanceof JSONObject) {
// traverseJsonObject((JSONObject) value, bundle);
// } else {
// bundle.putString(key, value.toString());
// }
bundle.putString(key, value.toString())
}
}
}

View File

@ -1,72 +0,0 @@
package com.jc.jcfw.util;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.Map;
public class SharedPreferencesHelper {
private final SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
public SharedPreferencesHelper(Context context, String FILE_NAME) {
sharedPreferences = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}
public void put(String key, Object object) {
if (object instanceof String) {
editor.putString(key, (String) object);
} else if (object instanceof Integer) {
editor.putInt(key, (Integer) object);
} else if (object instanceof Boolean) {
editor.putBoolean(key, (Boolean) object);
} else if (object instanceof Float) {
editor.putFloat(key, (Float) object);
} else if (object instanceof Long) {
editor.putLong(key, (Long) object);
} else {
editor.putString(key, object.toString());
}
editor.commit();
}
public Object getSharedPreference(String key, Object defaultObject) {
if (defaultObject instanceof String) {
return sharedPreferences.getString(key, (String) defaultObject);
} else if (defaultObject instanceof Integer) {
return sharedPreferences.getInt(key, (Integer) defaultObject);
} else if (defaultObject instanceof Boolean) {
return sharedPreferences.getBoolean(key, (Boolean) defaultObject);
} else if (defaultObject instanceof Float) {
return sharedPreferences.getFloat(key, (Float) defaultObject);
} else if (defaultObject instanceof Long) {
return sharedPreferences.getLong(key, (Long) defaultObject);
} else {
return sharedPreferences.getString(key, null);
}
}
public void remove(String key) {
editor.remove(key);
editor.commit();
}
public void clear() {
editor.clear();
editor.commit();
}
public Boolean contain(String key) {
return sharedPreferences.contains(key);
}
public Map<String, ?> getAll() {
return sharedPreferences.getAll();
}
}

View File

@ -0,0 +1,67 @@
package com.jc.jcfw.util
import android.content.Context
import android.content.SharedPreferences
class SharedPreferencesHelper(context: Context, FILE_NAME: String?) {
private val sharedPreferences: SharedPreferences
private val editor: SharedPreferences.Editor
init {
sharedPreferences = context.getSharedPreferences(
FILE_NAME,
Context.MODE_PRIVATE
)
editor = sharedPreferences.edit()
}
fun put(key: String?, `object`: Any) {
if (`object` is String) {
editor.putString(key, `object`)
} else if (`object` is Int) {
editor.putInt(key, `object`)
} else if (`object` is Boolean) {
editor.putBoolean(key, `object`)
} else if (`object` is Float) {
editor.putFloat(key, `object`)
} else if (`object` is Long) {
editor.putLong(key, `object`)
} else {
editor.putString(key, `object`.toString())
}
editor.commit()
}
fun getSharedPreference(key: String?, defaultObject: Any?): Any? {
return if (defaultObject is String) {
sharedPreferences.getString(key, defaultObject as String?)
} else if (defaultObject is Int) {
sharedPreferences.getInt(key, (defaultObject as Int?)!!)
} else if (defaultObject is Boolean) {
sharedPreferences.getBoolean(key, (defaultObject as Boolean?)!!)
} else if (defaultObject is Float) {
sharedPreferences.getFloat(key, (defaultObject as Float?)!!)
} else if (defaultObject is Long) {
sharedPreferences.getLong(key, (defaultObject as Long?)!!)
} else {
sharedPreferences.getString(key, null)
}
}
fun remove(key: String?) {
editor.remove(key)
editor.commit()
}
fun clear() {
editor.clear()
editor.commit()
}
fun contain(key: String?): Boolean {
return sharedPreferences.contains(key)
}
val all: Map<String, *>
get() = sharedPreferences.all
}

View File

@ -1,24 +0,0 @@
package com.jc.jcfw.util;
import android.os.Handler;
import android.os.Looper;
public class ThreadUtils {
/**
* check if current thread is main thread
*
* @return
*/
public static boolean isMainThread() {
return Looper.getMainLooper() == Looper.myLooper();
}
public static void runInMain(Runnable action) {
if (ThreadUtils.isMainThread()) {
action.run();
} else {
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(action);
}
}
}

View File

@ -0,0 +1,23 @@
package com.jc.jcfw.util
import android.os.Handler
import android.os.Looper
object ThreadUtils {
/**
* check if current thread is main thread
*
* @return
*/
private val isMainThread: Boolean
get() = Looper.getMainLooper() == Looper.myLooper()
fun runInMain(action: Runnable) {
if (isMainThread) {
action.run()
} else {
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(action)
}
}
}

View File

@ -1,36 +0,0 @@
package com.jc.jcfw.util;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.MainThread;
import com.ctf.games.release.dialog.QRCodeActivity;
public class UIUtils {
private static Toast toast;
@MainThread
public static void showToastReal(Context context,String text) {
if (toast == null) {
toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
} else {
toast.setDuration(Toast.LENGTH_SHORT);
toast.setText(text);
}
toast.show();
}
public static void showToast(Context context, String text) {
ThreadUtils.runInMain(() -> showToastReal(context, text));
}
@MainThread
public static void showQRCodeReal(Context context, String str, String title) {
QRCodeActivity qrCodeActivity = new QRCodeActivity(context);
qrCodeActivity.showQRCode(str, title);
qrCodeActivity.show();
}
public static void showQRCode(Context context, String str, String title) {
ThreadUtils.runInMain(() -> showQRCodeReal(context, str, title));
}
}

View File

@ -0,0 +1,35 @@
package com.jc.jcfw.util
import android.content.Context
import android.widget.Toast
import androidx.annotation.MainThread
import com.ctf.games.release.dialog.QRCodeActivity
object UIUtils {
private var toast: Toast? = null
@MainThread
fun showToastReal(context: Context?, text: String?) {
if (toast == null) {
toast = Toast.makeText(context, text, Toast.LENGTH_SHORT)
} else {
toast!!.duration = Toast.LENGTH_SHORT
toast!!.setText(text)
}
toast!!.show()
}
fun showToast(context: Context?, text: String?) {
ThreadUtils.runInMain { showToastReal(context, text) }
}
@MainThread
fun showQRCodeReal(context: Context?, str: String?, title: String?) {
val qrCodeActivity = QRCodeActivity(context!!)
qrCodeActivity.showQRCode(str, title)
qrCodeActivity.show()
}
fun showQRCode(context: Context?, str: String?, title: String?) {
ThreadUtils.runInMain { showQRCodeReal(context, str, title) }
}
}

View File

@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"
buildscript {
ext.kotlin_version = '1.7.20'
repositories {
google()
jcenter()
@ -15,6 +16,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.6'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name" translatable="false">CEBG</string>
<string name="app_name" translatable="false">CF</string>
<string name="game_view_content_description" translatable="false">Game view</string>
<string name="permission_camera" translatable="false">Scan QRCode need camera permission</string>
<string name="qr_code_title" translatable="false">QRCode</string>