/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * 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, see <http://www.gnu.org/licenses/>. */ package com.waz.zclient.core.api.scala; import android.os.Handler; import com.waz.api.AssetForUpload; import com.waz.api.AudioAssetForUpload; import com.waz.api.ConversationsList; import com.waz.api.IConversation; import com.waz.api.ImageAsset; import com.waz.api.ImageAssetFactory; import com.waz.api.MessageContent; import com.waz.api.Subscriber; import com.waz.api.Subscription; import com.waz.api.SyncIndicator; import com.waz.api.SyncState; import com.waz.api.UiSignal; import com.waz.api.UpdateListener; import com.waz.api.User; import com.waz.api.VoiceChannel; import com.waz.api.VoiceChannelState; import com.waz.api.ZMessagingApi; import com.waz.zclient.core.stores.connect.InboxLinkConversation; import com.waz.zclient.core.stores.conversation.ConversationChangeRequester; import com.waz.zclient.core.stores.conversation.ConversationStoreObserver; import com.waz.zclient.core.stores.conversation.IConversationStore; import com.waz.zclient.core.stores.conversation.InboxLoadRequester; import com.waz.zclient.core.stores.conversation.OnConversationLoadedListener; import com.waz.zclient.core.stores.conversation.OnInboxLoadedListener; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import timber.log.Timber; public class ScalaConversationStore implements IConversationStore { public static final String TAG = ScalaConversationStore.class.getName(); private static final int ARCHIVE_DELAY = 500; // observers attached to a IConversationStore private Set<ConversationStoreObserver> conversationStoreObservers = new HashSet<>(); private ConversationsList conversationsList; private ConversationsList establishedConversationsList; private UiSignal<IConversation> conversationUiSignal; private Subscription selectedConvSubscription; private IConversation selectedConversation; private ConversationsList.SearchableConversationsList inboxList; private SyncIndicator syncIndicator; private IConversation menuConversation; private ConversationChangeRequester conversationChangeRequester; private final UpdateListener syncStateUpdateListener = new UpdateListener() { @Override public void updated() { notifySyncChanged(syncIndicator.getState()); } }; private final UpdateListener inboxListUpdateListener = new UpdateListener() { @Override public void updated() { notifyConversationListUpdated(); } }; private final UpdateListener menuConversationUpdateListener = new UpdateListener() { @Override public void updated() { notifyMenuConversationUpdated(); } }; private final UpdateListener conversationListUpdateListener = new UpdateListener() { @Override public void updated() { if (conversationsList.size() == 0 && conversationsList.isReady()) { conversationsList.setSelectedConversation(null); } boolean changeSelectedConversation = selectedConversation == null && (conversationsList.size() > 0 || inboxList.size() > 0); if (conversationsList.isReady() && !conversationUiSignal.isEmpty() && changeSelectedConversation) { identifyCurrentConversation(null); } notifyConversationListUpdated(); } }; public ScalaConversationStore(ZMessagingApi zMessagingApi) { conversationsList = zMessagingApi.getConversations(); establishedConversationsList = conversationsList.getEstablishedConversations(); inboxList = conversationsList.getIncomingConversations(); conversationUiSignal = conversationsList.selectedConversation(); selectedConvSubscription = conversationUiSignal.subscribe(new Subscriber<IConversation>() { @Override public void next(IConversation value) { IConversation prev = selectedConversation; selectedConversation = value; boolean changeSelectedConversation = selectedConversation == null && (conversationsList.size() > 0 || inboxList.size() > 0); if (conversationsList.isReady() && changeSelectedConversation) { identifyCurrentConversation(prev); } else { // TODO: Check with SE. In some cases like clicking on inapp-notification signal will also notify when conversation changes to another conversation boolean conversationChanged = (prev != null && selectedConversation != null && !prev.getId().equals(selectedConversation.getId())); ConversationChangeRequester changeRequester = conversationChanged ? conversationChangeRequester : ConversationChangeRequester.UPDATER; notifyCurrentConversationHasChanged(prev, selectedConversation, changeRequester); } } }); conversationsList.addUpdateListener(conversationListUpdateListener); conversationListUpdateListener.updated(); inboxList.addUpdateListener(inboxListUpdateListener); syncIndicator = conversationsList.getSyncIndicator(); syncIndicator.addUpdateListener(syncStateUpdateListener); } @Override public void tearDown() { if (selectedConvSubscription != null) { selectedConvSubscription.cancel(); } if (syncIndicator != null) { syncIndicator.removeUpdateListener(syncStateUpdateListener); syncIndicator = null; } if (conversationsList != null) { conversationsList.removeUpdateListener(conversationListUpdateListener); conversationsList = null; } if (inboxList != null) { inboxList.removeUpdateListener(inboxListUpdateListener); inboxList = null; } if (menuConversation != null) { menuConversation.removeUpdateListener(menuConversationUpdateListener); menuConversation = null; } establishedConversationsList = null; selectedConvSubscription = null; selectedConversation = null; conversationUiSignal = null; } @Override public void onLogout() { conversationsList.setSelectedConversation(null); } @Override public int getPositionInList(IConversation conversation) { return conversationsList.getConversationIndex(conversation.getId()); } @Override public IConversation getConversation(String conversationId) { if (conversationId == null || conversationsList == null) { return null; } return conversationsList.getConversation(conversationId); } @Override public void loadConversation(String conversationId, OnConversationLoadedListener onConversationLoadedListener) { IConversation conversation = conversationsList.getConversation(conversationId); onConversationLoadedListener.onConversationLoaded(conversation); } @Override public void setCurrentConversation(IConversation conversation, ConversationChangeRequester conversationChangerSender) { if (conversation instanceof InboxLinkConversation) { conversation = inboxList.get(0); } if (conversation != null) { conversation.setArchived(false); } if (conversation != null) { Timber.i("Set current conversation to %s, requester %s", conversation.getName(), conversationChangerSender); } else { Timber.i("Set current conversation to null, requester %s", conversationChangerSender); } this.conversationChangeRequester = conversationChangerSender; IConversation oldConversation = conversationChangerSender == ConversationChangeRequester.FIRST_LOAD ? null : selectedConversation; conversationsList.setSelectedConversation(conversation); if (oldConversation == null || (oldConversation != null && conversation != null && oldConversation.getId().equals(conversation.getId()))) { // Notify explicitly if the conversation doesn't change, the UiSginal notifies only when the conversation changes notifyCurrentConversationHasChanged(oldConversation, conversation, conversationChangerSender); } } @Override public IConversation getCurrentConversation() { return selectedConversation; } @Override public String getCurrentConversationId() { return (selectedConversation == null) ? null : selectedConversation.getId(); } @Override public void loadCurrentConversation(OnConversationLoadedListener onConversationLoadedListener) { if (conversationsList != null && selectedConversation != null) { onConversationLoadedListener.onConversationLoaded(selectedConversation); } } @Override public void setCurrentConversationToNext(ConversationChangeRequester requester) { if (getNextConversation() == null) { return; } setCurrentConversation(getNextConversation(), requester); } @Override public IConversation getNextConversation() { if (conversationsList == null || conversationsList.size() == 0) { return null; } for (int i = 0; i < conversationsList.size(); i++) { IConversation previousConversation = i >= 1 ? conversationsList.get(i - 1) : null; IConversation conversation = conversationsList.get(i); IConversation nextConversation = i == (conversationsList.size() - 1) ? null : conversationsList.get(i + 1); if (selectedConversation.equals(conversation)) { if (nextConversation != null) { return nextConversation; } return previousConversation; } } return null; } @Override public void loadMenuConversation(String conversationId) { menuConversation = conversationsList.getConversation(conversationId); menuConversation.removeUpdateListener(menuConversationUpdateListener); menuConversation.addUpdateListener(menuConversationUpdateListener); menuConversationUpdateListener.updated(); } @Override public void loadConnectRequestInboxConversations(OnInboxLoadedListener onConversationsLoadedListener, InboxLoadRequester inboxLoadRequester) { final List<IConversation> matches = new ArrayList<>(); for (int i = 0; i < inboxList.size(); i++) { IConversation conversation = inboxList.get(i); if (isPendingIncomingConnectRequest(conversation)) { matches.add(conversation); } } onConversationsLoadedListener.onConnectRequestInboxConversationsLoaded(matches, inboxLoadRequester); } @Override public int getNumberOfActiveConversations() { if (establishedConversationsList == null) { return 0; } return establishedConversationsList.size(); } @Override public boolean hasOngoingCallInCurrentConversation() { if (selectedConversation == null) { return false; } VoiceChannel voiceChannel = selectedConversation.getVoiceChannel(); if (voiceChannel == null) { return false; } VoiceChannelState state = voiceChannel.getState(); return state != VoiceChannelState.NO_ACTIVE_USERS && state != VoiceChannelState.UNKNOWN; } @Override public SyncState getConversationSyncingState() { return syncIndicator.getState(); } @Override public void addConversationStoreObserver(ConversationStoreObserver conversationStoreObserver) { // Prevent concurrent modification (if this add was executed by one of current observers during notify* callback) Set<ConversationStoreObserver> observers = new HashSet<>(conversationStoreObservers); observers.add(conversationStoreObserver); conversationStoreObservers = observers; } @Override public void addConversationStoreObserverAndUpdate(ConversationStoreObserver conversationStoreObserver) { addConversationStoreObserver(conversationStoreObserver); if (selectedConversation != null) { conversationStoreObserver.onCurrentConversationHasChanged(null, selectedConversation, ConversationChangeRequester.UPDATER); conversationStoreObserver.onConversationSyncingStateHasChanged(getConversationSyncingState()); } if (conversationsList != null) { conversationStoreObserver.onConversationListUpdated(conversationsList); } } @Override public void removeConversationStoreObserver(ConversationStoreObserver conversationStoreObserver) { // Prevent concurrent modification if (conversationStoreObservers.contains(conversationStoreObserver)) { Set<ConversationStoreObserver> observers = new HashSet<>(conversationStoreObservers); observers.remove(conversationStoreObserver); conversationStoreObservers = observers; } } @Override public void createGroupConversation(Iterable<User> users, final ConversationChangeRequester conversationChangerSender) { conversationsList.createGroupConversation(users, new ConversationsList.ConversationCallback() { @Override public void onConversationsFound(Iterable<IConversation> iterable) { Iterator<IConversation> iterator = iterable.iterator(); if (!iterator.hasNext()) { return; } ConversationChangeRequester conversationChangeRequester = conversationChangerSender; if (conversationChangeRequester != ConversationChangeRequester.START_CONVERSATION_FOR_CALL && conversationChangeRequester != ConversationChangeRequester.START_CONVERSATION_FOR_VIDEO_CALL && conversationChangeRequester != ConversationChangeRequester.START_CONVERSATION_FOR_CAMERA) { conversationChangeRequester = ConversationChangeRequester.START_CONVERSATION; } setCurrentConversation(iterator.next(), conversationChangeRequester); } }); } @Override public void sendMessage(final String message) { sendMessage(getCurrentConversation(), message); } @Override public void sendMessage(IConversation conversation, String message) { if (conversation != null) { conversation.sendMessage(new MessageContent.Text(message)); } } @Override public void sendMessage(final byte[] jpegData) { IConversation current = getCurrentConversation(); if (current != null) { current.sendMessage(new MessageContent.Image(ImageAssetFactory.getImageAsset(jpegData))); } } @Override public void sendMessage(ImageAsset imageAsset) { sendMessage(getCurrentConversation(), imageAsset); } @Override public void sendMessage(MessageContent.Location location) { if (getCurrentConversation() == null) { return; } getCurrentConversation().sendMessage(location); } @Override public void sendMessage(AssetForUpload assetForUpload, MessageContent.Asset.ErrorHandler errorHandler) { sendMessage(getCurrentConversation(), assetForUpload, errorHandler); } @Override public void sendMessage(IConversation conversation, AssetForUpload assetForUpload, MessageContent.Asset.ErrorHandler errorHandler) { if (conversation != null) { Timber.i("Send file to %s", conversation.getName()); conversation.sendMessage(new MessageContent.Asset(assetForUpload, errorHandler)); } } @Override public void sendMessage(IConversation conversation, ImageAsset imageAsset) { if (conversation != null) { conversation.sendMessage(new MessageContent.Image(imageAsset)); } } @Override public void sendMessage(AudioAssetForUpload audioAssetForUpload, MessageContent.Asset.ErrorHandler errorHandler) { sendMessage(getCurrentConversation(), audioAssetForUpload, errorHandler); } @Override public void sendMessage(IConversation conversation, AudioAssetForUpload audioAssetForUpload, MessageContent.Asset.ErrorHandler errorHandler) { if (conversation != null) { Timber.i("Send audio file to %s", conversation.getName()); conversation.sendMessage(new MessageContent.Asset(audioAssetForUpload, errorHandler)); } } @Override public void knockCurrentConversation() { if (getCurrentConversation() != null) { getCurrentConversation().knock(); } } @Override public void mute() { mute(getCurrentConversation(), !getCurrentConversation().isMuted()); } @Override public void mute(IConversation conversation, boolean mute) { conversation.setMuted(mute); } @Override public void archive(IConversation conversation, boolean archive) { if (conversation.isSelected()) { final IConversation nextConversation = getNextConversation(); if (nextConversation != null) { // don't want to change selected item immediately new Handler().postDelayed(new Runnable() { @Override public void run() { if (conversationsList != null) { setCurrentConversation(nextConversation, ConversationChangeRequester.ARCHIVED_RESULT); } } }, ARCHIVE_DELAY); } } conversation.setArchived(archive); // Set current conversation to unarchived if (!archive) { setCurrentConversation(conversation, ConversationChangeRequester.CONVERSATION_LIST_UNARCHIVED_CONVERSATION); } } @Override public void leave(IConversation conversation) { conversation.leave(); } @Override public void deleteConversation(IConversation conversation, boolean leaveConversation) { if (leaveConversation) { conversation.leave(); } else { conversation.clear(); } } private void notifyConversationListUpdated() { for (ConversationStoreObserver conversationStoreObserver : conversationStoreObservers) { conversationStoreObserver.onConversationListUpdated(conversationsList); } } private void notifyMenuConversationUpdated() { for (ConversationStoreObserver conversationStoreObserver : conversationStoreObservers) { conversationStoreObserver.onMenuConversationHasChanged(menuConversation); } } protected void notifyCurrentConversationHasChanged(IConversation fromConversation, IConversation toConversation, ConversationChangeRequester conversationChangerSender) { for (ConversationStoreObserver conversationStoreObserver : conversationStoreObservers) { conversationStoreObserver.onCurrentConversationHasChanged(fromConversation, toConversation, conversationChangerSender); } } protected void notifySyncChanged(SyncState syncState) { for (ConversationStoreObserver observer : conversationStoreObservers) { observer.onConversationSyncingStateHasChanged(syncState); } } private void identifyCurrentConversation(IConversation previousSelectedConversation) { if (selectedConversation == null) { if (previousSelectedConversation != null && previousSelectedConversation.getType() == IConversation.Type.INCOMING_CONNECTION) { // Switch to another incoming connect request. // Previous (ignored) conversation might still be included in list of incoming conversations, find another one for (int i = 0; i < inboxList.size(); i++) { IConversation incomingConnectRequest = inboxList.get(i); if (!incomingConnectRequest.getId().equals(previousSelectedConversation.getId())) { setCurrentConversation(incomingConnectRequest, ConversationChangeRequester.FIRST_LOAD); return; } } } // TODO: AN-2974 if (conversationsList.size() > 0) { setCurrentConversation(conversationsList.get(0), ConversationChangeRequester.FIRST_LOAD); return; } } setCurrentConversation(selectedConversation, ConversationChangeRequester.FIRST_LOAD); } private boolean isPendingIncomingConnectRequest(IConversation conversation) { if (conversation.isMe() || conversation.getType() == IConversation.Type.GROUP) { return false; } return conversation.getType() == IConversation.Type.INCOMING_CONNECTION; } }