This commit is contained in:
CounterFire2023 2023-09-06 11:38:24 +08:00
parent 442d2efa60
commit 576a6296d5
7 changed files with 229 additions and 139 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_6_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-09-01T11:09:07.848547Z" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,org.cocos2dx.okhttp3.Call,execute" />
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +1,5 @@
package com.cege.games.release;
import static net.openid.appauth.AuthorizationResponse.TOKEN_TYPE_BEARER;
import static org.cocos2dx.lib.Cocos2dxHelper.getActivity;
import android.Manifest;
@ -48,8 +47,6 @@ import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.Scopes;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Scope;
@ -94,7 +91,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -160,8 +156,7 @@ public class MainActivity extends UnityPlayerActivity
private FirebaseAnalytics mFirebaseAnalytics;
private AppEventsLogger fbLogger;
// store params for request permission or jump to other activity
private final Map<Integer, Map<String, String>> paramCache = Maps.newHashMap();
private WalletUtil mWalletUtil;
private Consumer<String> nextAction = null;
@ -212,6 +207,7 @@ public class MainActivity extends UnityPlayerActivity
PayClient payClient = PayClient.getInstance();
payClient.init(this);
mWalletUtil = new WalletUtil(this);
}
@Override
@ -252,20 +248,20 @@ public class MainActivity extends UnityPlayerActivity
shareToTikTok(funId, data.getData());
break;
case RC_REQUEST_DRIVE_TO_UPLOAD:
mExecutor.submit(this::saveToDriveAppFolder);
mExecutor.submit(() -> {mWalletUtil.uploadCfgWithGPS(_result -> {});});
break;
case RC_REQUEST_DRIVE_TO_READ:
mExecutor.submit(this::loadDrivePass);
mExecutor.submit(() -> {
mWalletUtil.downloadCfgWithGPS(_file -> {
mWalletUtil.getPassLocal();
});
});
break;
}
} else {
boolean next = false;
if (requestCode == RC_REQUEST_DRIVE_TO_UPLOAD || requestCode == RC_REQUEST_DRIVE_TO_READ) {
Map<String, String> params = paramCache.get(requestCode);
if (params != null) {
JcSDK.nativeCb(params.get("funid"), "activity result with code: " + resultCode, null);
paramCache.remove(requestCode);
}
mWalletUtil.errorCB("activity result with code: " + resultCode);
}
if (requestCode == REQUEST_CODE_SCAN && data != null) {
if (data.getBooleanExtra("localImg", false)) {
@ -574,7 +570,7 @@ public class MainActivity extends UnityPlayerActivity
mExecutor.execute(() -> {
Log.i(TAG, "Warming up browser instance for auth request");
Uri uri = mAuthRequest.get().toUri();
Log.i(TAG, "URI: " + uri);
Log.d(TAG, "URI: " + uri);
CustomTabsIntent.Builder intentBuilder = mAuthService.createCustomTabsIntentBuilder(uri);
mAuthIntent.set(intentBuilder.build());
mAuthIntentLatch.countDown();
@ -747,7 +743,7 @@ public class MainActivity extends UnityPlayerActivity
tiktokOpenApi.authorize(request);
}
public void showQRCode(String funid, String str, String title, String oid) {
public void showQRCode(String funID, String str, String title, String oid) {
runOnUiThread(() -> {
if (qrCodeActivity == null) {
qrCodeActivity = new QRCodeActivity(getContext());
@ -895,10 +891,6 @@ public class MainActivity extends UnityPlayerActivity
// startActivity(Intent.createChooser(shareIntent, "share"));
// });
}
/**
* 打开本地文件器
*/
private void openFileSelector() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("video/*");
@ -906,26 +898,12 @@ public class MainActivity extends UnityPlayerActivity
startActivityForResult(intent, FILE_SELECTOR_CODE);
}
public void passStorageState(String funid, String account) {
public void passStorageState(String funID, String account) {
Log.i(TAG, "passStorageState with: " + account);
}
@WorkerThread
private void saveToDriveAppFolder() {
Map<String, String> params = paramCache.get(RC_REQUEST_DRIVE_TO_UPLOAD);
if (params == null) {
return;
}
WalletUtil.saveToDriveAppFolder(params.get("funid"), params.get("account"), JcSDK::nativeCb);
paramCache.remove(RC_REQUEST_DRIVE_TO_UPLOAD);
}
private void loginAndRequestDrivePermission(String funid, String account, int requestCode) {
private void loginAndRequestDrivePermission(int requestCode) {
Log.i(TAG, "no drive permission");
Map<String, String> params = Maps.newHashMap();
params.put("funid", funid);
params.put("account", account);
paramCache.put(requestCode, params);
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id1))
.requestScopes(new Scope(Scopes.EMAIL), new Scope(DriveScopes.DRIVE_APPDATA))
@ -935,65 +913,57 @@ public class MainActivity extends UnityPlayerActivity
startActivityForResult(signInIntent, requestCode);
}
public void storagePass(String funid, String account, String password) {
Log.i(TAG, String.format("storagePass with: %s | %s", funid, account));
try {
WalletUtil.savePassToLocal(this, account, password);
} catch (JSONException | IOException e) {
Log.i(TAG, String.format("error storage pass to local, %s", e.getMessage()));
JcSDK.nativeCb(funid, "error storage pass to local", null);
return;
}
if (!GoogleSignIn.hasPermissions(
GoogleSignIn.getLastSignedInAccount(getActivity()),
new Scope(DriveScopes.DRIVE_APPDATA))) {
loginAndRequestDrivePermission(funid, account, RC_REQUEST_DRIVE_TO_UPLOAD);
} else {
Log.i(TAG, "had drive permission");
WalletUtil.saveToDriveAppFolder(funid, account, JcSDK::nativeCb);
}
public void storagePass(String funID, String account, String password) {
Log.i(TAG, String.format("storagePass with: %s | %s", funID, account));
mWalletUtil.updateParams(funID, account);
mWalletUtil.savePassToLocal(password, _file -> {
if (isGooglePlayServicesAvailable()) {
if (!GoogleSignIn.hasPermissions(
GoogleSignIn.getLastSignedInAccount(getActivity()),
new Scope(DriveScopes.DRIVE_APPDATA))) {
loginAndRequestDrivePermission(RC_REQUEST_DRIVE_TO_UPLOAD);
} else {
Log.i(TAG, "had drive permission");
mWalletUtil.uploadCfgWithGPS(_result -> {});
}
} else {
AuthState state = mAuthStateManager.getCurrent();
mWalletUtil.uploadCfgWithApi(state.getAccessToken(), _result->{});
}
});
// runOnUiThread(() -> {
// Intent intent = new Intent(this, BiometricActivity.class);
// intent.putExtra("action", "encrypt");
// intent.putExtra("funid", funId);
// intent.putExtra("funID", funId);
// intent.putExtra("account", account);
// intent.putExtra("password", password);
// startActivity(intent);
// });
}
@WorkerThread
private void loadDrivePass() {
Map<String, String> params = paramCache.get(RC_REQUEST_DRIVE_TO_READ);
if (params == null) {
return;
}
WalletUtil.downloadCfgToLocal(params.get("funid"), params.get("account"));
WalletUtil.getPassLocal(this, params.get("funid"), params.get("account"));
paramCache.remove(RC_REQUEST_DRIVE_TO_READ);
}
public void authGetStoragePass(String funid, String account) {
public void authGetStoragePass(String funID, String account) {
Log.i(TAG, "authGetStoragePass with: " + account);
mWalletUtil.updateParams(funID, account);
if (isGooglePlayServicesAvailable()) {
if (!WalletUtil.localCfgExists(this, account)) {
if (!mWalletUtil.localCfgExists(account)) {
if (!GoogleSignIn.hasPermissions(
GoogleSignIn.getLastSignedInAccount(getActivity()),
new Scope(DriveScopes.DRIVE_APPDATA))) {
loginAndRequestDrivePermission(funid, account, RC_REQUEST_DRIVE_TO_READ);
loginAndRequestDrivePermission(RC_REQUEST_DRIVE_TO_READ);
} else {
WalletUtil.downloadCfgToLocal(funid, account);
WalletUtil.getPassLocal(this, funid, account);
mWalletUtil.downloadCfgWithGPS(_file -> {
mWalletUtil.getPassLocal();
});
}
} else {
WalletUtil.getPassLocal(this, funid, account);
mWalletUtil.getPassLocal();
}
} else {
if (!WalletUtil.localCfgExists(this, account)) {
if (!mWalletUtil.localCfgExists(account)) {
AuthState state = mAuthStateManager.getCurrent();
WalletUtil.downloadCfgWithApi(funid, account, state.getAccessToken());
WalletUtil.getPassLocal(this, funid, account);
mWalletUtil.downloadCfgWithApi(state.getAccessToken(), _file -> {
mWalletUtil.getPassLocal();
});
// TODO::
// 1. check whether the google account has been logged in, this situation occurs
// when non-google login
@ -1007,28 +977,28 @@ public class MainActivity extends UnityPlayerActivity
// this::handleCodeExchangeResponse);
// } else {
// Log.d(TAG, "access token no need refresh");
// WalletUtil.downloadCfgToLocal(funid, account);
// WalletUtil.getPassLocal(this, funid, account);
// WalletUtil.downloadCfgToLocal(funID, account);
// WalletUtil.getPassLocal(this, funID, account);
// }
// } else {
// Log.w(TAG, "not login");
// mExecutor.submit(this::doAuth);
// }
} else {
WalletUtil.getPassLocal(this, funid, account);
mWalletUtil.getPassLocal();
}
}
// runOnUiThread(() -> {
// Intent intent = new Intent(this, BiometricActivity.class);
// intent.putExtra("action", "decrypt");
// intent.putExtra("funid", funId);
// intent.putExtra("funID", funId);
// intent.putExtra("account", account);
// startActivity(intent);
// });
}
public void getClientId(String funid) {
public void getClientId(String funID) {
Log.i(TAG, "getClientId ");
}
}

View File

@ -18,6 +18,7 @@ import android.webkit.WebViewClient;
import com.cege.games.release.R;
import com.cege.games.release.dialog.BaseDialog;
import com.jc.jcfw.jsinterface.WalletInterface;
public class WebPageActivity extends Activity {
private WebView mWebView;
@ -46,6 +47,7 @@ public class WebPageActivity extends Activity {
String url = intent.getStringExtra("url");
// show web view
mWebView.loadUrl(url);
mWebView.addJavascriptInterface(new WalletInterface(this), "wallet");
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onReceivedTitle(WebView view, String title) {

View File

@ -3,11 +3,9 @@ package com.cege.games.release.wallet;
import android.content.Context;
import android.util.Log;
import com.cege.games.release.MainActivity;
import com.cege.games.release.R;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.common.api.Scope;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.drive.Drive;
@ -15,7 +13,6 @@ import com.google.api.services.drive.DriveScopes;
import com.google.common.base.Strings;
import com.jc.jcfw.JcSDK;
import com.jc.jcfw.NativeResult;
import com.jc.jcfw.security.BiometricResult;
import com.jc.jcfw.util.DriveUtils;
import com.jc.jcfw.util.DriverApiUtils;
import com.jc.jcfw.util.FileUtils;
@ -29,65 +26,97 @@ import java.util.Collections;
import java.util.function.Consumer;
public class WalletUtil {
private static final String TAG = WalletUtil.class.getSimpleName();
private final Context mContext;
private String funID;
private String account;
public WalletUtil(Context context) {
this.mContext = context;
}
private final String TAG = getClass().getSimpleName();
public void updateParams(String funID, String account) {
this.funID = funID;
this.account = account;
}
public void clearParams() {
this.funID = null;
this.account = null;
}
public String getAccount() {
return account;
}
public void errorCB(String msg) {
JcSDK.nativeCb(funID, msg, null);
clearParams();
}
public void successCB(String dataStr) {
JcSDK.nativeCb(funID, null, dataStr);
clearParams();
}
public static String generateFileName(String account) {
return String.format("wallet_%s.json", account);
}
public static File getWalletCfgFile(Context context, String account) {
public File getWalletCfgFile(String account) {
String filename = generateFileName(account);
File dic = new File(context.getFilesDir() + "/wallets");
File dic = new File(mContext.getFilesDir() + "/wallets");
if (!dic.exists()) {
dic.mkdir();
}
return new File(context.getFilesDir() + "/wallets", filename);
return new File(mContext.getFilesDir() + "/wallets", filename);
}
public static String savePassToLocal(Context context, String account, String pass) throws IOException, JSONException {
File filePath = getWalletCfgFile(context, account);
public void savePassToLocal(String pass, Consumer<File> func) {
File filePath = getWalletCfgFile(account);
JSONObject content = new JSONObject();
content.put("pass", pass);
FileUtils.writeFile(filePath, content.toString());
return filePath.getAbsolutePath();
try {
content.put("pass", pass);
FileUtils.writeFile(filePath, content.toString());
func.accept(filePath);
} catch (JSONException | IOException e) {
errorCB(e.getMessage());
}
}
public static void getPassLocal(Context context, String funId, String account) {
public void getPassLocal() {
try {
JSONObject json = loadLocalCfg(context, account);
JSONObject json = loadLocalCfg(account);
String passEncrypted = json.getString("pass");
String passDecrypted = JcSDK.decryptPass(account, passEncrypted);
JcSDK.nativeCb(funId, null, passDecrypted);
} catch (JSONException e) {
JcSDK.nativeCb(funId, "error decode json", null);
} catch (IOException e) {
JcSDK.nativeCb(funId, "error read cfg file", null);
successCB(passDecrypted);
} catch (JSONException | IOException e) {
errorCB(e.getMessage());
}
}
public static boolean localCfgExists(Context context, String account) {
File filePath = getWalletCfgFile(context, account);
public boolean localCfgExists(String account) {
File filePath = getWalletCfgFile(account);
return filePath.exists();
}
public static JSONObject loadLocalCfg(Context context, String account) throws JSONException, IOException {
File filePath = getWalletCfgFile(context, account);
public JSONObject loadLocalCfg(String account) throws JSONException, IOException {
File filePath = getWalletCfgFile(account);
if (!filePath.exists()) {
return null;
}
return FileUtils.readJsonFromFile(filePath);
}
public static void saveToDriveAppFolder(String funid, String account, Consumer<NativeResult> func) {
Context context = MainActivity.app;
GoogleSignInAccount ga = GoogleSignIn.getLastSignedInAccount(context);
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context,
public void uploadCfgWithGPS(Consumer<NativeResult> func) {
GoogleSignInAccount ga = GoogleSignIn.getLastSignedInAccount(mContext);
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(mContext,
Collections.singletonList(DriveScopes.DRIVE_APPDATA));
credential.setSelectedAccount(ga.getAccount());
Drive service = DriveUtils.generateService(credential, context.getString(R.string.app_name));
Drive service = DriveUtils.generateService(credential, mContext.getString(R.string.app_name));
try {
File file = getWalletCfgFile(MainActivity.app, account);
File file = getWalletCfgFile(account);
String fileName = file.getName();
String fileId = DriveUtils.queryOneAppFile(service, fileName);
if (Strings.isNullOrEmpty(fileId)) {
@ -95,59 +124,99 @@ public class WalletUtil {
fileId = DriveUtils.uploadAppFile(service, file, "application/json");
}
Log.i(TAG, "File ID: " + fileId);
func.accept(new NativeResult(funid, null, fileId));
func.accept(new NativeResult(funID, null, fileId));
successCB(fileId);
} catch (GoogleJsonResponseException e) {
Log.i(TAG, "Unable to create file: " + e.getDetails());
func.accept(new NativeResult(funid, e.getMessage(), null));
func.accept(new NativeResult(funID, e.getMessage(), null));
errorCB("Unable to create file: " +e.getMessage());
} catch (IOException e) {
Log.i(TAG, e.getMessage());
func.accept(new NativeResult(funid, e.getMessage(), null));
func.accept(new NativeResult(funID, e.getMessage(), null));
errorCB("Unable to create file: " + e.getMessage());
}
}
public static void downloadCfgToLocal(String funid, String account) {
Context context = MainActivity.app;
GoogleSignInAccount ga = GoogleSignIn.getLastSignedInAccount(context);
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context,
public void downloadCfgWithGPS(Consumer<File> func) {
GoogleSignInAccount ga = GoogleSignIn.getLastSignedInAccount(mContext);
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(mContext,
Collections.singletonList(DriveScopes.DRIVE_APPDATA));
credential.setSelectedAccount(ga.getAccount());
Drive service = DriveUtils.generateService(credential, context.getString(R.string.app_name));
Drive service = DriveUtils.generateService(credential, mContext.getString(R.string.app_name));
String fileName = generateFileName(account);
String fileId = DriveUtils.queryOneAppFile(service, fileName);
if (Strings.isNullOrEmpty(fileId)) {
Log.i(TAG, "file not found in drive");
errorCB("file not found in drive");
return;
}
boolean downloadSuccess = false;
int count = 0;
File fileLocal = getWalletCfgFile(account);
while (!downloadSuccess && count++ < 10) {
Log.i(TAG, String.format("download file: %s with GPS, try: %d", fileName, count));
try {
String jsonStr = DriveUtils.downloadFile(service, fileId);
File fileLocal = getWalletCfgFile(context, account);
FileUtils.writeFile(fileLocal, jsonStr);
downloadSuccess = true;
} catch (IOException e) {
Log.i(TAG, "error download file");
Log.i(TAG, "error download file with GPS");
}
}
if (downloadSuccess) {
func.accept(fileLocal);
} else {
errorCB("error download file with GPS");
}
}
public void downloadCfgWithApi(String accessToken, Consumer<File> func) {
DriverApiUtils api = new DriverApiUtils(accessToken);
String fileName = generateFileName(account);
File fileLocal = getWalletCfgFile(account);
boolean downloadSuccess = false;
int count = 0;
while (!downloadSuccess && count++ < 10) {
Log.i(TAG, String.format("download file: %s with api, try: %d", fileName, count));
try {
String fileId = api.queryOneAppFile(fileName);
if (Strings.isNullOrEmpty(fileId)) {
Log.i(TAG, "file not found in drive");
throw new IOException();
}
String jsonStr = api.fileInfo(fileId);
FileUtils.writeFile(fileLocal, jsonStr);
downloadSuccess = true;
} catch (IOException | JSONException e) {
Log.i(TAG, "error download file with api");
}
}
if (downloadSuccess) {
func.accept(fileLocal);
} else {
errorCB("error download file with api");
}
}
public static void downloadCfgWithApi(String funid, String account, String accesToken) {
Context context = MainActivity.app;
DriverApiUtils api = new DriverApiUtils(accesToken);
String fileName = generateFileName(account);
public void uploadCfgWithApi(String accessToken, Consumer<NativeResult> func) {
DriverApiUtils api = new DriverApiUtils(accessToken);
File fileLocal = getWalletCfgFile(account);
try {
String fileId = api.queryOneAppFile(fileName);
String fileName = generateFileName(account);
String fileId = api.queryOneAppFile(fileName);
if (Strings.isNullOrEmpty(fileId)) {
Log.i(TAG, "file not found in drive");
return;
String rep = api.uploadFile(fileLocal, "application/json");
JSONObject repData = new JSONObject(rep);
fileId = repData.getString("id");
}
String jsonStr = api.fileInfo(fileId);
File fileLocal = getWalletCfgFile(context, account);
FileUtils.writeFile(fileLocal, jsonStr);
Log.i(TAG, "success upload file with api, ID: " + fileId);
func.accept(new NativeResult(funID, null, fileId));
successCB(fileId);
} catch (IOException | JSONException e) {
Log.i(TAG, "error download file");
func.accept(new NativeResult(funID, e.getMessage(), null));
errorCB("error upload file with api: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,23 @@
package com.jc.jcfw.jsinterface;
import android.content.Context;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
public class WalletInterface {
Context mContext;
/** Instantiate the interface and set the context */
public WalletInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
// sign
}

View File

@ -18,6 +18,7 @@ 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 = "";
@ -76,24 +77,24 @@ public class DriverApiUtils {
}
}
public void createFile() throws IOException {
public String uploadFile(java.io.File filePath, String fileType) throws IOException {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
String fileName = filePath.getName();
String optionStr = "{\"name\":\""+fileName+"\",\"parents\":[\"appDataFolder\"]}";
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("", "upload-options.json",
RequestBody.create(MediaType.parse("application/octet-stream"),
new File("/Users/zhl/Desktop/drivetest/files/upload-options.json")))
.addFormDataPart("", "wallet_1.json",
RequestBody.create(MediaType.parse("application/octet-stream"),
new File("/Users/zhl/Desktop/drivetest/files/wallet_1.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(driveApiBase + "?uploadType=multipart")
.url(driveApiUploadBase + "?uploadType=multipart")
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Content-Type", fileType)
.addHeader("Authorization", "Bearer " + token)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
}