blob: de8538c14c151b8fa4d034c0f4cb70c1c6315202 [file] [log] [blame]
/*
* Copyright (C) 2004-2020 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
* Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
* Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package cx.ring.services;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import cx.ring.daemon.Blob;
import cx.ring.daemon.DataTransferInfo;
import cx.ring.daemon.Ringservice;
import cx.ring.daemon.StringMap;
import cx.ring.daemon.StringVect;
import cx.ring.daemon.UintVect;
import cx.ring.model.Account;
import cx.ring.model.AccountConfig;
import cx.ring.model.CallContact;
import cx.ring.model.Codec;
import cx.ring.model.ConfigKey;
import cx.ring.model.ContactEvent;
import cx.ring.model.Conversation;
import cx.ring.model.DataTransfer;
import cx.ring.model.DataTransferError;
import cx.ring.model.Interaction;
import cx.ring.model.Interaction.InteractionStatus;
import cx.ring.model.TextMessage;
import cx.ring.model.TrustRequest;
import cx.ring.model.Uri;
import cx.ring.smartlist.SmartListViewModel;
import cx.ring.utils.FileUtils;
import cx.ring.utils.Log;
import cx.ring.utils.StringUtils;
import cx.ring.utils.SwigNativeConverter;
import cx.ring.utils.VCardUtils;
import ezvcard.VCard;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
/**
* This service handles the accounts (Ring and SIP)
* - Load and manage the accounts stored in the daemon
* - Keep a local cache of the accounts
* - handle the callbacks that are send by the daemon
*/
public class AccountService {
private static final String TAG = AccountService.class.getSimpleName();
private static final int VCARD_CHUNK_SIZE = 1000;
private static final long DATA_TRANSFER_REFRESH_PERIOD = 500;
private static final int PIN_GENERATION_SUCCESS = 0;
private static final int PIN_GENERATION_WRONG_PASSWORD = 1;
private static final int PIN_GENERATION_NETWORK_ERROR = 2;
@Inject
@Named("DaemonExecutor")
ScheduledExecutorService mExecutor;
@Inject
HistoryService mHistoryService;
@Inject
DeviceRuntimeService mDeviceRuntimeService;
@Inject
VCardService mVCardService;
private Account mCurrentAccount;
private List<Account> mAccountList = new ArrayList<>();
private boolean mHasSipAccount;
private boolean mHasRingAccount;
private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>();
private DataTransfer mStartingTransfer = null;
private final BehaviorSubject<List<Account>> accountsSubject = BehaviorSubject.create();
private final Subject<Account> accountSubject = PublishSubject.create();
private final Observable<Account> currentAccountSubject = accountsSubject
.filter(l -> !l.isEmpty())
.map(l -> l.get(0))
.distinctUntilChanged();
public static class Message {
String accountId;
String messageId;
String callId;
String author;
Map<String, String> messages;
}
public static class Location {
public enum Type {
position,
stop
}
Type type;
String accountId;
String callId;
Uri peer;
long time;
double latitude;
double longitude;
public Type getType() {
return type;
}
public String getAccount() {
return accountId;
}
public Uri getPeer() {
return peer;
}
public long getDate() {
return time;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
}
private final Subject<Message> incomingMessageSubject = PublishSubject.create();
private final Observable<TextMessage> incomingTextMessageSubject = incomingMessageSubject
.flatMapMaybe(msg -> {
String message = msg.messages.get(CallService.MIME_TEXT_PLAIN);
if (message != null) {
return mHistoryService
.incomingMessage(msg.accountId, msg.messageId, msg.author, message)
.toMaybe();
}
return Maybe.empty();
})
.share();
private final Observable<Location> incomingLocationSubject = incomingMessageSubject
.flatMapMaybe(msg -> {
try {
String loc = msg.messages.get(CallService.MIME_GEOLOCATION);
if (loc == null)
return Maybe.empty();
JsonObject obj = JsonParser.parseString(loc).getAsJsonObject();
if (obj.size() < 2)
return Maybe.empty();
Location l = new Location();
JsonElement type = obj.get("type");
if (type == null || type.getAsString().equals(Location.Type.position.toString())) {
l.type = Location.Type.position;
l.latitude = obj.get("lat").getAsDouble();
l.longitude = obj.get("long").getAsDouble();
} else if (type.getAsString().equals(Location.Type.stop.toString())) {
l.type = Location.Type.stop;
}
l.time = obj.get("time").getAsLong();
l.accountId = msg.accountId;
l.callId = msg.callId;
l.peer = new Uri(msg.author);
return Maybe.just(l);
} catch (Exception e) {
Log.w(TAG, "Failed to receive geolocation", e);
return Maybe.empty();
}
})
.share();
private final Subject<TextMessage> textMessageSubject = PublishSubject.create();
private final Subject<DataTransfer> dataTransferSubject = PublishSubject.create();
private final Subject<TrustRequest> incomingRequestsSubject = PublishSubject.create();
public void refreshAccounts() {
accountsSubject.onNext(mAccountList);
}
public static class RegisteredName {
public String accountId;
public String name;
public String address;
public int state;
}
public static class UserSearchResult {
private final String accountId;
private final String query;
public int state;
public List<CallContact> results;
public UserSearchResult(String account, String query) {
accountId = account;
this.query = query;
}
public String getAccountId() {
return accountId;
}
public String getQuery() {
return query;
}
public List<Observable<SmartListViewModel>> getResultsViewModels() {
List<Observable<SmartListViewModel>> vms = new ArrayList<>(results.size());
for (CallContact user : results) {
vms.add(Observable.just(new SmartListViewModel(accountId, user, null)));
}
return vms;
}
}
private final Subject<RegisteredName> registeredNameSubject = PublishSubject.create();
private final Subject<UserSearchResult> searchResultSubject = PublishSubject.create();
private static class ExportOnRingResult {
String accountId;
int code;
String pin;
}
private static class DeviceRevocationResult {
String accountId;
String deviceId;
int code;
}
private static class MigrationResult {
String accountId;
String state;
}
private final Subject<ExportOnRingResult> mExportSubject = PublishSubject.create();
private final Subject<DeviceRevocationResult> mDeviceRevocationSubject = PublishSubject.create();
private final Subject<MigrationResult> mMigrationSubject = PublishSubject.create();
public Observable<RegisteredName> getRegisteredNames() {
return registeredNameSubject;
}
public Observable<UserSearchResult> getSearchResults() {
return searchResultSubject;
}
public Observable<TextMessage> getIncomingMessages() {
return incomingTextMessageSubject;
}
public Observable<Location> getLocationUpdates() {
return incomingLocationSubject;
}
public Observable<TextMessage> getMessageStateChanges() {
return textMessageSubject;
}
public Observable<TrustRequest> getIncomingRequests() {
return incomingRequestsSubject;
}
/**
* @return true if at least one of the loaded accounts is a SIP one
*/
public boolean hasSipAccount() {
return mHasSipAccount;
}
/**
* @return true if at least one of the loaded accounts is a Ring one
*/
public boolean hasRingAccount() {
return mHasRingAccount;
}
/**
* Loads the accounts from the daemon and then builds the local cache (also sends ACCOUNTS_CHANGED event)
*
* @param isConnected sets the initial connection state of the accounts
*/
public void loadAccountsFromDaemon(final boolean isConnected) {
mExecutor.execute(() -> {
refreshAccountsCacheFromDaemon();
setAccountsActive(isConnected);
});
}
private void refreshAccountsCacheFromDaemon() {
Log.w(TAG, "refreshAccountsCacheFromDaemon");
boolean hasSip = false, hasJami = false;
List<Account> curList = mAccountList;
List<String> accountIds = new ArrayList<>(Ringservice.getAccountList());
List<Account> newAccounts = new ArrayList<>(accountIds.size());
for (String id : accountIds) {
for (Account acc : curList)
if (acc.getAccountID().equals(id)) {
newAccounts.add(acc);
break;
}
}
// Cleanup removed accounts
for (Account acc : curList)
if (!newAccounts.contains(acc))
acc.cleanup();
mAccountList = newAccounts;
for (String accountId : accountIds) {
Account account = getAccount(accountId);
Map<String, String> details = Ringservice.getAccountDetails(accountId).toNative();
List<Map<String, String>> credentials = Ringservice.getCredentials(accountId).toNative();
Map<String, String> volatileAccountDetails = Ringservice.getVolatileAccountDetails(accountId).toNative();
if (account == null) {
account = new Account(accountId, details, credentials, volatileAccountDetails);
newAccounts.add(account);
} else {
account.setDetails(details);
account.setCredentials(credentials);
account.setVolatileDetails(volatileAccountDetails);
}
if (account.isSip()) {
hasSip = true;
} else if (account.isJami()) {
hasJami = true;
boolean enabled = account.isEnabled();
account.setDevices(Ringservice.getKnownRingDevices(accountId).toNative());
account.setContacts(Ringservice.getContacts(accountId).toNative());
List<Map<String, String>> requests = Ringservice.getTrustRequests(accountId).toNative();
for (Map<String, String> requestInfo : requests) {
TrustRequest request = new TrustRequest(accountId, requestInfo);
account.addRequest(request);
CallContact contact = account.getContactFromCache(request.getContactId());
if (!contact.detailsLoaded) {
mVCardService.loadVCardProfile(request.getVCard())
.subscribeOn(Schedulers.computation())
.subscribe(profile -> contact.setProfile(profile.first, profile.second));
}
// If name is in cache this can be synchronous
if (enabled)
Ringservice.lookupAddress(accountId, "", request.getContactId());
}
if (enabled) {
for (CallContact contact : account.getContacts().values()) {
if (!contact.isUsernameLoaded())
Ringservice.lookupAddress(accountId, "", contact.getPrimaryUri().getRawRingId());
}
}
}
}
mHasSipAccount = hasSip;
mHasRingAccount = hasJami;
if (!newAccounts.isEmpty()) {
Account newAccount = newAccounts.get(0);
if (mCurrentAccount != newAccount) {
mCurrentAccount = newAccount;
}
}
// migration to multi accounts
mHistoryService.migrateDatabase(accountIds);
mHistoryService.getMigrationStatus().firstOrError().subscribe(migrationStatus -> {
if (migrationStatus == HistoryService.MigrationStatus.SUCCESSFUL) {
mVCardService.migrateProfiles(accountIds);
for (String accountId : accountIds) {
Account account = getAccount(accountId);
if (account.isJami()) {
mVCardService.migrateContact(account.getContacts(), accountId);
migrateContactEvents(account, account.getContacts(), account.getRequestsMigration());
}
}
mVCardService.deleteLegacyProfiles();
}
}, e -> Log.e(TAG, "Error completing profile migration", e));
accountsSubject.onNext(newAccounts);
}
private Account getAccountByName(final String name) {
for (Account acc : mAccountList) {
if (acc.getAlias().equals(name))
return acc;
}
return null;
}
public String getNewAccountName(final String prefix) {
String name = String.format(prefix, "").trim();
if (getAccountByName(name) == null) {
return name;
}
int num = 1;
do {
num++;
name = String.format(prefix, num).trim();
} while (getAccountByName(name) != null);
return name;
}
/**
* Adds a new Account in the Daemon (also sends an ACCOUNT_ADDED event)
* Sets the new account as the current one
*
* @param map the account details
* @return the created Account
*/
public Observable<Account> addAccount(final Map<String, String> map) {
return Observable.fromCallable(() -> {
String accountId = Ringservice.addAccount(StringMap.toSwig(map));
if (StringUtils.isEmpty(accountId)) {
throw new RuntimeException("Can't create account.");
}
Account account = getAccount(accountId);
if (account == null) {
Map<String, String> accountDetails = Ringservice.getAccountDetails(accountId).toNative();
List<Map<String, String>> accountCredentials = Ringservice.getCredentials(accountId).toNative();
Map<String, String> accountVolatileDetails = Ringservice.getVolatileAccountDetails(accountId).toNative();
Map<String, String> accountDevices = Ringservice.getKnownRingDevices(accountId).toNative();
account = new Account(accountId, accountDetails, accountCredentials, accountVolatileDetails);
account.setDevices(accountDevices);
if (account.isSip()) {
account.setRegistrationState(AccountConfig.STATE_READY, -1);
}
mAccountList.add(account);
accountsSubject.onNext(mAccountList);
}
return account;
})
.flatMap(account -> accountSubject
.filter(acc -> acc.getAccountID().equals(account.getAccountID()))
.startWith(account))
.subscribeOn(Schedulers.from(mExecutor));
}
/**
* @return the current Account from the local cache
*/
public Account getCurrentAccount() {
return mCurrentAccount;
}
public int getCurrentAccountIndex() {
return mAccountList.indexOf(mCurrentAccount);
}
/**
* Sets the current Account in the local cache (also sends a ACCOUNTS_CHANGED event)
*/
public void setCurrentAccount(Account currentAccount) {
if (mCurrentAccount == currentAccount)
return;
mCurrentAccount = currentAccount;
// the account order is changed
// the current Account is now on the top of the list
final List<Account> accounts = getAccounts();
List<String> orderedAccountIdList = new ArrayList<>(accounts.size());
String selectedID = mCurrentAccount.getAccountID();
orderedAccountIdList.add(selectedID);
for (Account account : accounts) {
if (account.getAccountID().contentEquals(selectedID)) {
continue;
}
orderedAccountIdList.add(account.getAccountID());
}
setAccountOrder(orderedAccountIdList);
}
/**
* @return the Account from the local cache that matches the accountId
*/
public Account getAccount(String accountId) {
if (!StringUtils.isEmpty(accountId)) {
for (Account account : mAccountList) {
String accountID = account.getAccountID();
if (accountID.equals(accountId)) {
return account;
}
}
}
return null;
}
public Single<Account> getAccountSingle(final String accountId) {
return accountsSubject
.firstOrError()
.map(accounts -> {
for (Account account : accounts) {
String accountID = account.getAccountID();
if (accountID.equals(accountId)) {
return account;
}
}
Log.d(TAG, "getAccountSingle() can't find account " + accountId);
throw new IllegalArgumentException();
});
}
/**
* @return Accounts list from the local cache
*/
public List<Account> getAccounts() {
return mAccountList;
}
public Observable<List<Account>> getObservableAccountList() {
return accountsSubject;
}
private Single<List<Account>> loadAccountProfiles(List<Account> accounts) {
if (accounts.isEmpty())
return Single.just(accounts);
List<Single<Account>> loadedAccounts = new ArrayList<>(accounts.size());
for (Account account : accounts)
loadedAccounts.add(loadAccountProfile(account));
return Single.concatEager(loadedAccounts).toList(accounts.size());
}
public Observable<List<Account>> getProfileAccountList() {
return accountsSubject.concatMapSingle(this::loadAccountProfiles);
}
private Single<Account> loadAccountProfile(Account account) {
if (account.getProfile() == null)
return VCardUtils.loadLocalProfileFromDiskWithDefault(mDeviceRuntimeService.provideFilesDir(), account.getAccountID())
.subscribeOn(Schedulers.io())
.map(vCard -> {
account.setProfile(vCard);
return account;
})
.onErrorReturn(e -> account);
else
return Single.just(account);
}
public Subject<Account> getObservableAccounts() {
return accountSubject;
}
public Observable<Account> getObservableAccountUpdates(String accountId) {
return accountSubject.filter(acc -> acc.getAccountID().equals(accountId));
}
public Observable<Account> getObservableAccount(String accountId) {
return Observable.fromCallable(() -> getAccount(accountId))
.concatWith(getObservableAccountUpdates(accountId));
}
public Observable<Account> getCurrentAccountSubject() {
return currentAccountSubject;
}
public void subscribeBuddy(final String accountID, final String uri, final boolean flag) {
mExecutor.execute(() -> Ringservice.subscribeBuddy(accountID, uri, flag));
}
/**
* Send profile through SIP
*/
public void sendProfile(final String callId, final String accountId) {
mVCardService.loadSmallVCard(accountId, VCardService.MAX_SIZE_SIP)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.from(mExecutor))
.subscribe(vcard -> {
String stringVCard = VCardUtils.vcardToString(vcard);
int nbTotal = stringVCard.length() / VCARD_CHUNK_SIZE + (stringVCard.length() % VCARD_CHUNK_SIZE != 0 ? 1 : 0);
int i = 1;
Random r = new Random(System.currentTimeMillis());
int key = Math.abs(r.nextInt());
Log.d(TAG, "sendProfile, vcard " + stringVCard);
while (i <= nbTotal) {
HashMap<String, String> chunk = new HashMap<>();
Log.d(TAG, "length vcard " + stringVCard.length() + " id " + key + " part " + i + " nbTotal " + nbTotal);
String keyHashMap = VCardUtils.MIME_PROFILE_VCARD + "; id=" + key + ",part=" + i + ",of=" + nbTotal;
String message = stringVCard.substring(0, Math.min(VCARD_CHUNK_SIZE, stringVCard.length()));
chunk.put(keyHashMap, message);
Ringservice.sendTextMessage(callId, StringMap.toSwig(chunk), "Me", false);
if (stringVCard.length() > VCARD_CHUNK_SIZE) {
stringVCard = stringVCard.substring(VCARD_CHUNK_SIZE);
}
i++;
}
}, e -> Log.w(TAG, "Not sending empty profile", e));
}
public void setMessageDisplayed(String accountId, String contactId, String messageId) {
mExecutor.execute(() -> Ringservice.setMessageDisplayed(accountId, contactId, messageId, 3));
}
/**
* @return Account Ids list from Daemon
*/
public Single<List<String>> getAccountList() {
return Single.fromCallable(() -> (List<String>)new ArrayList<>(Ringservice.getAccountList()))
.subscribeOn(Schedulers.from(mExecutor));
}
/**
* Sets the order of the accounts in the Daemon
*
* @param accountOrder The ordered list of account ids
*/
public void setAccountOrder(final List<String> accountOrder) {
mExecutor.execute(() -> {
final StringBuilder order = new StringBuilder();
for (String accountId : accountOrder) {
order.append(accountId);
order.append(File.separator);
}
Ringservice.setAccountsOrder(order.toString());
});
}
/**
* @return the account details from the Daemon
*/
public Map<String, String> getAccountDetails(final String accountId) {
try {
return mExecutor.submit(() -> Ringservice.getAccountDetails(accountId).toNative()).get();
} catch (Exception e) {
Log.e(TAG, "Error running getAccountDetails()", e);
}
return null;
}
/**
* Sets the account details in the Daemon
*/
public void setAccountDetails(final String accountId, final Map<String, String> map) {
Log.i(TAG, "setAccountDetails() " + accountId);
mExecutor.execute(() -> Ringservice.setAccountDetails(accountId, StringMap.toSwig(map)));
}
public Single<String> migrateAccount(String accountId, String password) {
return mMigrationSubject
.filter(r -> r.accountId.equals(accountId))
.map(r -> r.state)
.firstOrError()
.doOnSubscribe(s -> {
final Account account = getAccount(accountId);
HashMap<String, String> details = account.getDetails();
details.put(ConfigKey.ARCHIVE_PASSWORD.key(), password);
mExecutor.execute(() -> Ringservice.setAccountDetails(accountId, StringMap.toSwig(details)));
})
.subscribeOn(Schedulers.from(mExecutor));
}
public void setAccountEnabled(final String accountId, final boolean active) {
mExecutor.execute(() -> Ringservice.sendRegister(accountId, active));
}
/**
* Sets the activation state of the account in the Daemon
*/
public void setAccountActive(final String accountId, final boolean active) {
mExecutor.execute(() -> Ringservice.setAccountActive(accountId, active));
}
/**
* Sets the activation state of all the accounts in the Daemon
*/
public void setAccountsActive(final boolean active) {
mExecutor.execute(() -> {
Log.i(TAG, "setAccountsActive() running... " + active);
for (Account a : mAccountList) {
// If the proxy is enabled we can considered the account
// as always active
if (a.isDhtProxyEnabled()) {
Ringservice.setAccountActive(a.getAccountID(), true);
} else {
Ringservice.setAccountActive(a.getAccountID(), active);
}
}
});
}
/**
* Sets the video activation state of all the accounts in the local cache
*/
public void setAccountsVideoEnabled(boolean isEnabled) {
for (Account account : mAccountList) {
account.setDetail(ConfigKey.VIDEO_ENABLED, isEnabled);
}
}
/**
* @return the account volatile details from the Daemon
*/
public Map<String, String> getVolatileAccountDetails(final String accountId) {
try {
return mExecutor.submit(() -> Ringservice.getVolatileAccountDetails(accountId).toNative()).get();
} catch (Exception e) {
Log.e(TAG, "Error running getVolatileAccountDetails()", e);
}
return null;
}
/**
* @return the default template (account details) for a type of account
*/
public Single<HashMap<String, String>> getAccountTemplate(final String accountType) {
Log.i(TAG, "getAccountTemplate() " + accountType);
return Single.fromCallable(() -> Ringservice.getAccountTemplate(accountType).toNative())
.subscribeOn(Schedulers.from(mExecutor));
}
/**
* Removes the account in the Daemon as well as local history
*/
public void removeAccount(final String accountId) {
Log.i(TAG, "removeAccount() " + accountId);
mExecutor.execute(() -> Ringservice.removeAccount(accountId));
mHistoryService.clearHistory(accountId).subscribe();
}
/**
* Exports the account on the DHT (used for multi-devices feature)
*/
public Single<String> exportOnRing(final String accountId, final String password) {
return mExportSubject
.filter(r -> r.accountId.equals(accountId))
.firstOrError()
.map(result -> {
switch (result.code) {
case PIN_GENERATION_SUCCESS:
return result.pin;
case PIN_GENERATION_WRONG_PASSWORD:
throw new IllegalArgumentException();
case PIN_GENERATION_NETWORK_ERROR:
throw new SocketException();
default:
throw new UnsupportedOperationException();
}
})
.doOnSubscribe(l -> {
Log.i(TAG, "exportOnRing() " + accountId);
mExecutor.execute(() -> Ringservice.exportOnRing(accountId, password));
})
.subscribeOn(Schedulers.io());
}
/**
* @return the list of the account's devices from the Daemon
*/
public Map<String, String> getKnownRingDevices(final String accountId) {
Log.i(TAG, "getKnownRingDevices() " + accountId);
try {
return mExecutor.submit(() -> Ringservice.getKnownRingDevices(accountId).toNative()).get();
} catch (Exception e) {
Log.e(TAG, "Error running getKnownRingDevices()", e);
}
return null;
}
/**
* @param accountId id of the account used with the device
* @param deviceId id of the device to revoke
* @param password password of the account
*/
public Single<Integer> revokeDevice(final String accountId, final String password, final String deviceId) {
return mDeviceRevocationSubject
.filter(r -> r.accountId.equals(accountId) && r.deviceId.equals(deviceId))
.firstOrError()
.map(r -> r.code)
.doOnSubscribe(l -> mExecutor.execute(() -> Ringservice.revokeDevice(accountId, password, deviceId)))
.subscribeOn(Schedulers.io());
}
/**
* @param accountId id of the account used with the device
* @param newName new device name
*/
public void renameDevice(final String accountId, final String newName) {
final Account account = getAccount(accountId);
mExecutor.execute(() -> {
Log.i(TAG, "renameDevice() thread running... " + newName);
StringMap details = Ringservice.getAccountDetails(accountId);
details.put(ConfigKey.ACCOUNT_DEVICE_NAME.key(), newName);
Ringservice.setAccountDetails(accountId, details);
account.setDetail(ConfigKey.ACCOUNT_DEVICE_NAME, newName);
account.setDevices(Ringservice.getKnownRingDevices(accountId).toNative());
});
}
public Completable exportToFile(String accountId, String absolutePath, String password) {
return Completable.fromAction(() -> {
if (!Ringservice.exportToFile(accountId, absolutePath, password))
throw new IllegalArgumentException("Can't export archive");
}).subscribeOn(Schedulers.from(mExecutor));
}
/**
* @param accountId id of the account
* @param oldPassword old account password
*/
public Completable setAccountPassword(final String accountId, final String oldPassword, final String newPassword) {
return Completable.fromAction(() -> {
if (!Ringservice.changeAccountPassword(accountId, oldPassword, newPassword))
throw new IllegalArgumentException("Can't change password");
}).subscribeOn(Schedulers.from(mExecutor));
}
/**
* Sets the active codecs list of the account in the Daemon
*/
public void setActiveCodecList(final String accountId, final List<Long> codecs) {
mExecutor.execute(() -> {
UintVect list = new UintVect();
list.reserve(codecs.size());
list.addAll(codecs);
Ringservice.setActiveCodecList(accountId, list);
accountSubject.onNext(getAccount(accountId));
});
}
/**
* @return The account's codecs list from the Daemon
*/
public Single<List<Codec>> getCodecList(final String accountId) {
return Single.fromCallable(() -> {
List<Codec> results = new ArrayList<>();
UintVect payloads = Ringservice.getCodecList();
UintVect activePayloads = Ringservice.getActiveCodecList(accountId);
for (int i = 0; i < payloads.size(); ++i) {
StringMap details = Ringservice.getCodecDetails(accountId, payloads.get(i));
if (details.size() > 1) {
results.add(new Codec(payloads.get(i), details.toNative(), activePayloads.contains(payloads.get(i))));
} else {
Log.i(TAG, "Error loading codec " + i);
}
}
return results;
}).subscribeOn(Schedulers.from(mExecutor));
}
public Map<String, String> validateCertificatePath(final String accountID, final String certificatePath, final String privateKeyPath, final String privateKeyPass) {
try {
return mExecutor.submit(() -> {
Log.i(TAG, "validateCertificatePath() running...");
return Ringservice.validateCertificatePath(accountID, certificatePath, privateKeyPath, privateKeyPass, "").toNative();
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running validateCertificatePath()", e);
}
return null;
}
public Map<String, String> validateCertificate(final String accountId, final String certificate) {
try {
return mExecutor.submit(() -> {
Log.i(TAG, "validateCertificate() running...");
return Ringservice.validateCertificate(accountId, certificate).toNative();
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running validateCertificate()", e);
}
return null;
}
public Map<String, String> getCertificateDetailsPath(final String certificatePath) {
try {
return mExecutor.submit(() -> {
Log.i(TAG, "getCertificateDetailsPath() running...");
return Ringservice.getCertificateDetails(certificatePath).toNative();
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running getCertificateDetailsPath()", e);
}
return null;
}
public Map<String, String> getCertificateDetails(final String certificateRaw) {
try {
return mExecutor.submit(() -> {
Log.i(TAG, "getCertificateDetails() running...");
return Ringservice.getCertificateDetails(certificateRaw).toNative();
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running getCertificateDetails()", e);
}
return null;
}
/**
* @return the supported TLS methods from the Daemon
*/
public List<String> getTlsSupportedMethods() {
Log.i(TAG, "getTlsSupportedMethods()");
return SwigNativeConverter.toJava(Ringservice.getSupportedTlsMethod());
}
/**
* @return the account's credentials from the Daemon
*/
public List<Map<String, String>> getCredentials(final String accountId) {
try {
return mExecutor.submit(() -> {
Log.i(TAG, "getCredentials() running...");
return Ringservice.getCredentials(accountId).toNative();
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running getCredentials()", e);
}
return null;
}
/**
* Sets the account's credentials in the Daemon
*/
public void setCredentials(final String accountId, final List<Map<String, String>> credentials) {
Log.i(TAG, "setCredentials() " + accountId);
mExecutor.execute(() -> Ringservice.setCredentials(accountId, SwigNativeConverter.toSwig(credentials)));
}
/**
* Sets the registration state to true for all the accounts in the Daemon
*/
public void registerAllAccounts() {
Log.i(TAG, "registerAllAccounts()");
mExecutor.execute(this::registerAllAccounts);
}
/**
* Backs up all the accounts into to an archive in the path
*/
public int backupAccounts(final List<String> accountIds, final String toDir, final String password) {
try {
return mExecutor.submit(() -> {
StringVect ids = new StringVect();
ids.addAll(accountIds);
return Ringservice.exportAccounts(ids, toDir, password);
}).get();
} catch (Exception e) {
Log.e(TAG, "Error running backupAccounts()", e);
}
return 1;
}
/**
* Restores the saved accounts from a path
*/
public int restoreAccounts(final String archivePath, final String password) {
try {
return mExecutor.submit(() -> Ringservice.importAccounts(archivePath, password)).get();
} catch (Exception e) {
Log.e(TAG, "Error running restoreAccounts()", e);
}
return 1;
}
/**
* Registers a new name on the blockchain for the account
*/
public void registerName(final Account account, final String password, final String name) {
if (account.registeringUsername) {
Log.w(TAG, "Already trying to register username");
return;
}
account.registeringUsername = true;
registerName(account.getAccountID(), password, name);
}
/**
* Register a new name on the blockchain for the account Id
*/
public void registerName(final String account, final String password, final String name) {
Log.i(TAG, "registerName()");
mExecutor.execute(() -> Ringservice.registerName(account, password, name));
}
/* contact requests */
/**
* @return all trust requests from the daemon for the account Id
*/
public List<Map<String, String>> getTrustRequests(final String accountId) {
try {
return mExecutor.submit(() -> Ringservice.getTrustRequests(accountId).toNative()).get();
} catch (Exception e) {
Log.e(TAG, "Error running getTrustRequests()", e);
}
return null;
}
/**
* Accepts a pending trust request
*/
public void acceptTrustRequest(final String accountId, final Uri from) {
Log.i(TAG, "acceptRequest() " + accountId + " " + from);
Account account = getAccount(accountId);
if (account != null) {
TrustRequest request = account.getRequest(from);
if (request != null) {
VCard vCard = request.getVCard();
if (vCard != null) {
VCardUtils.savePeerProfileToDisk(vCard, accountId, from.getRawRingId() + ".vcf", mDeviceRuntimeService.provideFilesDir());
}
}
account.removeRequest(from);
handleTrustRequest(accountId, from.getUri(), null, ContactType.INVITATION_ACCEPTED);
}
mExecutor.execute(() -> Ringservice.acceptTrustRequest(accountId, from.getRawRingId()));
}
/**
* Handles adding contacts and is the initial point of conversation creation
*
* @param accountId the user's account id
* @param contactUri the contacts raw string uri
*/
private void handleTrustRequest(String accountId, String contactUri, TrustRequest request, ContactType type) {
ContactEvent event = new ContactEvent();
switch (type) {
case ADDED:
break;
case INVITATION_RECEIVED:
event.setStatus(Interaction.InteractionStatus.UNKNOWN);
event.setAuthor(contactUri);
event.setTimestamp(request.getTimestamp());
break;
case INVITATION_ACCEPTED:
event.setStatus(Interaction.InteractionStatus.SUCCESS);
event.setAuthor(contactUri);
break;
case INVITATION_DISCARDED:
mHistoryService.clearHistory(contactUri, accountId, true).subscribe();
return;
default:
return;
}
insertContactEvent(accountId, contactUri, event);
}
/**
* Migrates contact events including trust requests to the database. This is necessary because the old database did not store contact events.
* Should only be called in the migration process.
*
* @param account the user's account object
* @param contacts the user's contacts (if they exist)
* @param requests the user's trust requests (if they exist)
*/
private void migrateContactEvents(Account account, Map<String, CallContact> contacts, Map<String, TrustRequest> requests) {
String accountId = account.getAccountID();
for (TrustRequest request : requests.values()) {
CallContact contact = account.getContactFromCache(request.getContactId());
ContactEvent event = new ContactEvent(contact, request);
insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event);
}
for (CallContact contact : contacts.values()) {
ContactEvent event = new ContactEvent(contact);
insertContactEvent(accountId, contact.getPrimaryUri().getUri(), event);
}
}
/**
* Inserts a contact event, and if needed, a conversation, into the database
*
* @param accountId the user's account ID
* @param participantUri the contact's string uri
* @param event the contact event
*/
private void insertContactEvent(String accountId, String participantUri, ContactEvent event) {
Conversation conversation = getAccount(accountId).getByUri(participantUri);
mHistoryService.insertInteraction(accountId, conversation, event).subscribe();
}
private enum ContactType {
ADDED, INVITATION_RECEIVED, INVITATION_ACCEPTED, INVITATION_DISCARDED
}
/**
* Refuses and blocks a pending trust request
*/
public boolean discardTrustRequest(final String accountId, final Uri contact) {
Account account = getAccount(accountId);
boolean removed = false;
if (account != null) {
removed = account.removeRequest(contact);
handleTrustRequest(accountId, contact.getUri(), null, ContactType.INVITATION_DISCARDED);
}
mExecutor.execute(() -> Ringservice.discardTrustRequest(accountId, contact.getRawRingId()));
return removed;
}
/**
* Sends a new trust request
*/
public void sendTrustRequest(final String accountId, final String to, final Blob message) {
Log.i(TAG, "sendTrustRequest() " + accountId + " " + to);
handleTrustRequest(accountId, new Uri(to).getUri(), null, ContactType.ADDED);
mExecutor.execute(() -> Ringservice.sendTrustRequest(accountId, to, message == null ? new Blob() : message));
}
/**
* Add a new contact for the account Id on the Daemon
*/
public void addContact(final String accountId, final String uri) {
Log.i(TAG, "addContact() " + accountId + " " + uri);
handleTrustRequest(accountId, new Uri(uri).getUri(), null, ContactType.ADDED);
mExecutor.execute(() -> Ringservice.addContact(accountId, uri));
}
/**
* Remove an existing contact for the account Id on the Daemon
*/
public void removeContact(final String accountId, final String uri, final boolean ban) {
Log.i(TAG, "removeContact() " + accountId + " " + uri + " ban:" + ban);
mExecutor.execute(() -> Ringservice.removeContact(accountId, uri, ban));
}
/**
* @return the contacts list from the daemon
*/
public List<Map<String, String>> getContacts(final String accountId) {
try {
return mExecutor.submit(() -> Ringservice.getContacts(accountId).toNative()).get();
} catch (Exception e) {
Log.e(TAG, "Error running getContacts()", e);
}
return null;
}
/**
* Looks up for the availibility of the name on the blockchain
*/
public void lookupName(final String account, final String nameserver, final String name) {
Log.i(TAG, "lookupName() " + account + " " + nameserver + " " + name);
mExecutor.execute(() -> Ringservice.lookupName(account, nameserver, name));
}
public Single<RegisteredName> findRegistrationByName(final String account, final String nameserver, final String name) {
if (StringUtils.isEmpty(name)) {
return Single.just(new RegisteredName());
}
return getRegisteredNames()
.filter(r -> account.equals(r.accountId) && name.equals(r.name))
.firstOrError()
.doOnSubscribe(s -> mExecutor.execute(() -> Ringservice.lookupName(account, nameserver, name)))
.subscribeOn(Schedulers.from(mExecutor));
}
public Single<UserSearchResult> searchUser(final String account, final String query) {
if (StringUtils.isEmpty(query)) {
return Single.just(new UserSearchResult(account, query));
}
String encodedUrl;
try {
encodedUrl = URLEncoder.encode(query, "UTF-8");
} catch (UnsupportedEncodingException e) {
return Single.error(e);
}
return getSearchResults()
.filter(r -> account.equals(r.accountId) && encodedUrl.equals(r.query))
.firstOrError()
.doOnSubscribe(s -> mExecutor.execute(() -> Ringservice.searchUser(account, encodedUrl)))
.subscribeOn(Schedulers.from(mExecutor));
}
/**
* Reverse looks up the address in the blockchain to find the name
*/
public void lookupAddress(final String account, final String nameserver, final String address) {
mExecutor.execute(() -> Ringservice.lookupAddress(account, nameserver, address));
}
public void pushNotificationReceived(final String from, final Map<String, String> data) {
// Log.i(TAG, "pushNotificationReceived()");
mExecutor.execute(() -> Ringservice.pushNotificationReceived(from, StringMap.toSwig(data)));
}
public void setPushNotificationToken(final String pushNotificationToken) {
Log.i(TAG, "setPushNotificationToken()");
mExecutor.execute(() -> Ringservice.setPushNotificationToken(pushNotificationToken));
}
void volumeChanged(String device, int value) {
Log.w(TAG, "volumeChanged " + device + " " + value);
}
void accountsChanged() {
// Accounts have changed in Daemon, we have to update our local cache
refreshAccountsCacheFromDaemon();
}
void stunStatusFailure(String accountId) {
Log.d(TAG, "stun status failure: " + accountId);
}
void registrationStateChanged(String accountId, String newState, int code, String detailString) {
Log.d(TAG, "registrationStateChanged: " + accountId + ", " + newState + ", " + code + ", " + detailString);
Account account = getAccount(accountId);
if (account == null) {
return;
}
String oldState = account.getRegistrationState();
if (oldState.contentEquals(AccountConfig.STATE_INITIALIZING) &&
!newState.contentEquals(AccountConfig.STATE_INITIALIZING)) {
account.setDetails(Ringservice.getAccountDetails(account.getAccountID()).toNative());
account.setCredentials(Ringservice.getCredentials(account.getAccountID()).toNative());
account.setDevices(Ringservice.getKnownRingDevices(account.getAccountID()).toNative());
account.setVolatileDetails(Ringservice.getVolatileAccountDetails(account.getAccountID()).toNative());
} else {
account.setRegistrationState(newState, code);
}
if (!oldState.equals(newState)) {
accountSubject.onNext(account);
}
}
void accountDetailsChanged(String accountId, Map<String, String> details) {
Account account = getAccount(accountId);
if (account == null) {
return;
}
Log.d(TAG, "accountDetailsChanged: " + accountId + " " + details.size());
account.setDetails(details);
accountSubject.onNext(account);
}
void volatileAccountDetailsChanged(String accountId, Map<String, String> details) {
Account account = getAccount(accountId);
if (account == null) {
return;
}
Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size());
account.setVolatileDetails(details);
accountSubject.onNext(account);
}
public void accountProfileReceived(String accountId, String name, String photo) {
Account account = getAccount(accountId);
if (account == null)
return;
mVCardService.saveVCardProfile(accountId, account.getUri(), name, photo)
.subscribeOn(Schedulers.io())
.subscribe(account::setProfile, e -> Log.e(TAG, "Error saving profile", e));
}
void profileReceived(String accountId, String peerId, String vcardPath) {
Account account = getAccount(accountId);
if (account == null)
return;
Log.w(TAG, "profileReceived: " + accountId + ", " + peerId + ", " + vcardPath);
CallContact contact = account.getContactFromCache(peerId);
mVCardService.peerProfileReceived(accountId, peerId, new File(vcardPath))
.subscribe(profile -> contact.setProfile(profile.first, profile.second), e -> Log.e(TAG, "Error saving contact profile", e));
}
void incomingAccountMessage(String accountId, String messageId, String callId, String from, Map<String, String> messages) {
Log.d(TAG, "incomingAccountMessage: " + accountId + " " + messages.size());
Message message = new Message();
message.accountId = accountId;
message.messageId = messageId;
message.callId = callId;
message.author = from;
message.messages = messages;
incomingMessageSubject.onNext(message);
}
void accountMessageStatusChanged(String accountId, long messageId, String to, int status) {
Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + messageId + ", " + to + ", " + status);
mHistoryService
.accountMessageStatusChanged(accountId, messageId, to, status)
.subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
}
public void composingStatusChanged(String accountId, String contactUri, int status) {
Log.d(TAG, "composingStatusChanged: " + accountId + ", " + contactUri + " " + status);
getAccountSingle(accountId)
.subscribe(account -> account.composingStatusChanged(new Uri(contactUri), Account.ComposingStatus.fromInt(status)));
}
void errorAlert(int alert) {
Log.d(TAG, "errorAlert : " + alert);
}
void knownDevicesChanged(String accountId, Map<String, String> devices) {
Account accountChanged = getAccount(accountId);
if (accountChanged != null) {
accountChanged.setDevices(devices);
accountSubject.onNext(accountChanged);
}
}
void exportOnRingEnded(String accountId, int code, String pin) {
Log.d(TAG, "exportOnRingEnded: " + accountId + ", " + code + ", " + pin);
ExportOnRingResult result = new ExportOnRingResult();
result.accountId = accountId;
result.code = code;
result.pin = pin;
mExportSubject.onNext(result);
}
void nameRegistrationEnded(String accountId, int state, String name) {
Log.d(TAG, "nameRegistrationEnded: " + accountId + ", " + state + ", " + name);
Account acc = getAccount(accountId);
if (acc == null) {
Log.w(TAG, "Can't find account for name registration callback");
return;
}
acc.registeringUsername = false;
acc.setVolatileDetails(Ringservice.getVolatileAccountDetails(acc.getAccountID()).toNative());
if (state == 0) {
acc.setDetail(ConfigKey.ACCOUNT_REGISTERED_NAME, name);
}
accountSubject.onNext(acc);
}
void migrationEnded(String accountId, String state) {
Log.d(TAG, "migrationEnded: " + accountId + ", " + state);
MigrationResult result = new MigrationResult();
result.accountId = accountId;
result.state = state;
mMigrationSubject.onNext(result);
}
void deviceRevocationEnded(String accountId, String device, int state) {
Log.d(TAG, "deviceRevocationEnded: " + accountId + ", " + device + ", " + state);
DeviceRevocationResult result = new DeviceRevocationResult();
result.accountId = accountId;
result.deviceId = device;
result.code = state;
if (state == 0) {
Account account = getAccount(accountId);
if (account != null) {
Map<String, String> devices = account.getDevices();
devices.remove(device);
account.setDevices(devices);
accountSubject.onNext(account);
}
}
mDeviceRevocationSubject.onNext(result);
}
void incomingTrustRequest(String accountId, String from, String message, long received) {
Log.d(TAG, "incomingTrustRequest: " + accountId + ", " + from + ", " + received);
Account account = getAccount(accountId);
if (account != null) {
TrustRequest request = new TrustRequest(accountId, from, received * 1000L, message);
VCard vcard = request.getVCard();
if (vcard != null) {
CallContact contact = account.getContactFromCache(request.getContactId());
if (!contact.detailsLoaded) {
VCardUtils.savePeerProfileToDisk(vcard, accountId, from + ".vcf", mDeviceRuntimeService.provideFilesDir());
mVCardService.loadVCardProfile(vcard)
.subscribeOn(Schedulers.computation())
.subscribe(profile -> contact.setProfile(profile.first, profile.second));
}
}
account.addRequest(request);
handleTrustRequest(accountId, new Uri(from).getUri(), request, ContactType.INVITATION_RECEIVED);
if (account.isEnabled())
lookupAddress(accountId, "", from);
incomingRequestsSubject.onNext(request);
}
}
void contactAdded(String accountId, String uri, boolean confirmed) {
Account account = getAccount(accountId);
if (account != null) {
account.addContact(uri, confirmed);
if (account.isEnabled())
lookupAddress(accountId, "", uri);
}
}
void contactRemoved(String accountId, String uri, boolean banned) {
Account account = getAccount(accountId);
Log.d(TAG, "Contact removed: " + uri + " User is banned: " + banned);
if (account != null) {
mHistoryService.clearHistory(uri, accountId, true).subscribe();
account.removeContact(uri, banned);
}
}
void registeredNameFound(String accountId, int state, String address, String name) {
// Log.d(TAG, "registeredNameFound: " + accountId + ", " + state + ", " + name + ", " + address);
if (!StringUtils.isEmpty(address)) {
Account account = getAccount(accountId);
if (account != null) {
account.registeredNameFound(state, address, name);
}
}
RegisteredName r = new RegisteredName();
r.accountId = accountId;
r.address = address;
r.name = name;
r.state = state;
registeredNameSubject.onNext(r);
}
public void userSearchEnded(String accountId, int state, String query, ArrayList<Map<String, String>> results) {
Account account = getAccount(accountId);
UserSearchResult r = new UserSearchResult(accountId, query);
r.state = state;
r.results = new ArrayList<>(results.size());
for (Map<String, String> m : results) {
String uri = m.get("id");
String username = m.get("username");
String firstName = m.get("firstName");
String lastName = m.get("lastName");
String picture_b64 = m.get("profilePicture");
CallContact contact = account.getContactFromCache(uri);
if (contact != null) {
contact.setUsername(username);
contact.setProfile(firstName + " " + lastName, mVCardService.base64ToBitmap(picture_b64));
r.results.add(contact);
}
}
searchResultSubject.onNext(r);
}
public DataTransferError sendFile(final DataTransfer dataTransfer, File file) {
mStartingTransfer = dataTransfer;
DataTransferInfo dataTransferInfo = new DataTransferInfo();
dataTransferInfo.setAccountId(dataTransfer.getAccount());
dataTransferInfo.setPeer(dataTransfer.getPeerId());
dataTransferInfo.setPath(file.getPath());
dataTransferInfo.setDisplayName(dataTransfer.getDisplayName());
Log.i(TAG, "sendFile() id=" + dataTransfer.getId() + " accountId=" + dataTransferInfo.getAccountId() + ", peer=" + dataTransferInfo.getPeer() + ", filePath=" + dataTransferInfo.getPath());
try {
return getDataTransferError(mExecutor.submit(() -> Ringservice.sendFile(dataTransferInfo, 0)).get());
} catch (Exception ignored) {
}
return DataTransferError.UNKNOWN;
}
public List<cx.ring.daemon.Message> getLastMessages(String accountId, long baseTime) {
try {
return mExecutor.submit(() -> SwigNativeConverter.toJava(Ringservice.getLastMessages(accountId, baseTime))).get();
} catch (Exception e) {
e.printStackTrace();
}
return new ArrayList<>();
}
public void acceptFileTransfer(long id) {
acceptFileTransfer(getDataTransfer(id));
}
public void acceptFileTransfer(DataTransfer transfer) {
if (transfer == null)
return;
File path = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
acceptFileTransfer(transfer.getDaemonId(), path.getAbsolutePath(), 0);
}
private void acceptFileTransfer(final Long dataTransferId, final String filePath, final long offset) {
Log.i(TAG, "acceptFileTransfer() id=" + dataTransferId + ", path=" + filePath + ", offset=" + offset);
mExecutor.execute(() -> Ringservice.acceptFileTransfer(dataTransferId, filePath, offset));
}
public void cancelDataTransfer(final Long dataTransferId) {
Log.i(TAG, "cancelDataTransfer() id=" + dataTransferId);
mExecutor.execute(() -> Ringservice.cancelDataTransfer(dataTransferId));
}
private class DataTransferRefreshTask implements Runnable {
private final DataTransfer mToUpdate;
public ScheduledFuture<?> scheduledTask;
DataTransferRefreshTask(DataTransfer t) {
mToUpdate = t;
}
@Override
public void run() {
synchronized (mToUpdate) {
if (mToUpdate.getStatus() == Interaction.InteractionStatus.TRANSFER_ONGOING) {
dataTransferEvent(mToUpdate.getDaemonId(), 5);
} else {
scheduledTask.cancel(false);
scheduledTask = null;
}
}
}
}
void dataTransferEvent(final long transferId, int eventCode) {
Interaction.InteractionStatus transferStatus = getDataTransferEventCode(eventCode);
Log.d(TAG, "Data Transfer " + transferStatus);
DataTransferInfo info = new DataTransferInfo();
if (getDataTransferError(Ringservice.dataTransferInfo(transferId, info)) != DataTransferError.SUCCESS)
return;
Account account = getAccount(info.getAccountId());
Conversation c = account.getByUri(new Uri(info.getPeer()).getUri());
boolean outgoing = info.getFlags() == 0;
DataTransfer transfer = mDataTransfers.get(transferId);
if (transfer == null) {
if (outgoing && mStartingTransfer != null) {
transfer = mStartingTransfer;
mStartingTransfer = null;
} else {
transfer = new DataTransfer(c, account.getAccountID(), info.getDisplayName(),
outgoing, info.getTotalSize(),
info.getBytesProgress(), transferId);
mHistoryService.insertInteraction(account.getAccountID(), c, transfer).blockingAwait();
}
mDataTransfers.put(transferId, transfer);
} else synchronized (transfer) {
InteractionStatus oldState = transfer.getStatus();
if (oldState != transferStatus) {
if (transferStatus == Interaction.InteractionStatus.TRANSFER_ONGOING) {
DataTransferRefreshTask task = new DataTransferRefreshTask(transfer);
task.scheduledTask = mExecutor.scheduleAtFixedRate(task,
DATA_TRANSFER_REFRESH_PERIOD,
DATA_TRANSFER_REFRESH_PERIOD, TimeUnit.MILLISECONDS);
} else if (transferStatus.isError()) {
if (!transfer.isOutgoing()) {
File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
tmpPath.delete();
}
} else if (transferStatus == (Interaction.InteractionStatus.TRANSFER_FINISHED)) {
if (!transfer.isOutgoing()) {
File tmpPath = mDeviceRuntimeService.getTemporaryPath(transfer.getPeerId(), transfer.getStoragePath());
File path = mDeviceRuntimeService.getConversationPath(transfer.getPeerId(), transfer.getStoragePath());
FileUtils.moveFile(tmpPath, path);
}
}
}
transfer.setStatus(transferStatus);
transfer.setBytesProgress(info.getBytesProgress());
mHistoryService.updateInteraction(transfer, account.getAccountID()).subscribe();
}
dataTransferSubject.onNext(transfer);
}
private static Interaction.InteractionStatus getDataTransferEventCode(int eventCode) {
Interaction.InteractionStatus dataTransferEventCode = Interaction.InteractionStatus.INVALID;
try {
dataTransferEventCode = InteractionStatus.fromIntFile(eventCode);
} catch (ArrayIndexOutOfBoundsException ignored) {
Log.e(TAG, "getEventCode: invalid data transfer status from daemon");
}
return dataTransferEventCode;
}
private static DataTransferError getDataTransferError(Long errorCode) {
DataTransferError dataTransferError = DataTransferError.UNKNOWN;
if (errorCode == null) {
Log.e(TAG, "getDataTransferError: invalid error code");
} else {
try {
dataTransferError = DataTransferError.values()[errorCode.intValue()];
} catch (ArrayIndexOutOfBoundsException ignored) {
Log.e(TAG, "getDataTransferError: invalid data transfer error from daemon");
}
}
return dataTransferError;
}
public DataTransfer getDataTransfer(long id) {
return mDataTransfers.get(id);
}
public Subject<DataTransfer> getDataTransfers() {
return dataTransferSubject;
}
public Observable<DataTransfer> observeDataTransfer(DataTransfer transfer) {
return dataTransferSubject
.filter(t -> t == transfer)
.startWith(transfer);
}
public void setProxyEnabled(boolean enabled) {
mExecutor.execute(() -> {
for (Account acc : mAccountList) {
if (acc.isJami() && (acc.isDhtProxyEnabled() != enabled)) {
Log.d(TAG, (enabled ? "Enabling" : "Disabling") + " proxy for account " + acc.getAccountID());
acc.setDhtProxyEnabled(enabled);
StringMap details = Ringservice.getAccountDetails(acc.getAccountID());
details.put(ConfigKey.PROXY_ENABLED.key(), enabled ? "true" : "false");
Ringservice.setAccountDetails(acc.getAccountID(), details);
}
}
});
}
}