extract autocrypt_peers from KeychainProvider into AutocryptPeerDao
This commit is contained in:
@@ -2,52 +2,64 @@ package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate;
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus;
|
||||
import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDao;
|
||||
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
class AutocryptInteractor {
|
||||
public class AutocryptInteractor {
|
||||
private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS;
|
||||
|
||||
private AutocryptPeerDataAccessObject autocryptPeerDao;
|
||||
private AutocryptPeerDao autocryptPeerDao;
|
||||
private KeyWritableRepository keyWritableRepository;
|
||||
|
||||
public static AutocryptInteractor getInstance(Context context, AutocryptPeerDataAccessObject autocryptPeerentityDao) {
|
||||
private final String packageName;
|
||||
|
||||
public static AutocryptInteractor getInstance(Context context, String packageName) {
|
||||
AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context);
|
||||
KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(context);
|
||||
|
||||
return new AutocryptInteractor(autocryptPeerentityDao, keyWritableRepository);
|
||||
return new AutocryptInteractor(autocryptPeerDao, keyWritableRepository, packageName);
|
||||
}
|
||||
|
||||
private AutocryptInteractor(AutocryptPeerDataAccessObject autocryptPeerDao,
|
||||
KeyWritableRepository keyWritableRepository) {
|
||||
private AutocryptInteractor(AutocryptPeerDao autocryptPeerDao,
|
||||
KeyWritableRepository keyWritableRepository, String packageName) {
|
||||
this.autocryptPeerDao = autocryptPeerDao;
|
||||
this.keyWritableRepository = keyWritableRepository;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
void updateAutocryptPeerState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) {
|
||||
AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId);
|
||||
Date effectiveDate = autocryptPeerUpdate.getEffectiveDate();
|
||||
|
||||
// 1. If the message’s effective date is older than the peers[from-addr].autocrypt_timestamp value, then no changes are required, and the update process terminates.
|
||||
Date lastSeenAutocrypt = autocryptPeerDao.getLastSeenKey(autocryptPeerId);
|
||||
if (lastSeenAutocrypt != null && effectiveDate.compareTo(lastSeenAutocrypt) <= 0) {
|
||||
Date lastSeenKey = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen_key() : null;
|
||||
if (lastSeenKey != null && effectiveDate.compareTo(lastSeenKey) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If the message’s effective date is more recent than peers[from-addr].last_seen then set peers[from-addr].last_seen to the message’s effective date.
|
||||
Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId);
|
||||
Date lastSeen = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen() : null;
|
||||
if (lastSeen == null || effectiveDate.after(lastSeen)) {
|
||||
autocryptPeerDao.updateLastSeen(autocryptPeerId, effectiveDate);
|
||||
autocryptPeerDao.insertOrUpdateLastSeen(packageName, autocryptPeerId, effectiveDate);
|
||||
}
|
||||
|
||||
// 3. If the Autocrypt header is unavailable, no further changes are required and the update process terminates.
|
||||
@@ -66,17 +78,18 @@ class AutocryptInteractor {
|
||||
// 6. Set peers[from-addr].prefer_encrypt to the corresponding prefer-encrypt value of the Autocrypt header.
|
||||
boolean isMutual = autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL;
|
||||
|
||||
autocryptPeerDao.updateKey(autocryptPeerId, effectiveDate, newMasterKeyId, isMutual);
|
||||
autocryptPeerDao.updateKey(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, isMutual);
|
||||
}
|
||||
|
||||
void updateAutocryptPeerGossipState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) {
|
||||
AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId);
|
||||
Date effectiveDate = autocryptPeerUpdate.getEffectiveDate();
|
||||
|
||||
// 1. If gossip-addr does not match any recipient in the mail’s To or Cc header, the update process terminates (i.e., header is ignored).
|
||||
// -> This should be taken care of in the mail client that sends us this data!
|
||||
|
||||
// 2. If peers[gossip-addr].gossip_timestamp is more recent than the message’s effective date, then the update process terminates.
|
||||
Date lastSeenGossip = autocryptPeerDao.getLastSeenGossip(autocryptPeerId);
|
||||
Date lastSeenGossip = currentAutocryptPeer.gossip_last_seen_key();
|
||||
if (lastSeenGossip != null && lastSeenGossip.after(effectiveDate)) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +107,8 @@ class AutocryptInteractor {
|
||||
// 4. Set peers[gossip-addr].gossip_key to the value of the keydata attribute.
|
||||
Long newMasterKeyId = saveKeyringResult.savedMasterKeyId;
|
||||
|
||||
autocryptPeerDao.updateKeyGossipFromAutocrypt(autocryptPeerId, effectiveDate, newMasterKeyId);
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptPeerId, effectiveDate, newMasterKeyId,
|
||||
GossipOrigin.GOSSIP_HEADER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -131,4 +145,97 @@ class AutocryptInteractor {
|
||||
}
|
||||
return uncachedKeyRing;
|
||||
}
|
||||
|
||||
public List<AutocryptRecommendationResult> determineAutocryptRecommendations(String... autocryptIds) {
|
||||
List<AutocryptRecommendationResult> result = new ArrayList<>(autocryptIds.length);
|
||||
|
||||
for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) {
|
||||
AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus);
|
||||
result.add(peerResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Determines Autocrypt "ui-recommendation", according to spec.
|
||||
* See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages
|
||||
*/
|
||||
private AutocryptRecommendationResult determineAutocryptRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(autocryptKeyStatus);
|
||||
if (keyRecommendation != null) return keyRecommendation;
|
||||
|
||||
AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(autocryptKeyStatus);
|
||||
if (gossipRecommendation != null) return gossipRecommendation;
|
||||
|
||||
return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISABLE, null, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptKeyRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
boolean hasKey = autocryptKeyStatus.hasKey();
|
||||
boolean isRevoked = autocryptKeyStatus.isKeyRevoked();
|
||||
boolean isExpired = autocryptKeyStatus.isKeyExpired();
|
||||
if (!hasKey || isRevoked || isExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AutocryptPeer autocryptPeer = autocryptKeyStatus.autocryptPeer();
|
||||
long masterKeyId = autocryptPeer.master_key_id();
|
||||
Date lastSeen = autocryptPeer.last_seen();
|
||||
Date lastSeenKey = autocryptPeer.last_seen_key();
|
||||
boolean isVerified = autocryptKeyStatus.isKeyVerified();
|
||||
if (lastSeenKey.getTime() < (lastSeen.getTime() - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS)) {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
boolean isMutual = autocryptPeer.is_mutual();
|
||||
if (isMutual) {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.MUTUAL, masterKeyId, isVerified);
|
||||
} else {
|
||||
return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.AVAILABLE, masterKeyId, isVerified);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptRecommendationResult determineAutocryptGossipRecommendation(AutocryptKeyStatus autocryptKeyStatus) {
|
||||
boolean gossipHasKey = autocryptKeyStatus.hasGossipKey();
|
||||
boolean gossipIsRevoked = autocryptKeyStatus.isGossipKeyRevoked();
|
||||
boolean gossipIsExpired = autocryptKeyStatus.isGossipKeyExpired();
|
||||
boolean isVerified = autocryptKeyStatus.isGossipKeyVerified();
|
||||
|
||||
if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long masterKeyId = autocryptKeyStatus.autocryptPeer().gossip_master_key_id();
|
||||
return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) {
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptId, effectiveDate, masterKeyId, GossipOrigin.SIGNATURE);
|
||||
}
|
||||
|
||||
public void updateKeyGossipFromDedup(String autocryptId, long masterKeyId) {
|
||||
autocryptPeerDao.updateKeyGossip(packageName, autocryptId, new Date(), masterKeyId, GossipOrigin.DEDUP);
|
||||
}
|
||||
|
||||
public static class AutocryptRecommendationResult {
|
||||
public final String peerId;
|
||||
public final Long masterKeyId;
|
||||
public final AutocryptState autocryptState;
|
||||
public final boolean isVerified;
|
||||
|
||||
AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId,
|
||||
boolean isVerified) {
|
||||
this.peerId = peerId;
|
||||
this.autocryptState = autocryptState;
|
||||
this.masterKeyId = masterKeyId;
|
||||
this.isVerified = isVerified;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum AutocryptState {
|
||||
DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,6 @@ import android.text.TextUtils;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptRecommendationResult;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptState;
|
||||
import org.sufficientlysecure.keychain.provider.DatabaseNotifyManager;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
@@ -52,8 +48,9 @@ import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainProvider;
|
||||
import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface;
|
||||
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptRecommendationResult;
|
||||
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptState;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@@ -69,8 +66,6 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
|
||||
private UriMatcher uriMatcher;
|
||||
private ApiPermissionHelper apiPermissionHelper;
|
||||
private KeychainProvider internalKeychainProvider;
|
||||
private DatabaseNotifyManager databaseNotifyManager;
|
||||
|
||||
|
||||
/**
|
||||
@@ -107,10 +102,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
throw new NullPointerException("Context can't be null during onCreate!");
|
||||
}
|
||||
|
||||
internalKeychainProvider = new KeychainProvider();
|
||||
internalKeychainProvider.attachInfo(context, null);
|
||||
apiPermissionHelper = new ApiPermissionHelper(context, new ApiDataAccessObject(getContext()));
|
||||
databaseNotifyManager = DatabaseNotifyManager.create(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -267,10 +259,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
throw new UnsupportedOperationException("Cannot wildcard-query autocrypt results!");
|
||||
}
|
||||
if (!isWildcardSelector && queriesAutocryptResult) {
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao =
|
||||
new AutocryptPeerDataAccessObject(internalKeychainProvider, callingPackageName,
|
||||
databaseNotifyManager);
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptPeerDao, selectionArgs);
|
||||
AutocryptInteractor autocryptInteractor =
|
||||
AutocryptInteractor.getInstance(getContext(), callingPackageName);
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptInteractor, selectionArgs);
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
@@ -310,7 +301,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
}
|
||||
|
||||
qb.setStrict(true);
|
||||
String query = qb.buildQuery(projection, null, null, groupBy, null, orderBy);
|
||||
String query = qb.buildQuery(projection, null, groupBy, null, orderBy, null);
|
||||
Cursor cursor = db.query(query);
|
||||
if (cursor != null) {
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
@@ -327,9 +318,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
|
||||
}
|
||||
|
||||
private void fillTempTableWithAutocryptRecommendations(SupportSQLiteDatabase db,
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao, String[] peerIds) {
|
||||
AutocryptInteractor autocryptInteractor, String[] peerIds) {
|
||||
List<AutocryptRecommendationResult> autocryptStates =
|
||||
autocryptPeerDao.determineAutocryptRecommendations(peerIds);
|
||||
autocryptInteractor.determineAutocryptRecommendations(peerIds);
|
||||
|
||||
fillTempTableWithAutocryptRecommendations(db, autocryptStates);
|
||||
}
|
||||
|
||||
@@ -68,10 +68,9 @@ import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.SecurityProblem;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDao;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeyRepository;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
||||
import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository;
|
||||
@@ -585,8 +584,8 @@ public class OpenPgpService extends Service {
|
||||
return signatureResult;
|
||||
}
|
||||
|
||||
AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
AutocryptPeerDao autocryptPeerentityDao =
|
||||
AutocryptPeerDao.getInstance(getBaseContext());
|
||||
Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeerId);
|
||||
|
||||
long masterKeyId = signatureResult.getKeyId();
|
||||
@@ -596,7 +595,9 @@ public class OpenPgpService extends Service {
|
||||
if (effectiveTime.after(now)) {
|
||||
effectiveTime = now;
|
||||
}
|
||||
autocryptPeerentityDao.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId);
|
||||
AutocryptInteractor autocryptInteractor =
|
||||
AutocryptInteractor.getInstance(this, mApiPermissionHelper.getCurrentCallingPackage());
|
||||
autocryptInteractor.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId);
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.NEW);
|
||||
} else if (masterKeyId == autocryptPeerMasterKeyId) {
|
||||
return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.OK);
|
||||
@@ -860,9 +861,8 @@ public class OpenPgpService extends Service {
|
||||
|
||||
private Intent updateAutocryptPeerImpl(Intent data) {
|
||||
try {
|
||||
AutocryptPeerDataAccessObject autocryptPeerDao = new AutocryptPeerDataAccessObject(getBaseContext(),
|
||||
mApiPermissionHelper.getCurrentCallingPackage());
|
||||
AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(getBaseContext(), autocryptPeerDao);
|
||||
AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(
|
||||
getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage());
|
||||
|
||||
if (data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) &&
|
||||
data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.sufficientlysecure.keychain.remote.ui.dialog;
|
||||
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -31,10 +30,10 @@ import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.remote.AutocryptInteractor;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
@@ -44,7 +43,7 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks<List<KeyInfo>> {
|
||||
private final int loaderId;
|
||||
|
||||
|
||||
private AutocryptPeerDataAccessObject autocryptPeerDao;
|
||||
private AutocryptInteractor autocryptInteractor;
|
||||
private String duplicateAddress;
|
||||
|
||||
private RemoteDeduplicateView view;
|
||||
@@ -73,7 +72,7 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks<List<KeyInfo>> {
|
||||
return;
|
||||
}
|
||||
|
||||
autocryptPeerDao = new AutocryptPeerDataAccessObject(context, packageName);
|
||||
this.autocryptInteractor = AutocryptInteractor.getInstance(context, packageName);
|
||||
|
||||
this.duplicateAddress = duplicateAddress;
|
||||
view.setAddressText(duplicateAddress);
|
||||
@@ -121,8 +120,8 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks<List<KeyInfo>> {
|
||||
return;
|
||||
}
|
||||
|
||||
long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId();
|
||||
autocryptPeerDao.updateKeyGossipFromDedup(duplicateAddress, new Date(), masterKeyId);
|
||||
long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId();
|
||||
autocryptInteractor.updateKeyGossipFromDedup(duplicateAddress, masterKeyId);
|
||||
|
||||
view.finish();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user