Skip to content
Snippets Groups Projects
Select Git revision
  • 7c822eb15464c0efdab2b26e2b6d4301d5f311d8
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • release/201811
  • release/201808
  • wip/patches_poly_2017/cedryk_doucet/abderahmane_bouziane
  • releases/beta1
  • android/release_462
  • android/release_461
  • android/release_460
  • android/release_459
  • android/release_458
  • android/release_457
  • android/release_456
  • android/release_455
  • android/release_454
  • android/release_453
  • android/release_452
  • android/release_451
  • android/release_450
  • android/release_449
  • android/release_448
  • android/release_447
  • android/release_446
  • android/release_445
  • android/release_444
  • android/release_443
38 results

ConversationFragment.java

Blame
  • Adrien Béraud's avatar
    Adrien Béraud authored
    Change-Id: I364db0b3660774c5e2ddaf4d4ba4abda0a443a9c
    7c822eb1
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ConversationFragment.java 48.18 KiB
    /*
     *  Copyright (C) 2004-2020 Savoir-faire Linux Inc.
     *
     *  Author: Adrien Béraud <adrien.beraud@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.fragments;
    
    import android.Manifest;
    import android.animation.LayoutTransition;
    import android.animation.ValueAnimator;
    import android.annotation.SuppressLint;
    import android.app.Activity;
    import android.content.ActivityNotFoundException;
    import android.content.ClipData;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.content.ServiceConnection;
    import android.content.SharedPreferences;
    import android.content.pm.PackageManager;
    import android.graphics.Typeface;
    import android.content.res.Resources;
    import android.os.Bundle;
    import android.os.Environment;
    import android.os.IBinder;
    import android.provider.MediaStore;
    import android.text.Editable;
    import android.text.TextUtils;
    import android.text.TextWatcher;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.Menu;
    import android.view.MenuInflater;
    import android.view.MenuItem;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.WindowManager;
    import android.view.inputmethod.EditorInfo;
    import android.widget.ImageView;
    import android.widget.RelativeLayout;
    import android.widget.Spinner;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import com.google.android.material.snackbar.Snackbar;
    
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.appcompat.app.ActionBar;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.appcompat.view.menu.MenuBuilder;
    import androidx.appcompat.view.menu.MenuPopupHelper;
    import androidx.appcompat.widget.PopupMenu;
    import androidx.appcompat.widget.Toolbar;
    import androidx.core.view.ViewCompat;
    import androidx.fragment.app.Fragment;
    import androidx.fragment.app.FragmentManager;
    import androidx.recyclerview.widget.DefaultItemAnimator;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import cx.ring.BuildConfig;
    import cx.ring.R;
    import cx.ring.adapters.ConversationAdapter;
    import cx.ring.adapters.NumberAdapter;
    import cx.ring.application.JamiApplication;
    import cx.ring.client.CallActivity;
    import cx.ring.client.ContactDetailsActivity;
    import cx.ring.client.ConversationActivity;
    import cx.ring.client.HomeActivity;
    import cx.ring.contacts.AvatarFactory;
    import cx.ring.conversation.ConversationPresenter;
    import cx.ring.conversation.ConversationView;
    import cx.ring.daemon.Ringservice;
    import cx.ring.databinding.FragConversationBinding;
    import cx.ring.interfaces.Colorable;
    import cx.ring.model.Account;
    import cx.ring.model.CallContact;
    import cx.ring.model.Conversation;
    import cx.ring.model.Interaction;
    import cx.ring.model.DataTransfer;
    import cx.ring.model.Phone;
    import cx.ring.model.Error;
    import cx.ring.model.Uri;
    import cx.ring.mvp.BaseSupportFragment;
    import cx.ring.services.LocationSharingService;
    import cx.ring.services.NotificationService;
    import cx.ring.services.NotificationServiceImpl;
    import cx.ring.utils.ActionHelper;
    import cx.ring.utils.AndroidFileUtils;
    import cx.ring.utils.ContentUriHandler;
    import cx.ring.utils.DeviceUtils;
    import cx.ring.utils.ConversationPath;
    import cx.ring.utils.MediaButtonsHelper;
    import cx.ring.views.AvatarDrawable;
    import io.reactivex.Completable;
    import io.reactivex.Single;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.disposables.CompositeDisposable;
    
    import static android.app.Activity.RESULT_OK;
    
    public class ConversationFragment extends BaseSupportFragment<ConversationPresenter> implements
            MediaButtonsHelper.MediaButtonsHelperCallback,
            ConversationView, SharedPreferences.OnSharedPreferenceChangeListener {
        private static final String TAG = ConversationFragment.class.getSimpleName();
    
        public static final int REQ_ADD_CONTACT = 42;
    
        public static final String KEY_CONTACT_RING_ID = BuildConfig.APPLICATION_ID + ".CONTACT_RING_ID";
        public static final String KEY_ACCOUNT_ID = BuildConfig.APPLICATION_ID + ".ACCOUNT_ID";
        public static final String KEY_PREFERENCE_PENDING_MESSAGE = "pendingMessage";
        public static final String KEY_PREFERENCE_CONVERSATION_COLOR = "color";
        public static final String EXTRA_SHOW_MAP = "showMap";
    
        private static final int REQUEST_CODE_FILE_PICKER = 1000;
        private static final int REQUEST_PERMISSION_CAMERA = 1001;
        private static final int REQUEST_CODE_TAKE_PICTURE = 1002;
        private static final int REQUEST_CODE_SAVE_FILE = 1003;
        private static final int REQUEST_CODE_CAPTURE_AUDIO = 1004;
        private static final int REQUEST_CODE_CAPTURE_VIDEO = 1005;
    
        private ServiceConnection locationServiceConnection = null;
    
        private FragConversationBinding binding;
        private MenuItem mAudioCallBtn = null;
        private MenuItem mVideoCallBtn = null;
    
        private View currentBottomView = null;
        private ConversationAdapter mAdapter = null;
        private int marginPx;
        private int marginPxTotal;
        private final ValueAnimator animation = new ValueAnimator();
    
        private SharedPreferences mPreferences;
    
        private File mCurrentPhoto = null;
        private String mCurrentFileAbsolutePath = null;
        private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
        private int mSelectedPosition;
    
        private boolean mIsBubble;
    
        private AvatarDrawable mConversationAvatar;
        private final Map<String, AvatarDrawable> mParticipantAvatars = new HashMap<>();
        private final Map<String, AvatarDrawable> mSmallParticipantAvatars = new HashMap<>();
        private int mapWidth, mapHeight;
    
        public AvatarDrawable getConversationAvatar(String uri) {
            return mParticipantAvatars.get(uri);
        }
        public AvatarDrawable getSmallConversationAvatar(String uri) {
            synchronized (mSmallParticipantAvatars) {
                return mSmallParticipantAvatars.get(uri);
            }
        }
    
        private static int getIndex(Spinner spinner, Uri myString) {
            for (int i = 0, n = spinner.getCount(); i < n; i++)
                if (((Phone) spinner.getItemAtPosition(i)).getNumber().equals(myString)) {
                    return i;
                }
            return 0;
        }
    
        @Override
        public void refreshView(final List<Interaction> conversation) {
            if (conversation == null) {
                return;
            }
            if (binding != null)
                binding.pbLoading.setVisibility(View.GONE);
            if (mAdapter != null) {
                mAdapter.updateDataset(conversation);
            }
            requireActivity().invalidateOptionsMenu();
        }
    
        @Override
        public void scrollToEnd() {
            if (mAdapter.getItemCount() > 0) {
                binding.histList.scrollToPosition(mAdapter.getItemCount() - 1);
            }
        }
    
        private static void setBottomPadding(@NonNull View view, int padding) {
            view.setPadding(
                    view.getPaddingLeft(),
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    padding);
        }
    
        private void updateListPadding() {
            if (currentBottomView != null && currentBottomView.getHeight() != 0) {
                setBottomPadding(binding.histList, currentBottomView.getHeight() + marginPxTotal);
                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
                params.bottomMargin = currentBottomView.getHeight() + marginPxTotal;
                binding.mapCard.setLayoutParams(params);
            }
        }
    
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            ((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this);
            Resources res = getResources();
            marginPx = res.getDimensionPixelSize(R.dimen.conversation_message_input_margin);
            mapWidth = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_width);
            mapHeight = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_height);
            marginPxTotal = marginPx;
    
            binding = FragConversationBinding.inflate(inflater, container, false);
            binding.setPresenter(this);
    
            animation.setDuration(150);
            animation.addUpdateListener(valueAnimator -> setBottomPadding(binding.histList, (Integer)valueAnimator.getAnimatedValue()));
    
            ViewCompat.setOnApplyWindowInsetsListener(binding.histList, (v, insets) -> {
                marginPxTotal = marginPx + insets.getSystemWindowInsetBottom();
                updateListPadding();
                insets.consumeSystemWindowInsets();
                return insets;
            });
            View layout = binding.conversationLayout;
    
            // remove action bar height for tablet layout
            if (DeviceUtils.isTablet(getContext())) {
                layout.setPadding(layout.getPaddingLeft(), 0, layout.getPaddingRight(), layout.getPaddingBottom());
            }
    
            int paddingTop = layout.getPaddingTop();
            ViewCompat.setOnApplyWindowInsetsListener(layout, (v, insets) -> {
                v.setPadding(
                        v.getPaddingLeft(),
                        paddingTop + insets.getSystemWindowInsetTop(),
                        v.getPaddingRight(),
                        v.getPaddingBottom());
                insets.consumeSystemWindowInsets();
                return insets;
            });
    
            binding.ongoingcallPane.setVisibility(View.GONE);
            binding.msgInputTxt.setMediaListener(contentInfo -> startFileSend(AndroidFileUtils
                    .getCacheFile(requireContext(), contentInfo.getContentUri())
                    .flatMapCompletable(this::sendFile)
                    .doFinally(contentInfo::releasePermission)));
            binding.msgInputTxt.setOnEditorActionListener((v, actionId, event) -> actionSendMsgText(actionId));
            binding.msgInputTxt.setOnFocusChangeListener((view, hasFocus) -> {
                if (hasFocus)  {
                    Fragment fragment = getChildFragmentManager().findFragmentById(R.id.mapLayout);
                    if (fragment != null) {
                        ((LocationSharingFragment) fragment).hideControls();
                    }
                }
            });
            binding.msgInputTxt.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }
    
                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }
    
                @Override
                public void afterTextChanged(Editable s) {
                    String message = s.toString();
                    boolean hasMessage = !TextUtils.isEmpty(message);
                    presenter.onComposingChanged(hasMessage);
                    if (hasMessage) {
                        binding.msgSend.setVisibility(View.VISIBLE);
                        binding.emojiSend.setVisibility(View.GONE);
                    } else {
                        binding.msgSend.setVisibility(View.GONE);
                        binding.emojiSend.setVisibility(View.VISIBLE);
                    }
                    if (mPreferences != null) {
                        if (hasMessage)
                            mPreferences.edit().putString(KEY_PREFERENCE_PENDING_MESSAGE, message).apply();
                        else
                            mPreferences.edit().remove(KEY_PREFERENCE_PENDING_MESSAGE).apply();
                    }
                }
            });
    
            setHasOptionsMenu(true);
            return binding.getRoot();
        }
    
        @Override
        public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            if (mPreferences != null) {
                String pendingMessage = mPreferences.getString(KEY_PREFERENCE_PENDING_MESSAGE, null);
                if (!TextUtils.isEmpty(pendingMessage)) {
                    binding.msgInputTxt.setText(pendingMessage);
                    binding.msgSend.setVisibility(View.VISIBLE);
                    binding.emojiSend.setVisibility(View.GONE);
                }
            }
    
            binding.msgInputTxt.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                if (oldBottom == 0 && oldTop == 0) {
                    updateListPadding();
                } else {
                    if (animation.isStarted())
                        animation.cancel();
                    animation.setIntValues(binding.histList.getPaddingBottom(), (currentBottomView == null ? 0 : currentBottomView.getHeight()) + marginPxTotal);
                    animation.start();
                }
            });
    
            DefaultItemAnimator animator = (DefaultItemAnimator) binding.histList.getItemAnimator();
            if (animator != null)
                animator.setSupportsChangeAnimations(false);
            binding.histList.setAdapter(mAdapter);
        }
    
        @Override
        public void setConversationColor(int color) {
            Colorable activity = (Colorable) getActivity();
            if (activity != null)
                activity.setColor(color);
            mAdapter.setPrimaryColor(color);
        }
    
        @Override
        public void onDestroyView() {
            if (mPreferences != null)
                mPreferences.unregisterOnSharedPreferenceChangeListener(this);
            animation.removeAllUpdateListeners();
            binding.histList.setAdapter(null);
            mCompositeDisposable.clear();
            if (locationServiceConnection != null) {
                try {
                    requireContext().unbindService(locationServiceConnection);
                } catch (Exception e) {
                    Log.w(TAG, "Error unbinding service: " + e.getMessage());
                }
            }
            mAdapter = null;
            super.onDestroyView();
            binding = null;
        }
    
        @Override
        public boolean onContextItemSelected(@NonNull MenuItem item) {
            if (mAdapter.onContextItemSelected(item))
                return true;
            return super.onContextItemSelected(item);
        }
    
        public void updateAdapterItem() {
            if (mSelectedPosition != -1) {
                mAdapter.notifyItemChanged(mSelectedPosition);
                mSelectedPosition = -1;
            }
        }
    
        public void sendMessageText() {
            String message = binding.msgInputTxt.getText().toString();
            clearMsgEdit();
            presenter.sendTextMessage(message);
        }
    
        public void sendEmoji() {
            presenter.sendTextMessage(binding.emojiSend.getText().toString());
        }
    
        @SuppressLint("RestrictedApi")
        public void expandMenu(View v) {
            Context context = requireContext();
            PopupMenu popup = new PopupMenu(context, v);
            popup.inflate(R.menu.conversation_share_actions);
            popup.setOnMenuItemClickListener(item -> {
                switch(item.getItemId()) {
                    case R.id.conv_send_audio:
                        sendAudioMessage();
                        break;
                    case R.id.conv_send_video:
                        sendVideoMessage();
                        break;
                    case R.id.conv_send_file:
                        presenter.selectFile();
                        break;
                    case R.id.conv_share_location:
                        shareLocation();
                        break;
                    case R.id.chat_plugins:
                        presenter.showPluginListHandlers();
                        break;
                }
                return false;
            });
            popup.getMenu().findItem(R.id.chat_plugins).setVisible(Ringservice.getPluginsEnabled() && Ringservice.getChatHandlers().size() > 0);
            MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) popup.getMenu(), v);
            menuHelper.setForceShowIcon(true);
            menuHelper.show();
        }
    
        public void showPluginListHandlers(String accountId, String contactId) {
            Log.w(TAG, "show Plugin Chat Handlers List");
    
            FragmentManager fragmentManager = getChildFragmentManager();
            PluginHandlersListFragment fragment = PluginHandlersListFragment.newInstance(accountId, contactId);
            fragmentManager.beginTransaction()
                    .add(R.id.pluginListHandlers, fragment, fragment.TAG)
                    .commit();
    
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
            if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
                binding.mapCard.setLayoutParams(params);
            }
            binding.mapCard.setVisibility(View.VISIBLE);
        }
    
        public void hidePluginListHandlers() {
            if (binding.mapCard.getVisibility() != View.GONE) {
                binding.mapCard.setVisibility(View.GONE);
    
                FragmentManager fragmentManager = getChildFragmentManager();
                Fragment fragment = fragmentManager.findFragmentById(R.id.pluginListHandlers);
    
                if (fragment != null) {
                    fragmentManager.beginTransaction()
                            .remove(fragment)
                            .commit();
                }
            }
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
            if (params.width != mapWidth) {
                params.width = mapWidth;
                params.height = mapHeight;
                binding.mapCard.setLayoutParams(params);
            }
        }
    
        public void shareLocation() {
            presenter.shareLocation();
        }
    
        public void closeLocationSharing(boolean isSharing) {
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
            if (params.width != mapWidth) {
                params.width = mapWidth;
                params.height = mapHeight;
                binding.mapCard.setLayoutParams(params);
            }
            if (!isSharing)
                hideMap();
        }
    
        public void openLocationSharing() {
            binding.conversationLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
            if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
                binding.mapCard.setLayoutParams(params);
            }
        }
    
        @Override
        public void startShareLocation(String accountId, String conversationId) {
            showMap(accountId, conversationId, true);
        }
    
        /**
         * Used to update with the past adapter position when a long click was registered
         */
        public void updatePosition(int position) {
            mSelectedPosition = position;
        }
    
        @Override
        public void showMap(String accountId, String contactId, boolean open)  {
            if (binding.mapCard.getVisibility() == View.GONE) {
                Log.w(TAG, "showMap " + accountId + " " + contactId);
    
                FragmentManager fragmentManager = getChildFragmentManager();
                LocationSharingFragment fragment = LocationSharingFragment.newInstance(accountId, contactId, open);
                fragmentManager.beginTransaction()
                        .add(R.id.mapLayout, fragment, "map")
                        .commit();
                binding.mapCard.setVisibility(View.VISIBLE);
            }
            if (open) {
                Fragment fragment = getChildFragmentManager().findFragmentById(R.id.mapLayout);
                if (fragment != null) {
                    ((LocationSharingFragment) fragment).showControls();
                }
            }
        }
    
        @Override
        public void hideMap() {
            if (binding.mapCard.getVisibility() != View.GONE) {
                binding.mapCard.setVisibility(View.GONE);
    
                FragmentManager fragmentManager = getChildFragmentManager();
                Fragment fragment = fragmentManager.findFragmentById(R.id.mapLayout);
    
                if (fragment != null) {
                    fragmentManager.beginTransaction()
                            .remove(fragment)
                            .commit();
                }
            }
        }
    
        public void sendAudioMessage() {
            if (!presenter.getDeviceRuntimeService().hasAudioPermission()) {
                requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_CODE_CAPTURE_AUDIO);
            } else {
                try {
                    Context ctx = requireContext();
                    Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
                    mCurrentPhoto = AndroidFileUtils.createAudioFile(ctx);
                    startActivityForResult(intent, REQUEST_CODE_CAPTURE_AUDIO);
                } catch (Exception ex) {
                    Log.e(TAG, "sendAudioMessage: error", ex);
                    Toast.makeText(getActivity(), "Can't find audio recorder app", Toast.LENGTH_SHORT).show();
                }
            }
        }
    
        public void sendVideoMessage() {
            if (!presenter.getDeviceRuntimeService().hasVideoPermission()) {
                requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_CAPTURE_VIDEO);
            } else {
                try {
                    Context context = requireContext();
                    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
                    intent.putExtra("android.intent.extras.CAMERA_FACING", android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
                    intent.putExtra("android.intent.extras.LENS_FACING_FRONT", 1);
                    intent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true);
                        mCurrentPhoto = AndroidFileUtils.createVideoFile(context);
                    intent.putExtra(MediaStore.EXTRA_OUTPUT, ContentUriHandler.getUriForFile(context, ContentUriHandler.AUTHORITY_FILES, mCurrentPhoto));
                    startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO);
                } catch (Exception ex) {
                    Log.e(TAG, "sendVideoMessage: error", ex);
                    Toast.makeText(getActivity(), "Can't find video recorder app", Toast.LENGTH_SHORT).show();
                }
            }
        }
    
        public void takePicture() {
            if (!presenter.getDeviceRuntimeService().hasVideoPermission()) {
                requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_TAKE_PICTURE);
                return;
            }
            Context c = getContext();
            if (c == null)
                return;
            try {
                File photoFile = AndroidFileUtils.createImageFile(c);
                Log.i(TAG, "takePicture: trying to save to " + photoFile);
                android.net.Uri photoURI = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, photoFile);
                Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                        .putExtra("android.intent.extras.CAMERA_FACING", 1)
                        .putExtra("android.intent.extras.LENS_FACING_FRONT", 1)
                        .putExtra("android.intent.extra.USE_FRONT_CAMERA", true);
                mCurrentPhoto = photoFile;
                startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE);
            } catch (Exception e) {
                Toast.makeText(c, "Error taking picture: " + e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
            }
        }
    
        @Override
        public void askWriteExternalStoragePermission() {
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, JamiApplication.PERMISSIONS_REQUEST);
        }
    
        @Override
        public void openFilePicker() {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("*/*");
    
            startActivityForResult(intent, REQUEST_CODE_FILE_PICKER);
        }
    
        private Completable sendFile(File file) {
            return Completable.fromAction(() -> presenter.sendFile(file));
        }
    
        private void startFileSend(Completable op) {
            setLoading(true);
            op.observeOn(AndroidSchedulers.mainThread())
                    .doFinally(() -> setLoading(false))
                    .subscribe(() -> {}, e -> {
                        Log.e(TAG, "startFileSend: not able to create cache file", e);
                        displayErrorToast(Error.INVALID_FILE);
                    });
        }
    
        @Override
        public void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
            Log.w(TAG, "onActivityResult: " + requestCode + " " + resultCode + " " + resultData);
            android.net.Uri uri = resultData == null ? null : resultData.getData();
            if (requestCode == REQUEST_CODE_FILE_PICKER) {
                if (resultCode == RESULT_OK && uri != null) {
                    startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri)
                            .observeOn(AndroidSchedulers.mainThread())
                            .flatMapCompletable(this::sendFile));
                }
            } else if (requestCode == REQUEST_CODE_TAKE_PICTURE
                    || requestCode == REQUEST_CODE_CAPTURE_AUDIO
                    || requestCode == REQUEST_CODE_CAPTURE_VIDEO)
            {
                if (resultCode != RESULT_OK) {
                    mCurrentPhoto = null;
                    return;
                }
                Log.w(TAG, "onActivityResult: mCurrentPhoto " + mCurrentPhoto.getAbsolutePath() + " " + mCurrentPhoto.exists() + " " + mCurrentPhoto.length());
                Single<File> file = null;
                if (mCurrentPhoto == null || !mCurrentPhoto.exists() || mCurrentPhoto.length() == 0) {
                    if (uri != null) {
                        file = AndroidFileUtils.getCacheFile(requireContext(), uri);
                    }
                } else {
                    file = Single.just(mCurrentPhoto);
                }
                mCurrentPhoto = null;
                if (file == null) {
                    Toast.makeText(getActivity(), "Can't find picture", Toast.LENGTH_SHORT).show();
                    return;
                }
                startFileSend(file.flatMapCompletable(this::sendFile));
            }
            // File download trough SAF
            else if (requestCode == ConversationFragment.REQUEST_CODE_SAVE_FILE) {
                if (resultCode == RESULT_OK &&  uri != null) {
                    writeToFile(uri);
                }
            }
        }
    
        private void writeToFile(android.net.Uri data) {
            File input = new File(mCurrentFileAbsolutePath);
            if (requireContext().getContentResolver() != null)
                mCompositeDisposable.add(AndroidFileUtils.copyFileToUri(requireContext().getContentResolver(), input, data).
                        observeOn(AndroidSchedulers.mainThread()).
                        subscribe(() -> Toast.makeText(getContext(), R.string.file_saved_successfully, Toast.LENGTH_SHORT).show(),
                                error -> Toast.makeText(getContext(), R.string.generic_error, Toast.LENGTH_SHORT).show()));
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            for (int i = 0, n = permissions.length; i < n; i++) {
                boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
                switch (permissions[i]) {
                    case Manifest.permission.CAMERA:
                        presenter.cameraPermissionChanged(granted);
                        if (granted) {
                            if (requestCode == REQUEST_CODE_CAPTURE_VIDEO) {
                                sendVideoMessage();
                            } else if (requestCode == REQUEST_CODE_TAKE_PICTURE) {
                                takePicture();
                            }
                        }
                        return;
                    case Manifest.permission.RECORD_AUDIO:
                        if (granted) {
                            if (requestCode == REQUEST_CODE_CAPTURE_AUDIO) {
                                sendAudioMessage();
                            }
                        }
                        return;
                    default:
                        break;
                }
            }
        }
    
        @Override
        public void addElement(Interaction element) {
            mAdapter.add(element);
            scrollToEnd();
        }
    
        @Override
        public void updateElement(Interaction element) {
            mAdapter.update(element);
        }
    
        @Override
        public void removeElement(Interaction element) {
            mAdapter.remove(element);
        }
    
        @Override
        public void setComposingStatus(Account.ComposingStatus composingStatus) {
            mAdapter.setComposingStatus(composingStatus);
            if (composingStatus == Account.ComposingStatus.Active)
                scrollToEnd();
        }
    
        @Override
        public void setLastDisplayed(Interaction interaction) {
            mAdapter.setLastDisplayed(interaction);
        }
    
        @Override
        public void shareFile(File path) {
            Context c = getContext();
            if (c == null)
                return;
            android.net.Uri fileUri = null;
            try {
                fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path);
            } catch (IllegalArgumentException e) {
                Log.e("File Selector", "The selected file can't be shared: " + path.getName());
            }
            if (fileUri != null) {
                Intent sendIntent = new Intent();
                sendIntent.setAction(Intent.ACTION_SEND);
                sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                String type = c.getContentResolver().getType(fileUri);
                sendIntent.setDataAndType(fileUri, type);
                sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
                startActivity(Intent.createChooser(sendIntent, null));
            }
        }
    
        @Override
        public void openFile(File path) {
            Context c = getContext();
            if (c == null)
                return;
            android.net.Uri fileUri = null;
            try {
                fileUri = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, path);
            } catch (IllegalArgumentException e) {
                Log.e("File Selector", "The selected file can't be shared: " + path.getName());
            }
            if (fileUri != null) {
                Intent sendIntent = new Intent();
                sendIntent.setAction(Intent.ACTION_VIEW);
                sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                String type = c.getContentResolver().getType(fileUri);
                sendIntent.setDataAndType(fileUri, type);
                sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
                //startActivity(Intent.createChooser(sendIntent, null));
                try {
                    startActivity(sendIntent);
                } catch (ActivityNotFoundException e) {
                    Snackbar.make(getView(), R.string.conversation_open_file_error, Snackbar.LENGTH_LONG).show();
                    Log.e("File Loader", "File of unknown type, could not open: " + path.getName());
                }
            }
        }
    
        boolean actionSendMsgText(int actionId) {
            switch (actionId) {
                case EditorInfo.IME_ACTION_SEND:
                    sendMessageText();
                    return true;
            }
            return false;
        }
    
        public void onClick() {
            presenter.clickOnGoingPane();
        }
    
        @Override
        public void onPause() {
            super.onPause();
            presenter.pause();
        }
    
        @Override
        public void onResume() {
            super.onResume();
            presenter.resume(mIsBubble);
        }
    
        @Override
        public void onDestroy() {
            mCompositeDisposable.dispose();
            super.onDestroy();
        }
    
        @Override
        public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
            if (!isVisible()) {
                return;
            }
            inflater.inflate(R.menu.conversation_actions, menu);
            mAudioCallBtn = menu.findItem(R.id.conv_action_audiocall);
            mVideoCallBtn = menu.findItem(R.id.conv_action_videocall);
        }
    
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            switch (item.getItemId()) {
                case android.R.id.home:
                    startActivity(new Intent(getActivity(), HomeActivity.class));
                    return true;
                case R.id.conv_action_audiocall:
                    presenter.goToCall(true);
                    return true;
                case R.id.conv_action_videocall:
                    presenter.goToCall(false);
                    return true;
                case R.id.conv_contact_details:
                    presenter.openContact();
                    return true;
                default:
                    return super.onOptionsItemSelected(item);
            }
        }
    
        @Override
        protected void initPresenter(ConversationPresenter presenter) {
            ConversationPath path = ConversationPath.fromBundle(getArguments());
            mIsBubble = getArguments().getBoolean(NotificationServiceImpl.EXTRA_BUBBLE);
            if (path == null)
                return;
    
            Uri contactUri = path.getConversationUri();
            mAdapter = new ConversationAdapter(this, presenter);
            presenter.init(contactUri, path.getAccountId());
            try {
                mPreferences = requireActivity().getSharedPreferences(path.getAccountId() + "_" + contactUri.getRawRingId(), Context.MODE_PRIVATE);
                mPreferences.registerOnSharedPreferenceChangeListener(this);
                presenter.setConversationColor(mPreferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)));
            } catch (Exception e) {
                Log.e(TAG, "Can't load conversation preferences");
            }
    
            if (locationServiceConnection == null) {
                locationServiceConnection = new ServiceConnection() {
                    @Override
                    public void onServiceConnected(ComponentName name, IBinder service) {
                        Log.w(TAG, "onServiceConnected");
                        LocationSharingService.LocalBinder binder = (LocationSharingService.LocalBinder) service;
                        LocationSharingService locationService = binder.getService();
                        ConversationPath path = new ConversationPath(presenter.getPath());
                        if (locationService.isSharing(path)) {
                            showMap(path.getAccountId(), contactUri.getUri(), false);
                        }
                        try {
                            requireContext().unbindService(locationServiceConnection);
                        } catch (Exception e) {
                            Log.w(TAG, "Error unbinding service", e);
                        }
                    }
    
                    @Override
                    public void onServiceDisconnected(ComponentName name) {
                        Log.w(TAG, "onServiceDisconnected");
                        locationServiceConnection = null;
                    }
                };
    
                Log.w(TAG, "bindService");
                requireContext().bindService(new Intent(requireContext(), LocationSharingService.class), locationServiceConnection, 0);
            }
        }
    
        @Override
        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
            switch (key) {
                case KEY_PREFERENCE_CONVERSATION_COLOR:
                    presenter.setConversationColor(prefs.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, getResources().getColor(R.color.color_primary_light)));
                    break;
            }
        }
    
        @Override
        public void displayContact(final CallContact contact) {
            mCompositeDisposable.clear();
            mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact)
                    .doOnSuccess(d -> {
                        mConversationAvatar = (AvatarDrawable) d;
                        mParticipantAvatars.put(contact.getPrimaryNumber(),
                                new AvatarDrawable((AvatarDrawable) d));
                    })
                    .flatMapObservable(d -> contact.getUpdatesSubject())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(c -> {
                        mConversationAvatar.update(c);
                        String uri = contact.getPrimaryNumber();
                        AvatarDrawable ad = mParticipantAvatars.get(uri);
                        if (ad != null)
                            ad.update(c);
                        setupActionbar(contact);
                        mAdapter.setPhoto();
                    }));
            mCompositeDisposable.add(AvatarFactory.getAvatar(requireContext(), contact, false)
                    .doOnSuccess(d -> mSmallParticipantAvatars.put(contact.getPrimaryNumber(), new AvatarDrawable((AvatarDrawable) d)))
                    .flatMapObservable(d -> contact.getUpdatesSubject())
                    .subscribe(c -> {
                        synchronized (mSmallParticipantAvatars) {
                            String uri = contact.getPrimaryNumber();
                            AvatarDrawable ad = mSmallParticipantAvatars.get(uri);
                            if (ad != null)
                                ad.update(c);
                        }
                    }));
        }
    
        @Override
        public void displayOnGoingCallPane(final boolean display) {
            binding.ongoingcallPane.setVisibility(display ? View.VISIBLE : View.GONE);
        }
    
        @Override
        public void displayNumberSpinner(final Conversation conversation, final Uri number) {
            binding.numberSelector.setVisibility(View.VISIBLE);
            binding.numberSelector.setAdapter(new NumberAdapter(getActivity(),
                    conversation.getContact(), false));
            binding.numberSelector.setSelection(getIndex(binding.numberSelector, number));
        }
    
        @Override
        public void hideNumberSpinner() {
            binding.numberSelector.setVisibility(View.GONE);
        }
    
        @Override
        public void clearMsgEdit() {
            binding.msgInputTxt.setText("");
        }
    
        @Override
        public void goToHome() {
            if (getActivity() instanceof ConversationActivity) {
                getActivity().finish();
            }
        }
    
        @Override
        public void goToAddContact(CallContact callContact) {
            startActivityForResult(ActionHelper.getAddNumberIntentForContact(callContact), REQ_ADD_CONTACT);
        }
    
        @Override
        public void goToCallActivity(String conferenceId) {
            startActivity(new Intent(Intent.ACTION_VIEW)
                    .setClass(requireActivity().getApplicationContext(), CallActivity.class)
                    .putExtra(NotificationService.KEY_CALL_ID, conferenceId));
        }
    
        @Override
        public void goToContactActivity(String accountId, String contactId) {
            startActivity(new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contactId),
                    requireActivity().getApplicationContext(), ContactDetailsActivity.class));
        }
    
        @Override
        public void goToCallActivityWithResult(String accountId, String contactRingId, boolean audioOnly) {
            Intent intent = new Intent(CallActivity.ACTION_CALL)
                    .setClass(requireActivity().getApplicationContext(), CallActivity.class)
                    .putExtra(KEY_ACCOUNT_ID, accountId)
                    .putExtra(CallFragment.KEY_AUDIO_ONLY, audioOnly)
                    .putExtra(KEY_CONTACT_RING_ID, contactRingId);
            startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL);
        }
    
        private void setupActionbar(CallContact contact) {
            if (!isVisible()) {
                return;
            }
    
            ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
            if (actionBar == null) {
                return;
            }
    
            Context context = actionBar.getThemedContext();
            String displayName = contact.getDisplayName();
            String identity = contact.getRingUsername();
    
            Activity activity = getActivity();
            if (activity instanceof HomeActivity) {
                Toolbar toolbar = getActivity().findViewById(R.id.main_toolbar);
                TextView title = toolbar.findViewById(R.id.contact_title);
                TextView subtitle = toolbar.findViewById(R.id.contact_subtitle);
                ImageView logo = toolbar.findViewById(R.id.contact_image);
    
                if (!((HomeActivity) activity).isConversationSelected()) {
                    title.setText("");
                    subtitle.setText("");
                    logo.setImageDrawable(null);
                    return;
                }
    
                logo.setVisibility(View.VISIBLE);
                title.setText(displayName);
                title.setTextSize(15);
                title.setTypeface(null, Typeface.NORMAL);
    
                if (identity != null && !identity.equals(displayName)) {
                    subtitle.setText(identity);
    
                    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) title.getLayoutParams();
                    params.addRule(RelativeLayout.ALIGN_TOP, R.id.contact_image);
                    title.setLayoutParams(params);
                } else {
                    subtitle.setText("");
    
                    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) title.getLayoutParams();
                    params.removeRule(RelativeLayout.ALIGN_TOP);
                    params.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
                    title.setLayoutParams(params);
                }
    
                logo.setImageDrawable(mConversationAvatar);
            } else {
                if (identity != null && !identity.equals(displayName)) {
                    actionBar.setSubtitle(identity);
                }
                actionBar.setTitle(displayName);
                int targetSize = (int) (AvatarFactory.SIZE_AB * context.getResources().getDisplayMetrics().density);
                mConversationAvatar.setInSize(targetSize);
                actionBar.setLogo(null);
                actionBar.setLogo(mConversationAvatar);
            }
        }
    
        public void blockContactRequest() {
            presenter.onBlockIncomingContactRequest();
        }
    
        public void refuseContactRequest() {
            presenter.onRefuseIncomingContactRequest();
        }
    
        public void acceptContactRequest() {
            presenter.onAcceptIncomingContactRequest();
        }
    
        public void addContact() {
            presenter.onAddContact();
        }
    
        @Override
        public void onPrepareOptionsMenu(@NonNull Menu menu) {
            super.onPrepareOptionsMenu(menu);
            boolean visible = binding.cvMessageInput.getVisibility() == View.VISIBLE;
            if (mAudioCallBtn != null)
                mAudioCallBtn.setVisible(visible);
            if (mVideoCallBtn != null)
                mVideoCallBtn.setVisible(visible);
        }
    
        @Override
        public void switchToUnknownView(String contactDisplayName) {
            binding.cvMessageInput.setVisibility(View.GONE);
            binding.unknownContactPrompt.setVisibility(View.VISIBLE);
            binding.trustRequestPrompt.setVisibility(View.GONE);
            binding.tvTrustRequestMessage.setText(String.format(getString(R.string.message_contact_not_trusted), contactDisplayName));
            binding.trustRequestMessageLayout.setVisibility(View.VISIBLE);
            currentBottomView = binding.unknownContactPrompt;
            requireActivity().invalidateOptionsMenu();
            updateListPadding();
        }
    
        @Override
        public void switchToIncomingTrustRequestView(String contactDisplayName) {
            binding.cvMessageInput.setVisibility(View.GONE);
            binding.unknownContactPrompt.setVisibility(View.GONE);
            binding.trustRequestPrompt.setVisibility(View.VISIBLE);
            binding.tvTrustRequestMessage.setText(String.format(getString(R.string.message_contact_not_trusted_yet), contactDisplayName));
            binding.trustRequestMessageLayout.setVisibility(View.VISIBLE);
            currentBottomView = binding.trustRequestPrompt;
            requireActivity().invalidateOptionsMenu();
            updateListPadding();
        }
    
        @Override
        public void switchToConversationView() {
            binding.cvMessageInput.setVisibility(View.VISIBLE);
            binding.unknownContactPrompt.setVisibility(View.GONE);
            binding.trustRequestPrompt.setVisibility(View.GONE);
            binding.trustRequestMessageLayout.setVisibility(View.GONE);
            currentBottomView = binding.cvMessageInput;
            requireActivity().invalidateOptionsMenu();
            updateListPadding();
        }
    
        @Override
        public void positiveMediaButtonClicked() {
            presenter.clickOnGoingPane();
        }
    
        @Override
        public void negativeMediaButtonClicked() {
            presenter.clickOnGoingPane();
        }
    
        @Override
        public void toggleMediaButtonClicked() {
            presenter.clickOnGoingPane();
        }
    
        private void setLoading(boolean isLoading) {
            if (binding == null)
                return;
            if (isLoading) {
                binding.btnTakePicture.setVisibility(View.GONE);
                binding.pbDataTransfer.setVisibility(View.VISIBLE);
            } else {
                binding.btnTakePicture.setVisibility(View.VISIBLE);
                binding.pbDataTransfer.setVisibility(View.GONE);
            }
        }
    
        public void handleShareIntent(Intent intent) {
            Log.w(TAG, "handleShareIntent " + intent);
    
            String action = intent.getAction();
            if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                String type = intent.getType();
                if (type == null) {
                    Log.w(TAG, "Can't share with no type");
                    return;
                }
                if (type.startsWith("text/plain")) {
                    binding.msgInputTxt.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
                } else {
                    android.net.Uri uri = intent.getData();
                    ClipData clip = intent.getClipData();
                    if (uri == null && clip != null && clip.getItemCount() > 0)
                        uri = clip.getItemAt(0).getUri();
                    if (uri == null)
                        return;
                    startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri).flatMapCompletable(this::sendFile));
                }
            } else if (Intent.ACTION_VIEW.equals(action)) {
                ConversationPath path = ConversationPath.fromIntent(intent);
                if (path != null && intent.getBooleanExtra(EXTRA_SHOW_MAP, false)) {
                    shareLocation();
                }
            }
        }
    
        /**
         * Creates an intent using Android Storage Access Framework
         * This intent is then received by applications that can handle it like
         * Downloads or Google drive
         * @param file DataTransfer of the file that is going to be stored
         * @param currentFileAbsolutePath absolute path of the file we want to save
         */
        public void startSaveFile(DataTransfer file, String currentFileAbsolutePath){
            //Get the current file absolute path and store it
            mCurrentFileAbsolutePath = currentFileAbsolutePath;
    
            try {
                //Use Android Storage File Access to download the file
                Intent downloadFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                downloadFileIntent.setType(AndroidFileUtils.getMimeTypeFromExtension(file.getExtension()));
                downloadFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
                downloadFileIntent.putExtra(Intent.EXTRA_TITLE,file.getDisplayName());
    
                startActivityForResult(downloadFileIntent, ConversationFragment.REQUEST_CODE_SAVE_FILE);
            } catch (Exception e) {
                Log.i(TAG, "No app detected for saving files.");
                File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                if (!directory.exists()) {
                    directory.mkdirs();
                }
                writeToFile(android.net.Uri.fromFile(new File(directory, file.getDisplayName())));
            }
        }
    
        @Override
        public void displayNetworkErrorPanel() {
            if (binding != null) {
                binding.errorMsgPane.setVisibility(View.VISIBLE);
                binding.errorMsgPane.setOnClickListener(null);
                binding.errorMsgPane.setText(R.string.error_no_network);
            }
        }
    
        @Override
        public void displayAccountOfflineErrorPanel() {
            if (binding != null) {
                binding.errorMsgPane.setVisibility(View.VISIBLE);
                binding.errorMsgPane.setOnClickListener(null);
                binding.errorMsgPane.setText(R.string.error_account_offline);
                for ( int idx = 0 ; idx < binding.btnContainer.getChildCount() ; idx++) {
                    binding.btnContainer.getChildAt(idx).setEnabled(false);
                }
            }
        }
    
        @Override
        public void setReadIndicatorStatus(boolean show) {
            if (mAdapter != null) {
                mAdapter.setReadIndicatorStatus(show);
            }
        }
    
        @Override
        public void hideErrorPanel() {
            if (binding != null) {
                binding.errorMsgPane.setVisibility(View.GONE);
            }
        }
    
    }