/**
* 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.pages.main.conversation;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.ActionMenuView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.waz.api.AssetFactory;
import com.waz.api.AssetForUpload;
import com.waz.api.AudioAssetForUpload;
import com.waz.api.AudioEffect;
import com.waz.api.ConversationsList;
import com.waz.api.EphemeralExpiration;
import com.waz.api.ErrorsList;
import com.waz.api.IConversation;
import com.waz.api.ImageAsset;
import com.waz.api.ImageAssetFactory;
import com.waz.api.InputStateIndicator;
import com.waz.api.Message;
import com.waz.api.MessageContent;
import com.waz.api.NetworkMode;
import com.waz.api.OtrClient;
import com.waz.api.Self;
import com.waz.api.SyncIndicator;
import com.waz.api.SyncState;
import com.waz.api.UpdateListener;
import com.waz.api.User;
import com.waz.api.UsersList;
import com.waz.api.Verification;
import com.waz.utils.wrappers.URI;
import com.waz.zclient.BaseActivity;
import com.waz.zclient.BuildConfig;
import com.waz.zclient.OnBackPressedListener;
import com.waz.zclient.R;
import com.waz.zclient.WireContext;
import com.waz.zclient.camera.controllers.GlobalCameraController;
import com.waz.zclient.controllers.IControllerFactory;
import com.waz.zclient.controllers.accentcolor.AccentColorObserver;
import com.waz.zclient.controllers.calling.CallingObserver;
import com.waz.zclient.controllers.confirmation.ConfirmationCallback;
import com.waz.zclient.controllers.confirmation.ConfirmationRequest;
import com.waz.zclient.controllers.confirmation.IConfirmationController;
import com.waz.zclient.controllers.currentfocus.IFocusController;
import com.waz.zclient.controllers.drawing.DrawingController;
import com.waz.zclient.controllers.drawing.IDrawingController;
import com.waz.zclient.controllers.giphy.GiphyObserver;
import com.waz.zclient.controllers.globallayout.KeyboardVisibilityObserver;
import com.waz.zclient.controllers.mentioning.MentioningObserver;
import com.waz.zclient.controllers.navigation.NavigationControllerObserver;
import com.waz.zclient.controllers.navigation.Page;
import com.waz.zclient.controllers.navigation.PagerControllerObserver;
import com.waz.zclient.controllers.permission.RequestPermissionsObserver;
import com.waz.zclient.controllers.singleimage.SingleImageObserver;
import com.waz.zclient.controllers.tracking.events.conversation.EditedMessageEvent;
import com.waz.zclient.controllers.tracking.events.navigation.OpenedMoreActionsEvent;
import com.waz.zclient.conversation.CollectionController;
import com.waz.zclient.core.api.scala.ModelObserver;
import com.waz.zclient.core.controllers.tracking.attributes.OpenedMediaAction;
import com.waz.zclient.core.controllers.tracking.events.filetransfer.SelectedTooLargeFileEvent;
import com.waz.zclient.core.controllers.tracking.events.media.CancelledRecordingAudioMessageEvent;
import com.waz.zclient.core.controllers.tracking.events.media.OpenedActionHintEvent;
import com.waz.zclient.core.controllers.tracking.events.media.OpenedEmojiKeyboardEvent;
import com.waz.zclient.core.controllers.tracking.events.media.OpenedMediaActionEvent;
import com.waz.zclient.core.controllers.tracking.events.media.PreviewedAudioMessageEvent;
import com.waz.zclient.core.controllers.tracking.events.media.SentPictureEvent;
import com.waz.zclient.core.controllers.tracking.events.media.SentVideoMessageEvent;
import com.waz.zclient.core.controllers.tracking.events.media.StartedRecordingAudioMessageEvent;
import com.waz.zclient.core.stores.IStoreFactory;
import com.waz.zclient.core.stores.conversation.ConversationChangeRequester;
import com.waz.zclient.core.stores.conversation.ConversationStoreObserver;
import com.waz.zclient.core.stores.inappnotification.InAppNotificationStoreObserver;
import com.waz.zclient.core.stores.inappnotification.KnockingEvent;
import com.waz.zclient.core.stores.network.DefaultNetworkAction;
import com.waz.zclient.core.stores.participants.ParticipantsStoreObserver;
import com.waz.zclient.media.SoundController;
import com.waz.zclient.messages.MessagesListView;
import com.waz.zclient.messages.controllers.EditActionSupport;
import com.waz.zclient.pages.BaseFragment;
import com.waz.zclient.pages.extendedcursor.ExtendedCursorContainer;
import com.waz.zclient.pages.extendedcursor.emoji.EmojiKeyboardLayout;
import com.waz.zclient.pages.extendedcursor.ephemeral.EphemeralLayout;
import com.waz.zclient.pages.extendedcursor.image.CursorImagesLayout;
import com.waz.zclient.pages.extendedcursor.image.ImagePreviewLayout;
import com.waz.zclient.pages.extendedcursor.voicefilter.VoiceFilterLayout;
import com.waz.zclient.pages.main.calling.enums.VoiceBarAppearance;
import com.waz.zclient.pages.main.conversation.views.MessageBottomSheetDialog;
import com.waz.zclient.pages.main.conversation.views.TypingIndicatorView;
import com.waz.zclient.pages.main.conversationlist.ConversationListAnimation;
import com.waz.zclient.pages.main.conversationpager.controller.SlidingPaneObserver;
import com.waz.zclient.pages.main.onboarding.OnBoardingHintFragment;
import com.waz.zclient.pages.main.onboarding.OnBoardingHintType;
import com.waz.zclient.pages.main.pickuser.controller.IPickUserController;
import com.waz.zclient.pages.main.profile.ZetaPreferencesActivity;
import com.waz.zclient.pages.main.profile.camera.CameraContext;
import com.waz.zclient.tracking.GlobalTrackingController;
import com.waz.zclient.ui.animation.interpolators.penner.Expo;
import com.waz.zclient.ui.audiomessage.AudioMessageRecordingView;
import com.waz.zclient.ui.cursor.CursorCallback;
import com.waz.zclient.ui.cursor.CursorLayout;
import com.waz.zclient.ui.cursor.CursorMenuItem;
import com.waz.zclient.ui.utils.KeyboardUtils;
import com.waz.zclient.ui.views.e2ee.ShieldView;
import com.waz.zclient.utils.AssetUtils;
import com.waz.zclient.utils.Callback;
import com.waz.zclient.utils.LayoutSpec;
import com.waz.zclient.utils.PermissionUtils;
import com.waz.zclient.utils.TrackingUtils;
import com.waz.zclient.utils.ViewUtils;
import com.waz.zclient.views.LoadingIndicatorView;
import com.waz.zclient.views.MentioningFragment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConversationFragment extends BaseFragment<ConversationFragment.Container> implements ConversationStoreObserver,
CallingObserver,
OnBoardingHintFragment.Container,
KeyboardVisibilityObserver,
AccentColorObserver,
ParticipantsStoreObserver,
InAppNotificationStoreObserver,
NavigationControllerObserver,
SlidingPaneObserver,
SingleImageObserver,
MentioningObserver,
GiphyObserver,
OnBackPressedListener,
CursorCallback,
AudioMessageRecordingView.Callback,
RequestPermissionsObserver,
ImagePreviewLayout.Callback,
AssetIntentsManager.Callback,
PagerControllerObserver,
CursorImagesLayout.Callback,
VoiceFilterLayout.Callback,
EmojiKeyboardLayout.Callback,
ExtendedCursorContainer.Callback,
EphemeralLayout.Callback,
TypingIndicatorView.Callback {
public static final String TAG = ConversationFragment.class.getName();
private static final String SAVED_STATE_PREVIEW = "SAVED_STATE_PREVIEW";
private static final int REQUEST_VIDEO_CAPTURE = 911;
private static final int CAMERA_PERMISSION_REQUEST_ID = 21;
private static final String[] EXTENDED_CURSOR_PERMISSIONS = new String[] {Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE};
private static final int OPEN_EXTENDED_CURSOR_IMAGES = 1254;
private static final String[] FILE_SHARING_PERMISSION = new String[] {android.Manifest.permission.READ_EXTERNAL_STORAGE};
private static final int FILE_SHARING_PERMISSION_REQUEST_ID = 179;
private static final String[] AUDIO_PERMISSION = new String[] {android.Manifest.permission.RECORD_AUDIO};
private static final int AUDIO_PERMISSION_REQUEST_ID = 864;
private static final int AUDIO_FILTER_PERMISSION_REQUEST_ID = 865;
private InputStateIndicator inputStateIndicator;
private UpdateListener typingListener;
private TypingIndicatorView typingIndicatorView;
private LoadingIndicatorView conversationLoadingIndicatorViewView;
private FrameLayout invisibleFooter;
private IConversation.Type toConversationType;
private Toolbar toolbar;
private ActionMenuView leftMenu;
private TextView toolbarTitle;
private ShieldView shieldView;
private CursorLayout cursorLayout;
private AudioMessageRecordingView audioMessageRecordingView;
private ExtendedCursorContainer extendedCursorContainer;
private List<URI> sharingUris = new ArrayList<>();
private AssetIntentsManager assetIntentsManager;
private ViewGroup containerPreview;
private boolean isPreviewShown;
private boolean isVideoMessageButtonClicked;
private MessageBottomSheetDialog messageBottomSheetDialog;
private MessagesListView listView;
private Self self = null;
public static ConversationFragment newInstance() {
return new ConversationFragment();
}
private final ModelObserver<IConversation> conversationModelObserver = new ModelObserver<IConversation>() {
@Override
public void updated(IConversation model) {
if (toolbar == null ||
toolbarTitle == null ||
shieldView == null) {
return;
}
shieldView.setVisibility(model.getVerified() == Verification.VERIFIED ? View.VISIBLE : View.GONE);
toolbarTitle.setText(model.getName());
toolbar.getMenu().clear();
if (!model.isMemberOfConversation()) {
return;
}
inflateCollectionIcon();
if (model.getType() == IConversation.Type.ONE_TO_ONE) {
toolbar.inflateMenu(R.menu.conversation_header_menu_video);
} else {
toolbar.inflateMenu(R.menu.conversation_header_menu_audio);
}
}
};
private final ModelObserver<SyncIndicator> syncIndicatorModelObserver = new ModelObserver<SyncIndicator>() {
@Override
public void updated(SyncIndicator syncIndicator) {
switch (syncIndicator.getState()) {
case SYNCING:
case WAITING:
conversationLoadingIndicatorViewView.show();
getControllerFactory().getLoadTimeLoggerController().conversationContentSyncStart();
return;
case COMPLETED:
case FAILED:
default:
conversationLoadingIndicatorViewView.hide();
getControllerFactory().getLoadTimeLoggerController().conversationContentSyncFinish();
}
}
};
private final MessageContent.Asset.ErrorHandler assetErrorHandler = new MessageContent.Asset.ErrorHandler() {
@Override
public void noWifiAndFileIsLarge(long sizeInBytes, NetworkMode net, final MessageContent.Asset.Answer answer) {
if (getActivity() == null) {
answer.ok();
return;
}
AlertDialog dialog = ViewUtils.showAlertDialog(getActivity(),
R.string.asset_upload_warning__large_file__title,
R.string.asset_upload_warning__large_file__message_default,
R.string.asset_upload_warning__large_file__button_accept,
R.string.asset_upload_warning__large_file__button_cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
answer.ok();
}
},
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
answer.cancel();
}
}
);
dialog.setCancelable(false);
if (sizeInBytes > 0) {
String fileSize = Formatter.formatFileSize(getContext(), sizeInBytes);
dialog.setMessage(getString(R.string.asset_upload_warning__large_file__message, fileSize));
}
}
};
private final MessageContent.Asset.ErrorHandler assetErrorHandlerVideo = new MessageContent.Asset.ErrorHandler() {
@Override
public void noWifiAndFileIsLarge(long sizeInBytes, NetworkMode net, final MessageContent.Asset.Answer answer) {
if (getActivity() == null) {
answer.ok();
return;
}
AlertDialog dialog = ViewUtils.showAlertDialog(getActivity(),
R.string.asset_upload_warning__large_file__title,
R.string.asset_upload_warning__large_file__message_default,
R.string.asset_upload_warning__large_file__button_accept,
R.string.asset_upload_warning__large_file__button_cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
answer.ok();
}
},
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
answer.cancel();
}
}
);
dialog.setCancelable(false);
if (sizeInBytes > 0) {
dialog.setMessage(getString(R.string.asset_upload_warning__large_file__message__video));
}
}
};
private final MessageContent.Asset.ErrorHandler assetErrorHandlerAudio = new MessageContent.Asset.ErrorHandler() {
@Override
public void noWifiAndFileIsLarge(long sizeInBytes, NetworkMode net, MessageContent.Asset.Answer answer) {
answer.ok();
}
};
private final ModelObserver<Self> selfModelObserver = new ModelObserver<Self>() {
@Override
public void updated(Self model) {
self = model;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Lifecycle
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == 0 ||
getContainer() == null ||
getControllerFactory().isTornDown()) {
return super.onCreateAnimation(transit, enter, nextAnim);
} else if (nextAnim == R.anim.fragment_animation_swap_profile_conversation_tablet_in || nextAnim == R.anim.fragment_animation_swap_profile_conversation_tablet_out) {
int width = ViewUtils.getOrientationDependentDisplayWidth(getActivity()) - getResources().getDimensionPixelSize(
R.dimen.framework__sidebar_width);
return new MessageStreamAnimation(enter,
getResources().getInteger(R.integer.wire__animation__duration__medium),
0,
width);
} else if (getControllerFactory().getPickUserController().isHideWithoutAnimations()) {
return new ConversationListAnimation(0,
getResources().getDimensionPixelSize(R.dimen.open_new_conversation__thread_list__max_top_distance),
enter,
0,
0,
false,
1f);
} else if (enter) {
return new ConversationListAnimation(0,
getResources().getDimensionPixelSize(R.dimen.open_new_conversation__thread_list__max_top_distance),
enter,
getResources().getInteger(R.integer.framework_animation_duration_long),
getResources().getInteger(R.integer.framework_animation_duration_medium),
false,
1f);
}
return new ConversationListAnimation(0,
getResources().getDimensionPixelSize(R.dimen.open_new_conversation__thread_list__max_top_distance),
enter,
getResources().getInteger(R.integer.framework_animation_duration_medium),
0,
false,
1f);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
assetIntentsManager = new AssetIntentsManager(getActivity(), this, savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_conversation, viewGroup, false);
extendedCursorContainer = ViewUtils.getView(view, R.id.ecc__conversation);
containerPreview = ViewUtils.getView(view, R.id.fl__conversation_overlay);
cursorLayout = ViewUtils.getView(view, R.id.cl__cursor);
new EditActionSupport((WireContext) getActivity(), cursorLayout); // TODO: remove that once cursorLayout is reimplemented in scala
audioMessageRecordingView = ViewUtils.getView(view, R.id.amrv_audio_message_recording);
toolbar = ViewUtils.getView(view, R.id.t_conversation_toolbar);
leftMenu = ViewUtils.getView(view, R.id.conversation_left_menu);
toolbarTitle = ViewUtils.getView(toolbar, R.id.tv__conversation_toolbar__title);
shieldView = ViewUtils.getView(view, R.id.sv__conversation_toolbar__verified_shield);
shieldView.setVisibility(View.GONE);
typingIndicatorView = ViewUtils.getView(view, R.id.tiv_typing_indicator_view);
typingIndicatorView.setCallback(this);
listView = ViewUtils.getView(view, R.id.messages_list_view);
toolbar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getControllerFactory().getConversationScreenController().showParticipants(toolbar, false);
}
});
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_audio_call:
getControllerFactory().getCallingController().startCall(false);
cursorLayout.closeEditMessage(false);
return true;
case R.id.action_video_call:
getControllerFactory().getCallingController().startCall(true);
cursorLayout.closeEditMessage(false);
return true;
}
return false;
}
});
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (cursorLayout == null || (LayoutSpec.isTablet(getContext()) && ViewUtils.isInLandscape(getContext()))) {
return;
}
cursorLayout.closeEditMessage(false);
getActivity().onBackPressed();
KeyboardUtils.closeKeyboardIfShown(getActivity());
}
});
leftMenu.setOnMenuItemClickListener(new ActionMenuView.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_collection:
getCollectionController().openCollection();
return true;
}
return false;
}
});
if (LayoutSpec.isTablet(getContext()) && ViewUtils.isInLandscape(getContext())) {
toolbar.setNavigationIcon(null);
}
conversationLoadingIndicatorViewView = ViewUtils.getView(view, R.id.lbv__conversation__loading_indicator);
if (BuildConfig.SHOW_MENTIONING) {
getChildFragmentManager().beginTransaction()
.add(R.id.fl__conversation_overlay,
MentioningFragment.getInstance(),
MentioningFragment.TAG)
.commit();
}
// invisible footer to scroll over inputfield
invisibleFooter = new FrameLayout(getActivity());
AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getResources().getDimensionPixelSize(R.dimen.cursor__list_view_footer__height));
invisibleFooter.setLayoutParams(params);
cursorLayout.showSendButton(false);
// Recording audio messages
audioMessageRecordingView.setCallback(this);
if (LayoutSpec.isTablet(getActivity())) {
view.setBackgroundColor(Color.WHITE);
}
if (savedInstanceState != null) {
isPreviewShown = savedInstanceState.getBoolean(SAVED_STATE_PREVIEW, false);
}
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
audioMessageRecordingView.setVisibility(View.INVISIBLE);
}
@Override
public void onStart() {
super.onStart();
getControllerFactory().getGlobalLayoutController().addKeyboardHeightObserver(extendedCursorContainer);
getControllerFactory().getGlobalLayoutController().addKeyboardVisibilityObserver(extendedCursorContainer);
getControllerFactory().getRequestPermissionsController().addObserver(this);
cursorLayout.setCursorCallback(this);
cursorLayout.showSendButtonAsEnterKey(!getControllerFactory().getUserPreferencesController().isCursorSendButtonEnabled());
hideSendButtonIfNeeded();
final String draftText = getStoreFactory().getDraftStore().getDraft(getStoreFactory().getConversationStore().getCurrentConversation());
if (!TextUtils.isEmpty(draftText)) {
cursorLayout.setText(draftText);
}
if (BuildConfig.SHOW_MENTIONING) {
getControllerFactory().getMentioningController().addObserver(this);
}
syncIndicatorModelObserver.resumeListening();
audioMessageRecordingView.setDarkTheme(getControllerFactory().getThemeController().isDarkTheme());
if (!getControllerFactory().getConversationScreenController().isConversationStreamUiInitialized()) {
getStoreFactory().getConversationStore().addConversationStoreObserverAndUpdate(this);
} else {
getStoreFactory().getConversationStore().addConversationStoreObserver(this);
}
getControllerFactory().getNavigationController().addNavigationControllerObserver(this);
getControllerFactory().getNavigationController().addPagerControllerObserver(this);
getControllerFactory().getGiphyController().addObserver(this);
getControllerFactory().getSingleImageController().addSingleImageObserver(this);
getControllerFactory().getAccentColorController().addAccentColorObserver(this);
getStoreFactory().getParticipantsStore().addParticipantsStoreObserver(this);
getControllerFactory().getGlobalLayoutController().addKeyboardVisibilityObserver(this);
getStoreFactory().getInAppNotificationStore().addInAppNotificationObserver(this);
getControllerFactory().getSlidingPaneController().addObserver(this);
extendedCursorContainer.setCallback(this);
selfModelObserver.setAndUpdate(getStoreFactory().getZMessagingApiStore().getApi().getSelf());
}
@Override
public void onResume() {
super.onResume();
if (LayoutSpec.isTablet(getContext())) {
conversationModelObserver.setAndUpdate(getStoreFactory().getConversationStore().getCurrentConversation());
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
assetIntentsManager.onSaveInstanceState(outState);
outState.putBoolean(SAVED_STATE_PREVIEW, isPreviewShown);
}
@Override
public void onPause() {
super.onPause();
KeyboardUtils.hideKeyboard(getActivity());
hideAudioMessageRecording();
if (messageBottomSheetDialog != null) {
if (messageBottomSheetDialog.isShowing()) {
messageBottomSheetDialog.dismiss();
}
messageBottomSheetDialog = null;
}
}
@Override
public void onStop() {
extendedCursorContainer.close(true);
extendedCursorContainer.setCallback(null);
cursorLayout.setCursorCallback(null);
getControllerFactory().getGlobalLayoutController().removeKeyboardHeightObserver(extendedCursorContainer);
getControllerFactory().getGlobalLayoutController().removeKeyboardVisibilityObserver(extendedCursorContainer);
if (BuildConfig.SHOW_MENTIONING) {
getControllerFactory().getMentioningController().removeObserver(this);
}
getControllerFactory().getGiphyController().removeObserver(this);
getControllerFactory().getSingleImageController().removeSingleImageObserver(this);
if (!cursorLayout.isEditingMessage()) {
getStoreFactory().getDraftStore().setDraft(getStoreFactory().getConversationStore().getCurrentConversation(),
cursorLayout.getText().trim());
}
getStoreFactory().getInAppNotificationStore().removeInAppNotificationObserver(this);
getStoreFactory().getParticipantsStore().removeParticipantsStoreObserver(this);
getControllerFactory().getGlobalLayoutController().removeKeyboardVisibilityObserver(this);
getControllerFactory().getNavigationController().removePagerControllerObserver(this);
// messagesListModelObserver.pauseListening();
syncIndicatorModelObserver.pauseListening();
getStoreFactory().getConversationStore().removeConversationStoreObserver(this);
getControllerFactory().getAccentColorController().removeAccentColorObserver(this);
getControllerFactory().getNavigationController().removeNavigationControllerObserver(this);
getControllerFactory().getSlidingPaneController().removeObserver(this);
getControllerFactory().getConversationScreenController().setConversationStreamUiReady(false);
getControllerFactory().getRequestPermissionsController().removeObserver(this);
selfModelObserver.pauseListening();
super.onStop();
}
@Override
public void onDestroyView() {
containerPreview = null;
cursorLayout = null;
conversationLoadingIndicatorViewView = null;
if (inputStateIndicator != null) {
inputStateIndicator.removeUpdateListener(typingListener);
inputStateIndicator = null;
}
typingIndicatorView.clear();
typingIndicatorView = null;
typingListener = null;
conversationModelObserver.clear();
toolbarTitle = null;
toolbar = null;
selfModelObserver.clear();
super.onDestroyView();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Container implementations
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onShowSingleImage(Message message) {
}
@Override
public void onShowUserImage(User user) {
}
@Override
public void onHideSingleImage() {
getControllerFactory().getNavigationController().setRightPage(Page.MESSAGE_STREAM, TAG);
}
@Override
public void onShowVideo(URI uri) {
}
@Override
public void onHideVideo() {
getControllerFactory().getNavigationController().setRightPage(Page.MESSAGE_STREAM, TAG);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Notifications
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConversationListUpdated(@NonNull ConversationsList conversationsList) {
}
@Override
public void onConversationListStateHasChanged(ConversationsList.ConversationsListState state) {
}
@Override
public void onCurrentConversationHasChanged(final IConversation fromConversation,
final IConversation toConversation,
final ConversationChangeRequester conversationChangeRequester) {
if (toConversation == null) {
return;
}
if (LayoutSpec.isPhone(getContext())) {
conversationModelObserver.setAndUpdate(toConversation);
}
if (isPreviewShown && fromConversation != null && !toConversation.getId().equals(fromConversation.getId())) {
onCancelPreview();
}
extendedCursorContainer.close(true);
getControllerFactory().getConversationScreenController().setSingleConversation(toConversation.getType() == IConversation.Type.ONE_TO_ONE);
if (BuildConfig.SHOW_MENTIONING) {
getControllerFactory().getMentioningController().setCurrentConversation(toConversation);
}
int duration = getResources().getInteger(R.integer.framework_animation_duration_short);
// post to give the RootFragment the chance to drive its animations first
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (cursorLayout == null) {
return;
}
final boolean changeToDifferentConversation = fromConversation == null ||
!fromConversation.getId().equals(toConversation.getId());
// handle draft
if (fromConversation != null && changeToDifferentConversation &&
!cursorLayout.isEditingMessage()) {
getStoreFactory().getDraftStore().setDraft(fromConversation, cursorLayout.getText().trim());
}
if (toConversation.getType() == IConversation.Type.WAIT_FOR_CONNECTION) {
return;
}
KeyboardUtils.hideKeyboard(getActivity());
conversationLoadingIndicatorViewView.hide();
cursorLayout.enableMessageWriting();
if (changeToDifferentConversation) {
getControllerFactory().getConversationScreenController().setConversationStreamUiReady(false);
toConversationType = toConversation.getType();
// messagesListModelObserver.setAndUpdate(toConversation.getMessages());
getControllerFactory().getSharingController().maybeResetSharedText(fromConversation);
getControllerFactory().getSharingController().maybeResetSharedUris(fromConversation);
cursorLayout.setVisibility(toConversation.isActive() ? View.VISIBLE : View.GONE);
if (!inSplitPortraitMode()) {
resetCursor();
}
final String draftText = getStoreFactory().getDraftStore().getDraft(toConversation);
if (TextUtils.isEmpty(draftText)) {
resetCursor();
} else {
cursorLayout.setText(draftText);
}
cursorLayout.setConversation(toConversation);
hideAudioMessageRecording();
}
final boolean isSharing = getControllerFactory().getSharingController().isSharedConversation(
toConversation);
final boolean isSharingText = !TextUtils.isEmpty(getControllerFactory().getSharingController().getSharedText()) && isSharing;
final List<URI> sharedFileUris = getControllerFactory().getSharingController().getSharedFileUris();
final boolean isSharingFiles = !(sharedFileUris == null || sharedFileUris.isEmpty()) && isSharing;
if (isSharing) {
if (isSharingText) {
final String draftText = getControllerFactory().getSharingController().getSharedText();
if (TextUtils.isEmpty(draftText)) {
resetCursor();
} else {
cursorLayout.setText(draftText);
}
cursorLayout.enableMessageWriting();
KeyboardUtils.showKeyboard(getActivity());
getControllerFactory().getSharingController().maybeResetSharedText(toConversation);
} else if (isSharingFiles) {
if (PermissionUtils.hasSelfPermissions(getActivity(), FILE_SHARING_PERMISSION)) {
for (URI uri : sharedFileUris) {
getStoreFactory().getConversationStore().sendMessage(AssetFactory.fromContentUri(uri),
assetErrorHandler);
}
} else {
sharingUris.addAll(sharedFileUris);
ActivityCompat.requestPermissions(getActivity(),
FILE_SHARING_PERMISSION,
FILE_SHARING_PERMISSION_REQUEST_ID);
}
getControllerFactory().getSharingController().maybeResetSharedUris(toConversation);
}
}
if (inputStateIndicator != null) {
inputStateIndicator.getTypingUsers().removeUpdateListener(typingListener);
}
inputStateIndicator = toConversation.getInputStateIndicator();
typingIndicatorView.setInputStateIndicator(inputStateIndicator);
if (inputStateIndicator != null) {
inputStateIndicator.getTypingUsers().addUpdateListener(typingListener);
}
}
}, duration);
// Saving factories since this fragment may be re-created before the runnable is done,
// but we still want runnable to work.
final IStoreFactory storeFactory = getStoreFactory();
final IControllerFactory controllerFactory = getControllerFactory();
// TODO: Remove when call issue is resolved with https://wearezeta.atlassian.net/browse/CM-645
// And also why do we use the ConversationFragment to start a call from somewhere else....
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (storeFactory == null || storeFactory.isTornDown() ||
controllerFactory == null || controllerFactory.isTornDown()) {
return;
}
switch (conversationChangeRequester) {
case START_CONVERSATION_FOR_VIDEO_CALL:
controllerFactory.getCallingController().startCall(true);
break;
case START_CONVERSATION_FOR_CALL:
controllerFactory.getCallingController().startCall(false);
break;
case START_CONVERSATION_FOR_CAMERA:
controllerFactory.getCameraController().openCamera(CameraContext.MESSAGE);
break;
}
}
}, 1000);
}
@Override
public void onConversationSyncingStateHasChanged(SyncState syncState) {
}
@Override
public void onMenuConversationHasChanged(IConversation fromConversation) {
}
@Override
public void onCursorPositionChanged(float x, float y) {
}
@Override
public void onQueryResultChanged(@NonNull List<User> usersList) {
}
@Override
public void onMentionedUserSelected(@NonNull String query, @NonNull User user) {
final int cursorPosition = cursorLayout.getSelection();
final String text = cursorLayout.getText();
if (cursorPosition == -1 || TextUtils.isEmpty(text)) {
cursorLayout.setText(text);
cursorLayout.setSelection(text.length());
return;
}
final String[] words = text.split(" ");
StringBuilder builder = new StringBuilder();
int desiredCursorPosition = 0;
boolean inserted = false;
for (int i = 0, wordsLength = words.length; i < wordsLength; i++) {
String word = words[i];
if (!inserted && builder.length() + word.length() + 1 > cursorPosition) {
final int diff = word.length() - 1 - query.length();
String rest = word.substring(query.length() + 1);
builder.append('@')
.append(user.getDisplayName())
.append(' ');
if (!TextUtils.isEmpty(rest)) {
builder.append(rest);
}
desiredCursorPosition = builder.length() - diff;
inserted = true;
} else {
builder.append(word);
}
if (i < wordsLength - 1) {
builder.append(' ');
}
}
cursorLayout.setText(builder.toString());
cursorLayout.setSelection(desiredCursorPosition);
}
@Override
public void onKeyboardVisibilityChanged(boolean keyboardIsVisible, int keyboardHeight, View currentFocus) {
cursorLayout.notifyKeyboardVisibilityChanged(keyboardIsVisible, currentFocus);
if (keyboardIsVisible &&
getControllerFactory().getFocusController().getCurrentFocus() == IFocusController.CONVERSATION_CURSOR &&
!cursorLayout.isEditingMessage()) {
getControllerFactory().getNavigationController().setMessageStreamState(VoiceBarAppearance.MINI);
}
if (!keyboardIsVisible) {
getControllerFactory().getNavigationController().setMessageStreamState(VoiceBarAppearance.FULL);
}
}
private void inflateCollectionIcon() {
MenuInflater inflater = getActivity().getMenuInflater();
leftMenu.getMenu().clear();
boolean searchInProgress = !getCollectionController().contentSearchQuery().currentValue("").get().originalString().isEmpty();
if (searchInProgress) {
inflater.inflate(R.menu.conversation_header_menu_collection_searching, leftMenu.getMenu());
} else {
inflater.inflate(R.menu.conversation_header_menu_collection, leftMenu.getMenu());
}
}
//////////////////////////////////////////////////////////////////////////////////////////
//
// Cursor callback
//
//////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onEditTextHasChanged(int cursorPosition, String text) {
if (inputStateIndicator != null) {
if (text.isEmpty()) {
inputStateIndicator.textCleared();
} else {
inputStateIndicator.textChanged();
}
}
if (getControllerFactory().getUserPreferencesController().isCursorSendButtonEnabled()) {
cursorLayout.showSendButton(!TextUtils.isEmpty(text));
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
assetIntentsManager.onActivityResult(requestCode, resultCode, data);
}
//////////////////////////////////////////////////////////////////////////////////////////
//
// AccentColorObserver
//
//////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAccentColorHasChanged(Object sender, int color) {
cursorLayout.setAccentColor(color);
conversationLoadingIndicatorViewView.setColor(color);
audioMessageRecordingView.setAccentColor(color);
extendedCursorContainer.setAccentColor(color);
}
@Override
public void onIncomingMessage(Message message) {
}
@Override
public void onIncomingKnock(KnockingEvent knock) {
}
@Override
public void conversationUpdated(IConversation conversation) {
if (conversation == null || getStoreFactory() == null || getStoreFactory().isTornDown()) {
return;
}
if (!LayoutSpec.isTablet(getActivity())) {
toolbarTitle.setText(conversation.getName());
}
if (cursorLayout == null) {
return;
}
final IConversation currentConversation = getStoreFactory().getConversationStore().getCurrentConversation();
if (currentConversation == null || !currentConversation.isMemberOfConversation()) {
cursorLayout.setVisibility(View.GONE);
} else {
cursorLayout.setVisibility(View.VISIBLE);
}
}
@Override
public void participantsUpdated(UsersList participants) {
}
@Override
public void otherUserUpdated(User otherUser) {
}
//////////////////////////////////////////////////////////////////////////////////////////
//
// NavigationController Callback
//
//////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onPageVisible(Page page) {
if (page == Page.MESSAGE_STREAM) {
inflateCollectionIcon();
cursorLayout.enableMessageWriting();
}
}
@Override
public void onPageStateHasChanged(Page page) {
}
//////////////////////////////////////////////////////////////////////////////
//
// GroupCallingStoreObserver
//
//////////////////////////////////////////////////////////////////////////////
@Override
public void onStartCall(boolean withVideo) {
}
public IConversation.Type getConversationType() {
return toConversationType;
}
//////////////////////////////////////////////////////////////////////////////
//
// CurrentFocusObserver
//
//////////////////////////////////////////////////////////////////////////////
private boolean inSplitPortraitMode() {
return LayoutSpec.isTablet(getActivity()) && ViewUtils.isInPortrait(getActivity()) && getControllerFactory().getNavigationController().getPagerPosition() == 0;
}
@Override
public void onSearch(String keyword) {
}
@Override
public void onRandomSearch() {
}
@Override
public void onTrendingSearch() {
}
@Override
public void onCloseGiphy() {
resetCursor();
}
@Override
public void onCancelGiphy() {
}
//////////////////////////////////////////////////////////////////////////////
//
// SlidingPaneObserver
//
//////////////////////////////////////////////////////////////////////////////
@Override
public void onPanelSlide(View panel, float slideOffset) {
}
@Override
public void onPanelOpened(View panel) {
if (ViewUtils.isInLandscape(getActivity())) {
return;
}
KeyboardUtils.closeKeyboardIfShown(getActivity());
}
@Override
public void onPanelClosed(View panel) {
}
private void resetCursor() {
cursorLayout.setText("");
}
@Override
public void dismissOnBoardingHint(OnBoardingHintType requestedType) {
}
@Override
public void onSyncError(final ErrorsList.ErrorDescription errorDescription) {
switch (errorDescription.getType()) {
case CANNOT_SEND_ASSET_FILE_NOT_FOUND:
ViewUtils.showAlertDialog(getActivity(),
R.string.asset_upload_error__not_found__title,
R.string.asset_upload_error__not_found__message,
R.string.asset_upload_error__not_found__button,
null,
true);
errorDescription.dismiss();
break;
case CANNOT_SEND_ASSET_TOO_LARGE:
AlertDialog dialog = ViewUtils.showAlertDialog(getActivity(),
R.string.asset_upload_error__file_too_large__title,
R.string.asset_upload_error__file_too_large__message_default,
R.string.asset_upload_error__file_too_large__button,
null,
true);
long maxAllowedSizeInBytes = AssetFactory.getMaxAllowedAssetSizeInBytes();
if (maxAllowedSizeInBytes > 0) {
String maxFileSize = Formatter.formatShortFileSize(getContext(), maxAllowedSizeInBytes);
dialog.setMessage(getString(R.string.asset_upload_error__file_too_large__message, maxFileSize));
}
errorDescription.dismiss();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new SelectedTooLargeFileEvent());
break;
case RECORDING_FAILURE:
ViewUtils.showAlertDialog(getActivity(),
R.string.audio_message__recording__failure__title,
R.string.audio_message__recording__failure__message,
R.string.alert_dialog__confirmation,
null,
true);
errorDescription.dismiss();
break;
case CANNOT_SEND_MESSAGE_TO_UNVERIFIED_CONVERSATION:
onErrorCanNotSentMessageToUnverifiedConversation(errorDescription);
break;
}
}
@SuppressLint("NewApi")
@Override
public void onCursorButtonClicked(CursorMenuItem cursorMenuItem) {
final IConversation conversation = getStoreFactory().getConversationStore().getCurrentConversation();
switch (cursorMenuItem) {
case AUDIO_MESSAGE:
if (PermissionUtils.hasSelfPermissions(getActivity(), AUDIO_PERMISSION)) {
openExtendedCursor(ExtendedCursorContainer.Type.VOICE_FILTER_RECORDING);
} else {
ActivityCompat.requestPermissions(getActivity(),
AUDIO_PERMISSION,
AUDIO_FILTER_PERMISSION_REQUEST_ID);
}
break;
case CAMERA:
if (LayoutSpec.isTablet(getContext())) {
KeyboardUtils.closeKeyboardIfShown(getActivity());
getControllerFactory().getCameraController().openCamera(CameraContext.MESSAGE);
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.PHOTO, conversation));
} else {
if (PermissionUtils.hasSelfPermissions(getContext(), EXTENDED_CURSOR_PERMISSIONS)) {
openExtendedCursor(ExtendedCursorContainer.Type.IMAGES);
} else {
ActivityCompat.requestPermissions(getActivity(),
EXTENDED_CURSOR_PERMISSIONS,
OPEN_EXTENDED_CURSOR_IMAGES);
}
}
break;
case PING:
getStoreFactory().getNetworkStore().doIfHasInternetOrNotifyUser(new DefaultNetworkAction() {
@Override
public void execute(NetworkMode networkMode) {
getStoreFactory().getConversationStore().knockCurrentConversation();
SoundController ctrl = inject(SoundController.class);
if (ctrl != null) {
ctrl.playPingFromMe();
}
}
});
TrackingUtils.onSentPingMessage(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
getStoreFactory().getConversationStore().getCurrentConversation());
break;
case SKETCH:
getControllerFactory().getDrawingController().showDrawing(null,
IDrawingController.DrawingDestination.SKETCH_BUTTON);
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.SKETCH, conversation));
break;
case FILE:
assetIntentsManager.openFileSharing();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.FILE, conversation));
break;
case VIDEO_MESSAGE:
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.VIDEO_MESSAGE, conversation));
isVideoMessageButtonClicked = true;
getCameraController().releaseCamera(new Callback<Void>() {
@Override
public void callback(Void v) {
if (!isVideoMessageButtonClicked || assetIntentsManager == null) {
return;
}
isVideoMessageButtonClicked = false;
assetIntentsManager.maybeCaptureVideo(getActivity(), AssetIntentsManager.IntentType.VIDEO_CURSOR_BUTTON);
}
});
break;
case LOCATION:
GoogleApiAvailability googleAPI = GoogleApiAvailability.getInstance();
if (ConnectionResult.SUCCESS == googleAPI.isGooglePlayServicesAvailable(getContext())) {
KeyboardUtils.hideKeyboard(getActivity());
getControllerFactory().getLocationController().showShareLocation();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.LOCATION, conversation));
} else {
Toast.makeText(getContext(), R.string.location_sharing__missing_play_services, Toast.LENGTH_LONG).show();
}
break;
case MORE:
case LESS:
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new OpenedMoreActionsEvent(
getConversationTypeString()));
break;
case GIF:
getControllerFactory().getGiphyController().handleInput(cursorLayout.getText());
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.GIPHY, conversation));
break;
}
}
private CollectionController getCollectionController() {
return ((BaseActivity) getActivity()).injectJava(CollectionController.class);
}
private GlobalCameraController getCameraController() {
return ((BaseActivity) getActivity()).injectJava(GlobalCameraController.class);
}
private void openExtendedCursor(ExtendedCursorContainer.Type type) {
final IConversation conversation = getStoreFactory().getConversationStore().getCurrentConversation();
switch (type) {
case NONE:
break;
case VOICE_FILTER_RECORDING:
extendedCursorContainer.openVoiceFilter(this);
hideSendButtonIfNeeded();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.AUDIO_MESSAGE,
conversation));
break;
case IMAGES:
extendedCursorContainer.openCursorImages(this);
hideSendButtonIfNeeded();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.PHOTO,
conversation));
break;
}
}
@Override
public void onCursorButtonLongPressed(CursorMenuItem cursorMenuItem) {
switch (cursorMenuItem) {
case AUDIO_MESSAGE:
if (PermissionUtils.hasSelfPermissions(getActivity(), AUDIO_PERMISSION)) {
extendedCursorContainer.close(true);
if (audioMessageRecordingView.getVisibility() == View.VISIBLE) {
break;
}
SoundController ctrl = inject(SoundController.class);
if (ctrl != null) {
ctrl.shortVibrate();
}
audioMessageRecordingView.prepareForRecording();
audioMessageRecordingView.setVisibility(View.VISIBLE);
final IConversation conversation = getStoreFactory().getConversationStore().getCurrentConversation();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.cursorAction(OpenedMediaAction.AUDIO_MESSAGE,
conversation));
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new StartedRecordingAudioMessageEvent(
getConversationTypeString(),
true));
} else {
ActivityCompat.requestPermissions(getActivity(), AUDIO_PERMISSION, AUDIO_PERMISSION_REQUEST_ID);
}
break;
}
}
@Override
public void onMotionEventFromCursorButton(CursorMenuItem cursorMenuItem, MotionEvent motionEvent) {
if (cursorMenuItem != CursorMenuItem.AUDIO_MESSAGE ||
audioMessageRecordingView == null ||
audioMessageRecordingView.getVisibility() == View.INVISIBLE) {
return;
}
audioMessageRecordingView.onMotionEventFromAudioMessageButton(motionEvent);
}
@Override
public void onMessageSubmitted(String message) {
if (TextUtils.isEmpty(message.trim())) {
return;
}
resetCursor();
getStoreFactory().getConversationStore().sendMessage(message);
TrackingUtils.onSentTextMessage(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
getStoreFactory().getConversationStore().getCurrentConversation());
getStoreFactory().getNetworkStore().doIfHasInternetOrNotifyUser(null);
getControllerFactory().getSharingController().maybeResetSharedText(getStoreFactory().getConversationStore().getCurrentConversation());
}
public void onFocusChange(boolean hasFocus) {
if (cursorLayout == null) {
return;
}
if (hasFocus) {
getControllerFactory().getFocusController().setFocus(IFocusController.CONVERSATION_CURSOR);
}
if (LayoutSpec.isPhone(getActivity()) || !getControllerFactory().getPickUserController().isShowingPickUser(
IPickUserController.Destination.CONVERSATION_LIST)) {
return;
}
// On tablet, apply Page.MESSAGE_STREAM soft input mode when conversation cursor has focus (soft input mode of page gets changed when left startui is open)
int softInputMode = hasFocus ?
getControllerFactory().getGlobalLayoutController().getSoftInputModeForPage(Page.MESSAGE_STREAM)
:
getControllerFactory().getGlobalLayoutController().getSoftInputModeForPage(Page.PICK_USER);
ViewUtils.setSoftInputMode(getActivity().getWindow(), softInputMode, TAG);
}
@Override
public void onCursorClicked() {
if (!cursorLayout.isEditingMessage()) {
listView.scrollToBottom();
}
}
@Override
public void onShowedActionHint(CursorMenuItem item) {
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new OpenedActionHintEvent(item.name(),
getConversationTypeString()));
}
@Override
public void onApprovedMessageEditing(Message message) {
KeyboardUtils.hideKeyboard(getActivity());
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new EditedMessageEvent(message));
}
@Override
public void onClosedMessageEditing() {
getControllerFactory().getConversationScreenController().setMessageBeingEdited(null);
}
@Override
public void onEmojiButtonClicked(boolean showEmojiKeyboard) {
if (showEmojiKeyboard) {
KeyboardUtils.hideKeyboard(getActivity());
extendedCursorContainer.openEmojis(getControllerFactory().getUserPreferencesController().getRecentEmojis(),
getControllerFactory().getUserPreferencesController().getUnsupportedEmojis(),
this);
boolean withBot = getStoreFactory().getConversationStore().getCurrentConversation().isOtto();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new OpenedEmojiKeyboardEvent(withBot));
cursorLayout.showSendButton(true);
} else {
extendedCursorContainer.close(false);
KeyboardUtils.showKeyboard(getActivity());
}
}
@Override
public void onEphemeralButtonClicked(EphemeralExpiration currentEphemeralExpiration) {
extendedCursorContainer.openEphemeral(this, currentEphemeralExpiration);
if (currentEphemeralExpiration == EphemeralExpiration.NONE) {
IConversation conversation = getStoreFactory().getConversationStore().getCurrentConversation();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.ephemeral(conversation,
false));
}
}
@Override
public void onEphemeralButtonDoubleClicked(EphemeralExpiration currentEphemeralExpiration) {
EphemeralExpiration lastExpiraton = EphemeralExpiration.getForMillis(getControllerFactory().getUserPreferencesController().getLastEphemeralValue());
if (lastExpiraton.equals(EphemeralExpiration.NONE)) {
return;
}
if (currentEphemeralExpiration.equals(EphemeralExpiration.NONE)) {
onEphemeralExpirationSelected(lastExpiraton, true);
IConversation conversation = getStoreFactory().getConversationStore().getCurrentConversation();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(OpenedMediaActionEvent.ephemeral(conversation, true));
} else {
onEphemeralExpirationSelected(EphemeralExpiration.NONE, true);
}
}
@Override
public boolean onBackPressed() {
if (isPreviewShown) {
onCancelPreview();
return true;
}
if (getChildFragmentManager().getBackStackEntryCount() > 0) {
getChildFragmentManager().popBackStack();
return true;
}
if (extendedCursorContainer.isExpanded()) {
extendedCursorContainer.close(false);
return true;
}
if (cursorLayout.isEditingMessage()) {
cursorLayout.closeEditMessage(true);
return true;
}
return false;
}
@Override
public void onRequestPermissionsResult(int requestCode, int[] grantResults) {
if (assetIntentsManager.onRequestPermissionsResult(requestCode, grantResults)) {
return;
}
switch (requestCode) {
case OPEN_EXTENDED_CURSOR_IMAGES:
if (PermissionUtils.verifyPermissions(grantResults)) {
openExtendedCursor(ExtendedCursorContainer.Type.IMAGES);
}
break;
case CAMERA_PERMISSION_REQUEST_ID:
if (PermissionUtils.verifyPermissions(grantResults)) {
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
takeVideoIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
}
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);
} else {
onCameraPermissionsFailed();
}
break;
case FILE_SHARING_PERMISSION_REQUEST_ID:
if (PermissionUtils.verifyPermissions(grantResults)) {
for (URI uri : sharingUris) {
getStoreFactory().getConversationStore().sendMessage(AssetFactory.fromContentUri(uri),
assetErrorHandler);
}
sharingUris.clear();
} else {
ViewUtils.showAlertDialog(getActivity(),
R.string.asset_upload_error__not_found__title,
R.string.asset_upload_error__not_found__message,
R.string.asset_upload_error__not_found__button,
null,
true);
}
break;
case AUDIO_PERMISSION_REQUEST_ID:
// No actions required if permission is granted
// TODO: https://wearezeta.atlassian.net/browse/AN-4027 Show information dialog if permission is not granted
break;
case AUDIO_FILTER_PERMISSION_REQUEST_ID:
if (PermissionUtils.verifyPermissions(grantResults)) {
openExtendedCursor(ExtendedCursorContainer.Type.VOICE_FILTER_RECORDING);
} else {
Toast.makeText(getActivity(),
R.string.audio_message_error__missing_audio_permissions,
Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
private void onCameraPermissionsFailed() {
Toast.makeText(getActivity(),
R.string.video_message_error__missing_camera_permissions,
Toast.LENGTH_SHORT).show();
}
@Override
public void onSendAudioMessage(AudioAssetForUpload audioAssetForUpload,
AudioEffect appliedAudioEffect,
boolean sentWithQuickAction) {
getStoreFactory().getConversationStore().sendMessage(audioAssetForUpload, assetErrorHandlerAudio);
hideAudioMessageRecording();
TrackingUtils.tagSentAudioMessageEvent(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
audioAssetForUpload,
appliedAudioEffect,
true,
sentWithQuickAction,
getStoreFactory().getConversationStore().getCurrentConversation());
}
@Override
public void onCancel() {
extendedCursorContainer.close(false);
}
@Override
public void onAudioMessageRecordingStarted() {
getControllerFactory().getGlobalLayoutController().keepScreenAwake();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new StartedRecordingAudioMessageEvent(
getConversationTypeString(),
false));
}
@Override
public void sendRecording(AudioAssetForUpload audioAssetForUpload, AudioEffect appliedAudioEffect) {
getStoreFactory().getConversationStore().sendMessage(audioAssetForUpload, assetErrorHandlerAudio);
hideAudioMessageRecording();
TrackingUtils.tagSentAudioMessageEvent(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
audioAssetForUpload,
appliedAudioEffect,
false,
false,
getStoreFactory().getConversationStore().getCurrentConversation());
extendedCursorContainer.close(true);
}
@Override
public void onCancelledAudioMessageRecording() {
hideAudioMessageRecording();
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new CancelledRecordingAudioMessageEvent(
getConversationTypeString()));
}
@Override
public void onPreviewedAudioMessage() {
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new PreviewedAudioMessageEvent(getConversationTypeString()));
}
@Override
public void onStartedRecordingAudioMessage() {
getControllerFactory().getGlobalLayoutController().keepScreenAwake();
}
private void hideAudioMessageRecording() {
if (audioMessageRecordingView.getVisibility() == View.INVISIBLE) {
return;
}
audioMessageRecordingView.reset();
audioMessageRecordingView.setVisibility(View.INVISIBLE);
getControllerFactory().getGlobalLayoutController().resetScreenAwakeState();
}
private void onErrorCanNotSentMessageToUnverifiedConversation(final ErrorsList.ErrorDescription errorDescription) {
if (getControllerFactory().getNavigationController().getCurrentPage() != Page.MESSAGE_STREAM) {
return;
}
KeyboardUtils.hideKeyboard(getActivity());
final IConversation currentConversation = errorDescription.getConversation();
final Iterable<? extends User> users = currentConversation.getUsers();
final Map<User, String> userNameMap = new HashMap<>();
int tmpUnverifiedDevices = 0;
int userCount = 0;
final boolean onlySelfChanged;
for (User user : users) {
if (user.getVerified() == Verification.VERIFIED) {
continue;
}
userCount++;
userNameMap.put(user, user.getDisplayName());
for (OtrClient client : user.getOtrClients()) {
if (client.getVerified() == Verification.VERIFIED) {
continue;
}
tmpUnverifiedDevices++;
}
}
if (self != null && self.getUser().getVerified() != Verification.VERIFIED) {
onlySelfChanged = userCount == 0;
userCount++;
userNameMap.put(self.getUser(), getString(R.string.conversation_degraded_confirmation__header__you));
for (OtrClient client : self.getUser().getOtrClients()) {
if (client.getVerified() == Verification.VERIFIED) {
continue;
}
tmpUnverifiedDevices++;
}
} else {
onlySelfChanged = false;
}
final List<String> userNameList = new ArrayList<>(userNameMap.values());
final int userNameCount = userNameList.size();
final String header;
if (userNameCount == 0) {
header = getResources().getString(R.string.conversation__degraded_confirmation__header__someone);
} else if (userNameCount == 1) {
final int unverifiedDevices = Math.max(1, tmpUnverifiedDevices);
header = getResources().getQuantityString(R.plurals.conversation__degraded_confirmation__header__single_user,
unverifiedDevices,
userNameList.get(0));
} else {
header = getString(R.string.conversation__degraded_confirmation__header__multiple_user,
TextUtils.join(", ", userNameList.subList(0, userNameCount - 1)),
userNameList.get(userNameCount - 1));
}
int tmpMessageCount = 0;
for (Message m : errorDescription.getMessages()) {
tmpMessageCount++;
}
final int messageCount = Math.max(1, tmpMessageCount);
final String message = getResources().getQuantityString(R.plurals.conversation__degraded_confirmation__message,
messageCount);
final ConfirmationCallback callback = new ConfirmationCallback() {
@Override
public void positiveButtonClicked(boolean checkboxIsSelected) {
final Iterable<? extends Message> messages = errorDescription.getMessages();
for (Message message : messages) {
message.retry();
}
errorDescription.dismiss();
}
@Override
public void negativeButtonClicked() {
}
@Override
public void canceled() {
}
@Override
public void onHideAnimationEnd(boolean confirmed, boolean canceled, boolean checkboxIsSelected) {
if (confirmed || canceled) {
return;
}
if (onlySelfChanged) {
getContext().startActivity(ZetaPreferencesActivity.getOtrDevicesPreferencesIntent(getContext()));
} else {
final View anchorView = ViewUtils.getView(getActivity(), R.id.cursor_menu_item_participant);
getControllerFactory().getConversationScreenController().showParticipants(anchorView, true);
}
}
};
final String positiveButton = getString(R.string.conversation__degraded_confirmation__positive_action);
final String negativeButton = onlySelfChanged ?
getString(R.string.conversation__degraded_confirmation__negative_action_self) :
getResources().getQuantityString(R.plurals.conversation__degraded_confirmation__negative_action, userCount);
final ConfirmationRequest request = new ConfirmationRequest.Builder()
.withHeader(header)
.withMessage(message)
.withPositiveButton(positiveButton)
.withNegativeButton(negativeButton)
.withConfirmationCallback(callback)
.withCancelButton()
.withBackgroundImage(R.drawable.degradation_overlay)
.withWireTheme(getControllerFactory().getThemeController().getThemeDependentOptionsTheme())
.build();
getControllerFactory().getConfirmationController().requestConfirmation(request,
IConfirmationController.CONVERSATION);
}
private String getConversationTypeString() {
return getConversationType() != null ? getConversationType().name() : "";
}
@Override
public void openCamera() {
getControllerFactory().getCameraController().openCamera(CameraContext.MESSAGE);
}
@Override
public void openVideo() {
assetIntentsManager.maybeCaptureVideo(getActivity(), AssetIntentsManager.IntentType.VIDEO);
}
@Override
public void openGallery() {
assetIntentsManager.openGallery();
}
@Override
public void onPictureTaken(ImageAsset imageAsset) {
showImagePreview(imageAsset, ImagePreviewLayout.Source.CAMERA);
}
@Override
public void onGalleryPictureSelected(ImageAsset asset) {
isPreviewShown = true;
showImagePreview(asset, ImagePreviewLayout.Source.IN_APP_GALLERY);
}
private void showImagePreview(ImageAsset asset, ImagePreviewLayout.Source source) {
ImagePreviewLayout imagePreviewLayout = createPreviewLayout();
imagePreviewLayout.setImageAsset(asset,
source,
this);
imagePreviewLayout.setAccentColor(getControllerFactory().getAccentColorController().getAccentColor().getColor());
imagePreviewLayout.setTitle(getStoreFactory().getConversationStore().getCurrentConversation().getName());
containerPreview.addView(imagePreviewLayout);
openPreview(containerPreview);
}
private ImagePreviewLayout createPreviewLayout() {
return (ImagePreviewLayout) LayoutInflater.from(getContext()).inflate(
R.layout.fragment_cursor_images_preview,
containerPreview,
false);
}
private void openPreview(View containerPreview) {
isPreviewShown = true;
getControllerFactory().getNavigationController().setPagerEnabled(false);
containerPreview.setTranslationY(getView().getMeasuredHeight());
containerPreview
.animate()
.translationY(0)
.setDuration(getResources().getInteger(R.integer.animation_duration_medium))
.setInterpolator(new Expo.EaseOut());
}
@Override
public void onCancelPreview() {
isPreviewShown = false;
getControllerFactory().getNavigationController().setPagerEnabled(true);
containerPreview
.animate()
.translationY(getView().getMeasuredHeight())
.setDuration(getResources().getInteger(R.integer.animation_duration_medium))
.setInterpolator(new Expo.EaseIn())
.withEndAction(new Runnable() {
@Override
public void run() {
if (containerPreview != null) {
containerPreview.removeAllViews();
}
}
});
}
@Override
public void onSketchOnPreviewPicture(ImageAsset imageAsset,
ImagePreviewLayout.Source source,
DrawingController.DrawingMethod method) {
getControllerFactory().getDrawingController().showDrawing(imageAsset,
IDrawingController.DrawingDestination.CAMERA_PREVIEW_VIEW,
method);
extendedCursorContainer.close(true);
}
@Override
public void onSendPictureFromPreview(ImageAsset imageAsset, ImagePreviewLayout.Source source) {
getStoreFactory().getConversationStore().sendMessage(imageAsset);
TrackingUtils.onSentPhotoMessage(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
getStoreFactory().getConversationStore().getCurrentConversation(),
source);
extendedCursorContainer.close(true);
onCancelPreview();
}
@Override
public void onDataReceived(AssetIntentsManager.IntentType type, URI uri) {
switch (type) {
case FILE_SHARING:
sharingUris.clear();
if (PermissionUtils.hasSelfPermissions(getActivity(), FILE_SHARING_PERMISSION)) {
getStoreFactory().getConversationStore().sendMessage(AssetFactory.fromContentUri(uri),
assetErrorHandler);
} else {
sharingUris.add(uri);
ActivityCompat.requestPermissions(getActivity(),
FILE_SHARING_PERMISSION,
FILE_SHARING_PERMISSION_REQUEST_ID);
}
break;
case GALLERY:
showImagePreview(ImageAssetFactory.getImageAsset(uri),
ImagePreviewLayout.Source.DEVICE_GALLERY);
break;
case VIDEO_CURSOR_BUTTON:
sendVideo(uri);
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new SentVideoMessageEvent((int) (AssetUtils.getVideoAssetDurationMilliSec(
getContext(),
uri) / 1000),
getStoreFactory().getConversationStore().getCurrentConversation(),
SentVideoMessageEvent.Source.CURSOR_BUTTON));
break;
case VIDEO:
sendVideo(uri);
((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class).tagEvent(new SentVideoMessageEvent((int) (AssetUtils.getVideoAssetDurationMilliSec(
getContext(),
uri) / 1000),
getStoreFactory().getConversationStore().getCurrentConversation(),
SentVideoMessageEvent.Source.KEYBOARD));
break;
case CAMERA:
sendImage(uri);
TrackingUtils.onSentPhotoMessage(((BaseActivity) getActivity()).injectJava(GlobalTrackingController.class),
getStoreFactory().getConversationStore().getCurrentConversation(),
SentPictureEvent.Source.CAMERA,
SentPictureEvent.Method.FULL_SCREEN);
extendedCursorContainer.close(true);
break;
}
}
private void sendVideo(URI uri) {
AssetForUpload assetForUpload = AssetFactory.fromContentUri(uri);
getStoreFactory().getConversationStore().sendMessage(assetForUpload, assetErrorHandlerVideo);
getControllerFactory().getNavigationController().setRightPage(Page.MESSAGE_STREAM, TAG);
extendedCursorContainer.close(true);
}
private void sendImage(URI uri) {
ImageAsset imageAsset = ImageAssetFactory.getImageAsset(uri);
getStoreFactory().getConversationStore().sendMessage(imageAsset);
}
@Override
public void onCanceled(AssetIntentsManager.IntentType type) {
}
@Override
public void onFailed(AssetIntentsManager.IntentType type) {
}
@Override
public void openIntent(Intent intent, AssetIntentsManager.IntentType intentType) {
if (MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction()) &&
extendedCursorContainer.getType() == ExtendedCursorContainer.Type.IMAGES &&
extendedCursorContainer.isExpanded()) {
// Close keyboard camera before requesting external camera for recording video
extendedCursorContainer.close(true);
}
startActivityForResult(intent, intentType.requestCode);
getActivity().overridePendingTransition(R.anim.camera_in, R.anim.camera_out);
}
@Override
public void onPermissionFailed(AssetIntentsManager.IntentType type) {
}
@Override
public void onPagerEnabledStateHasChanged(boolean enabled) {
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (positionOffset > 0) {
extendedCursorContainer.close(true);
}
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onExtendedCursorClosed(ExtendedCursorContainer.Type lastType) {
cursorLayout.onExtendedCursorClosed();
hideSendButtonIfNeeded();
if (lastType == ExtendedCursorContainer.Type.EPHEMERAL) {
EphemeralExpiration expiration = getStoreFactory().getConversationStore().getCurrentConversation().getEphemeralExpiration();
if (!expiration.equals(EphemeralExpiration.NONE)) {
getControllerFactory().getUserPreferencesController().setLastEphemeralValue(expiration.milliseconds);
}
}
getControllerFactory().getGlobalLayoutController().resetScreenAwakeState();
}
private void hideSendButtonIfNeeded() {
if (!getControllerFactory().getUserPreferencesController().isCursorSendButtonEnabled() || TextUtils.isEmpty(cursorLayout.getText())) {
cursorLayout.showSendButton(false);
}
}
@Override
public void onEmojiSelected(String emoji) {
cursorLayout.insertText(emoji);
getControllerFactory().getUserPreferencesController().addRecentEmoji(emoji);
}
@Override
public void onTypingIndicatorVisibilityChanged(boolean visible) {
if (visible) {
cursorLayout.showTopbar(false);
} else {
cursorLayout.showTopbar(!listView.scrollController().shouldScrollToBottom());
}
}
@Override
public void onEphemeralExpirationSelected(EphemeralExpiration expiration, boolean close) {
if (getStoreFactory() == null || getStoreFactory().isTornDown()) {
return;
}
if (close) {
extendedCursorContainer.close(false);
}
getStoreFactory().getConversationStore().getCurrentConversation().setEphemeralExpiration(expiration);
}
public interface Container {
}
}