| /* |
| * 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); |
| } |
| } |
| }); |
| } |
| } |