mirror of
https://github.com/nextcloud/android.git
synced 2024-11-26 07:05:49 +03:00
Improve push / remove push registration
This commit is contained in:
parent
c9a53cb042
commit
7f1739f5f7
9 changed files with 237 additions and 67 deletions
|
@ -197,8 +197,8 @@ dependencies {
|
|||
compile 'org.greenrobot:eventbus:3.0.0'
|
||||
compile 'com.googlecode.ez-vcard:ez-vcard:0.10.2'
|
||||
|
||||
// uncomment for gplay, modified
|
||||
// compile 'com.google.android.gms:play-services:10.2.1'
|
||||
// uncomment for gplay, modified
|
||||
//compile 'com.google.android.gms:play-services:10.2.4'
|
||||
|
||||
compile 'org.parceler:parceler-api:1.1.6'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.6'
|
||||
|
|
|
@ -36,8 +36,6 @@ public class NCFirebaseInstanceIDService extends FirebaseInstanceIdService {
|
|||
//You can implement this method to store the token on your server
|
||||
if (!TextUtils.isEmpty(getResources().getString(R.string.push_server_url))) {
|
||||
PreferenceManager.setPushToken(MainApp.getAppContext(), FirebaseInstanceId.getInstance().getToken());
|
||||
PreferenceManager.setPushTokenUpdateTime(MainApp.getAppContext(), System.currentTimeMillis());
|
||||
|
||||
PushUtils.pushRegistrationToServer();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,12 @@
|
|||
|
||||
package com.owncloud.android.ui.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.owncloud.android.ui.events.TokenPushEvent;
|
||||
import com.owncloud.android.utils.PushUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
|
@ -33,4 +36,11 @@ public class ModifiedFileDisplayActivity extends FileDisplayActivity {
|
|||
PushUtils.pushRegistrationToServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// see if there's stuff to push every time we start the app
|
||||
EventBus.getDefault().post(new TokenPushEvent());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,9 +27,12 @@ import android.content.Context;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.authentication.AccountUtils;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.PushArbitraryData;
|
||||
import com.owncloud.android.db.PreferenceManager;
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
||||
|
@ -39,8 +42,12 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForNotificationsOperation;
|
||||
import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForProxyOperation;
|
||||
import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForNotificationsOperation;
|
||||
import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForProxyOperation;
|
||||
import com.owncloud.android.lib.resources.notifications.models.PushResponse;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
@ -65,7 +72,11 @@ public class PushUtils {
|
|||
private static final String KEYPAIR_PRIV_EXTENSION = ".priv";
|
||||
private static final String KEYPAIR_PUB_EXTENSION = ".pub";
|
||||
|
||||
public static String generateSHA512Hash(String pushToken) {
|
||||
public static final String KEY_PUSH = "push";
|
||||
|
||||
private static ArbitraryDataProvider arbitraryDataProvider;
|
||||
|
||||
private static String generateSHA512Hash(String pushToken) {
|
||||
MessageDigest messageDigest = null;
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-512");
|
||||
|
@ -86,7 +97,7 @@ public class PushUtils {
|
|||
return result.toString();
|
||||
}
|
||||
|
||||
public static int generateRsa2048KeyPair() {
|
||||
private static int generateRsa2048KeyPair() {
|
||||
String keyPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder() + File.separator
|
||||
+ KEYPAIR_FOLDER;
|
||||
|
||||
|
@ -124,8 +135,58 @@ public class PushUtils {
|
|||
return -2;
|
||||
}
|
||||
|
||||
private static void deleteRegistrationForAccount(Account account) {
|
||||
Context context = MainApp.getAppContext();
|
||||
OwnCloudAccount ocAccount = null;
|
||||
arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().getContentResolver());
|
||||
|
||||
try {
|
||||
ocAccount = new OwnCloudAccount(account, context);
|
||||
OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
|
||||
getClientFor(ocAccount, context);
|
||||
|
||||
RemoteOperation unregisterAccountDeviceForNotificationsOperation = new
|
||||
UnregisterAccountDeviceForNotificationsOperation();
|
||||
|
||||
RemoteOperationResult remoteOperationResult = unregisterAccountDeviceForNotificationsOperation.
|
||||
execute(mClient);
|
||||
|
||||
if (remoteOperationResult.getHttpCode() == HttpStatus.SC_ACCEPTED) {
|
||||
String arbitraryValue;
|
||||
if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account, KEY_PUSH))) {
|
||||
Gson gson = new Gson();
|
||||
PushArbitraryData pushArbitraryData = gson.fromJson(arbitraryValue, PushArbitraryData.class);
|
||||
RemoteOperation unregisterAccountDeviceForProxyOperation =
|
||||
new UnregisterAccountDeviceForProxyOperation(context.getResources().
|
||||
getString(R.string.push_server_url),
|
||||
pushArbitraryData.getDeviceIdentifier(),
|
||||
pushArbitraryData.getDeviceIdentifierSignature(),
|
||||
pushArbitraryData.getUserPublicKey());
|
||||
|
||||
remoteOperationResult = unregisterAccountDeviceForProxyOperation.execute(mClient);
|
||||
|
||||
if (remoteOperationResult.isSuccess()) {
|
||||
arbitraryDataProvider.deleteKeyForAccount(account, KEY_PUSH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
|
||||
Log_OC.d(TAG, "Failed to find an account");
|
||||
} catch (AuthenticatorException e) {
|
||||
Log_OC.d(TAG, "Failed via AuthenticatorException");
|
||||
} catch (IOException e) {
|
||||
Log_OC.d(TAG, "Failed via IOException");
|
||||
} catch (OperationCanceledException e) {
|
||||
Log_OC.d(TAG, "Failed via OperationCanceledException");
|
||||
}
|
||||
}
|
||||
|
||||
public static void pushRegistrationToServer() {
|
||||
String token = PreferenceManager.getPushToken(MainApp.getAppContext());
|
||||
arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().getContentResolver());
|
||||
|
||||
if (!TextUtils.isEmpty(MainApp.getAppContext().getResources().getString(R.string.push_server_url)) &&
|
||||
!TextUtils.isEmpty(token)) {
|
||||
PushUtils.generateRsa2048KeyPair();
|
||||
|
@ -139,42 +200,56 @@ public class PushUtils {
|
|||
publicKey = "-----BEGIN PUBLIC KEY-----\n" + publicKey + "\n-----END PUBLIC KEY-----\n";
|
||||
|
||||
Context context = MainApp.getAppContext();
|
||||
String providerValue;
|
||||
Gson gson = new Gson();
|
||||
for (Account account : AccountUtils.getAccounts(context)) {
|
||||
try {
|
||||
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
|
||||
OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
|
||||
getClientFor(ocAccount, context);
|
||||
if (!TextUtils.isEmpty(providerValue = arbitraryDataProvider.getValue(account, KEY_PUSH))) {
|
||||
PushArbitraryData accountPushData = gson.fromJson(providerValue, PushArbitraryData.class);
|
||||
if (!accountPushData.getPushToken().equals(token) && !accountPushData.isShouldBeDeleted()) {
|
||||
try {
|
||||
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
|
||||
OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
|
||||
getClientFor(ocAccount, context);
|
||||
|
||||
RemoteOperation registerAccountDeviceForNotificationsOperation =
|
||||
new RegisterAccountDeviceForNotificationsOperation(pushTokenHash,
|
||||
publicKey,
|
||||
context.getResources().getString(R.string.push_server_url));
|
||||
RemoteOperation registerAccountDeviceForNotificationsOperation =
|
||||
new RegisterAccountDeviceForNotificationsOperation(pushTokenHash,
|
||||
publicKey,
|
||||
context.getResources().getString(R.string.push_server_url));
|
||||
|
||||
RemoteOperationResult remoteOperationResult = registerAccountDeviceForNotificationsOperation.
|
||||
execute(mClient);
|
||||
RemoteOperationResult remoteOperationResult = registerAccountDeviceForNotificationsOperation.
|
||||
execute(mClient);
|
||||
|
||||
if (remoteOperationResult.isSuccess()) {
|
||||
PushResponse pushResponse = remoteOperationResult.getPushResponseData();
|
||||
if (remoteOperationResult.isSuccess()) {
|
||||
PushResponse pushResponse = remoteOperationResult.getPushResponseData();
|
||||
|
||||
RemoteOperation registerAccountDeviceForProxyOperation = new
|
||||
RegisterAccountDeviceForProxyOperation(
|
||||
context.getResources().getString(R.string.push_server_url),
|
||||
token, pushResponse.getDeviceIdentifier(), pushResponse.getSignature(),
|
||||
pushResponse.getPublicKey());
|
||||
RemoteOperation registerAccountDeviceForProxyOperation = new
|
||||
RegisterAccountDeviceForProxyOperation(
|
||||
context.getResources().getString(R.string.push_server_url),
|
||||
token, pushResponse.getDeviceIdentifier(), pushResponse.getSignature(),
|
||||
pushResponse.getPublicKey());
|
||||
|
||||
remoteOperationResult = registerAccountDeviceForProxyOperation.execute(mClient);
|
||||
PreferenceManager.setPushTokenLastSentTime(MainApp.getAppContext(),
|
||||
System.currentTimeMillis());
|
||||
remoteOperationResult = registerAccountDeviceForProxyOperation.execute(mClient);
|
||||
|
||||
if (remoteOperationResult.isSuccess()) {
|
||||
PushArbitraryData pushArbitraryData = new PushArbitraryData(token,
|
||||
pushResponse.getDeviceIdentifier(), pushResponse.getSignature(),
|
||||
pushResponse.getPublicKey(), false);
|
||||
arbitraryDataProvider.storeOrUpdateKeyValue(account, KEY_PUSH,
|
||||
gson.toJson(pushArbitraryData));
|
||||
}
|
||||
}
|
||||
} catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
|
||||
Log_OC.d(TAG, "Failed to find an account");
|
||||
} catch (AuthenticatorException e) {
|
||||
Log_OC.d(TAG, "Failed via AuthenticatorException");
|
||||
} catch (IOException e) {
|
||||
Log_OC.d(TAG, "Failed via IOException");
|
||||
} catch (OperationCanceledException e) {
|
||||
Log_OC.d(TAG, "Failed via OperationCanceledException");
|
||||
}
|
||||
} else if (accountPushData.isShouldBeDeleted()) {
|
||||
deleteRegistrationForAccount(account);
|
||||
}
|
||||
} catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
|
||||
Log_OC.d(TAG, "Failed to find an account");
|
||||
} catch (AuthenticatorException e) {
|
||||
Log_OC.d(TAG, "Failed via AuthenticatorException");
|
||||
} catch (IOException e) {
|
||||
Log_OC.d(TAG, "Failed via IOException");
|
||||
} catch (OperationCanceledException e) {
|
||||
Log_OC.d(TAG, "Failed via OperationCanceledException");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,9 +47,19 @@ public class ArbitraryDataProvider {
|
|||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
public int deleteKeyForAccount(Account account, String key) {
|
||||
int result = contentResolver.delete(
|
||||
ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
|
||||
ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? AND " +
|
||||
ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + "= ?",
|
||||
new String[]{account.name, key}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public void storeOrUpdateKeyValue(Account account, String key, String newValue) {
|
||||
|
||||
|
||||
ArbitraryDataSet data = getArbitraryDataSet(account, key);
|
||||
if (data == null) {
|
||||
Log_OC.v(TAG, "Adding arbitrary data with cloud id: " + account.name + " key: " + key
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017 Mario Danic
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.datamodel;
|
||||
|
||||
public class PushArbitraryData {
|
||||
public String pushToken;
|
||||
public String deviceIdentifier;
|
||||
public String deviceIdentifierSignature;
|
||||
public String userPublicKey;
|
||||
public boolean shouldBeDeleted;
|
||||
|
||||
public PushArbitraryData() {
|
||||
}
|
||||
|
||||
public PushArbitraryData(String pushToken, String deviceIdentifier, String deviceIdentifierSignature,
|
||||
String userPublicKey, boolean shouldBeDeleted) {
|
||||
this.pushToken = pushToken;
|
||||
this.deviceIdentifier = deviceIdentifier;
|
||||
this.deviceIdentifierSignature = deviceIdentifierSignature;
|
||||
this.userPublicKey = userPublicKey;
|
||||
this.shouldBeDeleted = shouldBeDeleted;
|
||||
}
|
||||
|
||||
public String getPushToken() {
|
||||
return pushToken;
|
||||
}
|
||||
|
||||
public void setPushToken(String pushToken) {
|
||||
this.pushToken = pushToken;
|
||||
}
|
||||
|
||||
public String getDeviceIdentifier() {
|
||||
return deviceIdentifier;
|
||||
}
|
||||
|
||||
public void setDeviceIdentifier(String deviceIdentifier) {
|
||||
this.deviceIdentifier = deviceIdentifier;
|
||||
}
|
||||
|
||||
public String getDeviceIdentifierSignature() {
|
||||
return deviceIdentifierSignature;
|
||||
}
|
||||
|
||||
public void setDeviceIdentifierSignature(String deviceIdentifierSignature) {
|
||||
this.deviceIdentifierSignature = deviceIdentifierSignature;
|
||||
}
|
||||
|
||||
public String getUserPublicKey() {
|
||||
return userPublicKey;
|
||||
}
|
||||
|
||||
public void setUserPublicKey(String userPublicKey) {
|
||||
this.userPublicKey = userPublicKey;
|
||||
}
|
||||
|
||||
public boolean isShouldBeDeleted() {
|
||||
return shouldBeDeleted;
|
||||
}
|
||||
|
||||
public void setShouldBeDeleted(boolean shouldBeDeleted) {
|
||||
this.shouldBeDeleted = shouldBeDeleted;
|
||||
}
|
||||
}
|
|
@ -47,33 +47,15 @@ public abstract class PreferenceManager {
|
|||
private static final String PREF__LEGACY_CLEAN = "legacyClean";
|
||||
private static final String PREF__AUTO_UPLOAD_UPDATE_PATH = "autoUploadPathUpdate";
|
||||
private static final String PREF__PUSH_TOKEN = "pushToken";
|
||||
private static final String PREF__PUSH_TOKEN_UPDATE_TIME = "pushTokenLastUpdated";
|
||||
private static final String PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME = "pushTokenLastSent";
|
||||
|
||||
public static void setPushTokenLastSentTime(Context context, long time) {
|
||||
saveLongPreference(context, PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME, time);
|
||||
}
|
||||
|
||||
public static long getPushTokenLastSentTime(Context context) {
|
||||
return getDefaultSharedPreferences(context).getLong(PREF__PUSH_TOKEN_LAST_REGISTRATION_TIME, -1);
|
||||
}
|
||||
|
||||
public static void setPushToken(Context context, String pushToken) {
|
||||
saveStringPreference(context, PREF__PUSH_TOKEN, pushToken);
|
||||
saveStringPreferenceNow(context, PREF__PUSH_TOKEN, pushToken);
|
||||
}
|
||||
|
||||
public static String getPushToken(Context context) {
|
||||
return getDefaultSharedPreferences(context).getString(PREF__PUSH_TOKEN, "");
|
||||
}
|
||||
|
||||
public static void setPushTokenUpdateTime(Context context, long time) {
|
||||
saveLongPreference(context, PREF__PUSH_TOKEN_UPDATE_TIME, time);
|
||||
}
|
||||
|
||||
public static long getPushTokenUpdateTime(Context context) {
|
||||
return getDefaultSharedPreferences(context).getLong(PREF__PUSH_TOKEN_UPDATE_TIME, -1);
|
||||
}
|
||||
|
||||
public static boolean instantPictureUploadEnabled(Context context) {
|
||||
return getDefaultSharedPreferences(context).getBoolean(PREF__INSTANT_UPLOADING, false);
|
||||
}
|
||||
|
@ -289,6 +271,12 @@ public abstract class PreferenceManager {
|
|||
appPreferences.apply();
|
||||
}
|
||||
|
||||
private static void saveStringPreferenceNow(Context context, String key, String value) {
|
||||
SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
|
||||
appPreferences.putString(key, value);
|
||||
appPreferences.commit();
|
||||
}
|
||||
|
||||
private static void saveIntPreference(Context context, String key, int value) {
|
||||
SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
|
||||
appPreferences.putInt(key, value);
|
||||
|
|
|
@ -162,8 +162,6 @@ public abstract class FileActivity extends DrawerActivity
|
|||
|
||||
setAccount(account, savedInstanceState != null);
|
||||
|
||||
checkContactsBackupJob();
|
||||
|
||||
mOperationsServiceConnection = new OperationsServiceConnection();
|
||||
bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
|
@ -243,16 +241,6 @@ public abstract class FileActivity extends DrawerActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void checkContactsBackupJob() {
|
||||
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
|
||||
|
||||
if (getAccount() != null && arbitraryDataProvider.getBooleanValue(getAccount(),
|
||||
ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP)) {
|
||||
ContactsPreferenceActivity.startContactBackupJob(getAccount());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Getter for the main {@link OCFile} handled by the activity.
|
||||
*
|
||||
|
|
|
@ -47,17 +47,22 @@ import android.widget.LinearLayout;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.authentication.AccountUtils;
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.PushArbitraryData;
|
||||
import com.owncloud.android.lib.common.UserInfo;
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation;
|
||||
import com.owncloud.android.ui.events.TokenPushEvent;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.PushUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import butterknife.BindString;
|
||||
|
@ -349,6 +354,21 @@ public class UserInfoActivity extends FileActivity {
|
|||
ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
|
||||
"false");
|
||||
|
||||
|
||||
String arbitraryDataPushString;
|
||||
|
||||
if (!TextUtils.isEmpty(arbitraryDataPushString = arbitraryDataProvider.getValue(
|
||||
account, PushUtils.KEY_PUSH))) {
|
||||
Gson gson = new Gson();
|
||||
PushArbitraryData pushArbitraryData = gson.fromJson(arbitraryDataPushString,
|
||||
PushArbitraryData.class);
|
||||
pushArbitraryData.setShouldBeDeleted(true);
|
||||
arbitraryDataProvider.storeOrUpdateKeyValue(account, PushUtils.KEY_PUSH,
|
||||
gson.toJson(pushArbitraryData));
|
||||
EventBus.getDefault().post(new TokenPushEvent());
|
||||
}
|
||||
|
||||
|
||||
if (getActivity() != null && !removeDirectly) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(KEY_ACCOUNT, Parcels.wrap(account));
|
||||
|
|
Loading…
Reference in a new issue