blob: d25c9485fd568b336312accf377045a43aa33716 [file] [log] [blame]
/*
* 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.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);
if (presenter.isRecordingBlocked()) {
getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
}
@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;
}
return false;
});
MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) popup.getMenu(), v);
menuHelper.setForceShowIcon(true);
menuHelper.show();
}
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 {
Context ctx = requireContext();
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
try {
mCurrentPhoto = AndroidFileUtils.createAudioFile(ctx);
} catch (IOException ex) {
Log.e(TAG, "takePicture: error creating temporary file", ex);
return;
}
startActivityForResult(intent, REQUEST_CODE_CAPTURE_AUDIO);
} else {
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 {
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);
if (intent.resolveActivity(context.getPackageManager()) != null) {
try {
mCurrentPhoto = AndroidFileUtils.createVideoFile(context);
} catch (IOException ex) {
Log.e(TAG, "takePicture: error creating temporary file", ex);
return;
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, ContentUriHandler.getUriForFile(context, ContentUriHandler.AUTHORITY_FILES, mCurrentPhoto));
startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO);
} else {
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 setReadIndicatorStatus(boolean show) {
if (mAdapter != null) {
mAdapter.setReadIndicatorStatus(show);
}
}
@Override
public void hideErrorPanel() {
if (binding != null) {
binding.errorMsgPane.setVisibility(View.GONE);
}
}
}