Files
arcanechat-android/src/org/thoughtcrime/securesms/ConversationFragment.java
T
bjoern 853000f8dc option to add webxdc apps to home screen (#2353)
* add 'Add to Home Screen' menus for webxdc apps

* make adding shortcut basically work

* avoid starting webxdc from foreign accounts

* recreate back stack, if possible

according to
https://developer.android.com/training/notify-user/navigation :
"When you start an activity from a notification,
you must preserve the user's expected navigation experience.
Tapping Back should take the user
back through the app's normal work flow to the Home screen [...]"

same seems to be true for shortcuts using the same class for constructing
the back stack.

the back stack is reconstructed for API 26 (Oreo, 2017) or newer,
for oder API `requestPinShortcut()`
only uses the top-level activity for the shortcut.
working around that is probably possible, but would be quite some effort in an
already complicated area and would result in bugs that are not so easy to see.

also, on these old androids, users are probably used to the no-back-stack
behaviour.

* show a warning if app was deleted

* add 'Add to Home Screen' also to gallery

beside chat- and webxdc-activity,
this is the third place where the webxdc is listed.

as adding may result in dialogs from the launcher,
we allow the option only for a single selected webxdc,
as otherwise a series of dialogs would pop up.

* show 'Done' toast short, this is what we are doing at all the other places when showing 'Done'
2022-08-15 11:30:14 +02:00

989 lines
40 KiB
Java

/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import static com.b44t.messenger.DcContact.DC_CONTACT_ID_SELF;
import static org.thoughtcrime.securesms.util.RelayUtil.setForwardingMessageIds;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
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.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
import org.thoughtcrime.securesms.videochat.VideochatUtil;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends MessageSelectorFragment
{
private static final String TAG = ConversationFragment.class.getSimpleName();
private static final String KEY_LIMIT = "limit";
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
private static final int CODE_ADD_EDIT_CONTACT = 77;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
private ConversationFragmentListener listener;
private Recipient recipient;
private long chatId;
private int startingPosition;
private boolean firstLoad;
private Locale locale;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private StickyHeaderDecoration dateDecoration;
private View scrollToBottomButton;
private View floatingLocationButton;
private TextView noMessageTextView;
private Timer reloadTimer;
public boolean isPaused;
private Debouncer markseenDebouncer;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
this.dcContext = DcHelper.getContext(getContext());
DcEventCenter eventCenter = DcHelper.getEventCenter(getContext());
eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_DELIVERED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_FAILED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
markseenDebouncer = new Debouncer(800);
reloadTimer = new Timer("reloadTimer", false);
reloadTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Util.runOnMain(ConversationFragment.this::reloadList);
}
}, 60 * 1000, 60 * 1000);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
list = ViewUtil.findById(view, android.R.id.list);
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
final SetStartingPositionLinearLayoutManager layoutManager = new SetStartingPositionLinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, true);
list.setHasFixedSize(false);
list.setLayoutManager(layoutManager);
list.setItemAnimator(null);
new ConversationItemSwipeCallback(
msg -> actionMode == null,
this::handleReplyMessage
).attachToRecyclerView(list);
// setLayerType() is needed to allow larger items (long texts in our case)
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
return view;
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initializeResources();
initializeListAdapter();
}
private void setNoMessageText() {
DcChat dcChat = getListAdapter().getChat();
if(dcChat.isMultiUser()){
if (dcChat.isBroadcast()) {
noMessageTextView.setText(R.string.chat_new_broadcast_hint);
} else if (dcChat.isUnpromoted()) {
noMessageTextView.setText(R.string.chat_new_group_hint);
}
else {
noMessageTextView.setText(R.string.chat_no_messages);
}
}
else if(dcChat.isSelfTalk()) {
noMessageTextView.setText(R.string.saved_messages_explain);
}
else if(dcChat.isDeviceTalk()) {
noMessageTextView.setText(R.string.device_talk_explain);
}
else {
String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName());
noMessageTextView.setText(message);
}
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(getContext()).removeObservers(this);
reloadTimer.cancel();
super.onDestroy();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.listener = (ConversationFragmentListener)activity;
}
@Override
public void onResume() {
super.onResume();
dcContext.marknoticedChat((int) chatId);
if (list.getAdapter() != null) {
list.getAdapter().notifyDataSetChanged();
}
if (isPaused) {
isPaused = false;
markseenDebouncer.publish(() -> manageMessageSeenState());
}
}
@Override
public void onPause() {
super.onPause();
setLastSeen(System.currentTimeMillis());
isPaused = true;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
dateDecoration.onConfigurationChanged(newConfig);
}
public void onNewIntent() {
if (actionMode != null) {
actionMode.finish();
}
initializeResources();
initializeListAdapter();
if (chatId == -1) {
reloadList();
updateLocationButton();
}
}
public void moveToLastSeen() {
if (list == null || getListAdapter() == null) {
Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet.");
return;
}
if (getListAdapter().getLastSeenPosition() < 0) {
return;
}
final int lastSeenPosition = getListAdapter().getLastSeenPosition() + 1;
if (lastSeenPosition > 0) {
list.post(() -> ((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
}
}
private void initializeResources() {
this.chatId = this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId));
this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
this.firstLoad = true;
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
list.addOnScrollListener(scrollListener);
}
private void initializeListAdapter() {
if (this.recipient != null && this.chatId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), locale, selectionClickListener, this.recipient);
list.setAdapter(adapter);
dateDecoration = new StickyHeaderDecoration(adapter, false, false);
list.addItemDecoration(dateDecoration);
int freshMsgs = dcContext.getFreshMsgCount((int) chatId);
SetStartingPositionLinearLayoutManager layoutManager = (SetStartingPositionLinearLayoutManager) list.getLayoutManager();
if (startingPosition > -1) {
layoutManager.setStartingPosition(startingPosition);
} else if (freshMsgs > 0) {
layoutManager.setStartingPosition(freshMsgs - 1);
}
reloadList();
updateLocationButton();
if (freshMsgs > 0) {
getListAdapter().setLastSeenPosition(freshMsgs - 1);
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter());
list.addItemDecoration(lastSeenDecoration);
}
}
}
@Override
protected void setCorrectMenuVisibility(Menu menu) {
Set<DcMsg> messageRecords = getListAdapter().getSelectedItems();
if (actionMode != null && messageRecords.size() == 0) {
actionMode.finish();
return;
}
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_share).setVisible(false);
menu.findItem(R.id.menu_context_reply).setVisible(false);
menu.findItem(R.id.menu_context_reply_privately).setVisible(false);
menu.findItem(R.id.menu_add_to_home_screen).setVisible(false);
} else {
DcMsg messageRecord = messageRecords.iterator().next();
DcChat chat = getListAdapter().getChat();
menu.findItem(R.id.menu_context_details).setVisible(true);
menu.findItem(R.id.menu_context_share).setVisible(messageRecord.hasFile());
boolean canReply = canReplyToMsg(messageRecord);
menu.findItem(R.id.menu_context_reply).setVisible(chat.canSend() && canReply);
boolean showReplyPrivately = chat.isMultiUser() && !messageRecord.isOutgoing() && canReply;
menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately);
menu.findItem(R.id.menu_add_to_home_screen).setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC);
}
// if one of the selected items cannot be saved, disable saving.
boolean canSave = true;
// if one of the selected items is not from self, disable resending.
boolean canResend = true;
for (DcMsg messageRecord : messageRecords) {
if (canSave && !messageRecord.hasFile()) {
canSave = false;
}
if (canResend && !messageRecord.isOutgoing()) {
canResend = false;
}
if (!canSave && !canResend) {
break;
}
}
menu.findItem(R.id.menu_context_save_attachment).setVisible(canSave);
menu.findItem(R.id.menu_resend).setVisible(canResend);
}
static boolean canReplyToMsg(DcMsg dcMsg) {
return !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_VIDEOCHAT_INVITATION;
}
private ConversationAdapter getListAdapter() {
return (ConversationAdapter) list.getAdapter();
}
public void reload(Recipient recipient, long chatId) {
this.recipient = recipient;
if (this.chatId != chatId) {
this.chatId = chatId;
initializeListAdapter();
}
}
public void scrollToTop() {
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
if (adapter.getItemCount()>0) {
final int pos = adapter.getItemCount()-1;
list.post(() -> {
list.getLayoutManager().scrollToPosition(pos);
});
}
}
public void scrollToBottom() {
if (((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition() < SCROLL_ANIMATION_THRESHOLD
&& !AccessibilityUtil.areAnimationsDisabled(getContext())) {
list.smoothScrollToPosition(0);
} else {
list.scrollToPosition(0);
}
}
void setLastSeen(long lastSeen) {
getListAdapter().setLastSeen(lastSeen);
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
if (lastSeen > 0) {
lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter());
list.addItemDecoration(lastSeenDecoration);
}
}
private void handleCopyMessage(final Set<DcMsg> dcMsgsSet) {
List<DcMsg> dcMsgsList = new LinkedList<>(dcMsgsSet);
Collections.sort(dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived()));
boolean singleMsg = dcMsgsList.size() == 1;
StringBuilder result = new StringBuilder();
DcMsg prevMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
for (DcMsg msg : dcMsgsList) {
if (result.length() > 0) {
result.append("\n\n");
}
if (msg.getFromId() != prevMsg.getFromId() && !singleMsg) {
DcContact contact = dcContext.getContact(msg.getFromId());
result.append(msg.getSenderName(contact, false)).append(":\n");
}
if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) {
result.append(msg.getText());
} else {
result.append(msg.getSummarytext(10000000));
}
prevMsg = msg;
}
if (result.length() > 0) {
Util.writeTextToClipboard(getActivity(), result.toString());
Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.copied_to_clipboard), Toast.LENGTH_LONG).show();
}
}
private void handleForwardMessage(final Set<DcMsg> messageRecords) {
Intent composeIntent = new Intent();
int[] msgIds = DcMsg.msgSetToIds(messageRecords);
setForwardingMessageIds(composeIntent, msgIds);
ConversationListRelayingActivity.start(this, composeIntent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@SuppressLint("RestrictedApi")
private void handleReplyMessage(final DcMsg message) {
if (getActivity() != null) {
//noinspection ConstantConditions
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
}
listener.handleReplyMessage(message);
}
private void handleReplyMessagePrivately(final DcMsg msg) {
if (getActivity() != null) {
int privateChatId = dcContext.createChatByContactId(msg.getFromId());
DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
replyMsg.setQuote(msg);
dcContext.setDraft(privateChatId, replyMsg);
Intent intent = new Intent(getActivity(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, privateChatId);
getActivity().startActivity(intent);
getActivity().finish();
} else {
Log.e(TAG, "Activity was null");
}
}
private void handleResendMessage(final Set<DcMsg> dcMsgsSet) {
int[] ids = DcMsg.msgSetToIds(dcMsgsSet);
if (dcContext.resendMsgs(ids)) {
actionMode.finish();
Toast.makeText(getContext(), R.string.sending, Toast.LENGTH_SHORT).show();
} else {
new AlertDialog.Builder(getContext())
.setMessage(dcContext.getLastError())
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
private void reloadList() {
reloadList(false);
}
private void reloadList(boolean chatModified) {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return;
}
// if chat is a contact request and is accepted/blocked, the DcChat object must be reloaded, otherwise DcChat.canSend() returns wrong values
if (chatModified) {
adapter.reloadChat();
}
int oldCount = 0;
int oldIndex = 0;
int pixelOffset = 0;
if (!firstLoad) {
oldCount = adapter.getItemCount();
oldIndex = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
View firstView = list.getLayoutManager().findViewByPosition(oldIndex);
pixelOffset = (firstView == null) ? 0 : list.getBottom() - firstView.getBottom() - list.getPaddingBottom();
}
if (getContext() == null) {
Log.e(TAG, "reloadList: getContext() was null");
return;
}
DcContext dcContext = DcHelper.getContext(getContext());
long startMs = System.currentTimeMillis();
int[] msgs = dcContext.getChatMsgs((int) chatId, 0, 0);
Log.i(TAG, "⏰ getChatMsgs(" + chatId + "): " + (System.currentTimeMillis() - startMs) + "ms");
adapter.changeData(msgs);
if (firstLoad) {
if (startingPosition >= 0) {
getListAdapter().pulseHighlightItem(startingPosition);
}
firstLoad = false;
} else if(oldIndex > 0) {
int newIndex = oldIndex + msgs.length - oldCount;
if (newIndex < 0) { newIndex = 0; pixelOffset = 0; }
else if (newIndex >= msgs.length) { newIndex = msgs.length - 1; pixelOffset = 0; }
((LinearLayoutManager) list.getLayoutManager()).scrollToPositionWithOffset(newIndex, pixelOffset);
}
if(!adapter.isActive()){
setNoMessageText();
noMessageTextView.setVisibility(View.VISIBLE);
}
else{
noMessageTextView.setVisibility(View.GONE);
}
if (!isPaused) {
markseenDebouncer.publish(() -> manageMessageSeenState());
}
}
private void updateLocationButton() {
floatingLocationButton.setVisibility(dcContext.isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
}
private void scrollAndHighlight(final int pos, boolean smooth) {
list.post(() -> {
if (smooth && !AccessibilityUtil.areAnimationsDisabled(getContext())) {
list.smoothScrollToPosition(pos);
} else {
list.scrollToPosition(pos);
}
getListAdapter().pulseHighlightItem(pos);
});
}
public void scrollToMsgId(final int msgId) {
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
int position = adapter.msgIdToPosition(msgId);
if (position!=-1) {
scrollAndHighlight(position, false);
} else {
Log.e(TAG, "msgId {} not found for scrolling");
}
}
private void scrollMaybeSmoothToMsgId(final int msgId) {
LinearLayoutManager layout = ((LinearLayoutManager) list.getLayoutManager());
boolean smooth = false;
ConversationAdapter adapter = (ConversationAdapter) list.getAdapter();
if (adapter == null) return;
int position = adapter.msgIdToPosition(msgId);
if (layout != null) {
int distance1 = Math.abs(position - layout.findFirstVisibleItemPosition());
int distance2 = Math.abs(position - layout.findLastVisibleItemPosition());
int distance = Math.min(distance1, distance2);
smooth = distance < 15;
Log.i(TAG, "Scrolling to destMsg, smoth: " + smooth + ", distance: " + distance);
}
if (position != -1) {
scrollAndHighlight(position, smooth);
} else {
Log.e(TAG, "msgId not found for scrolling: " + msgId);
}
}
public interface ConversationFragmentListener {
void handleReplyMessage(DcMsg messageRecord);
}
private class ConversationScrollListener extends OnScrollListener {
private final Animation scrollButtonInAnimation;
private final Animation scrollButtonOutAnimation;
private boolean wasAtBottom = true;
private boolean wasAtZoomScrollHeight = false;
//private long lastPositionId = -1;
ConversationScrollListener(@NonNull Context context) {
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
this.scrollButtonInAnimation.setDuration(100);
this.scrollButtonOutAnimation.setDuration(50);
}
@Override
public void onScrolled(final RecyclerView rv, final int dx, final int dy) {
boolean currentlyAtBottom = isAtBottom();
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
// int positionId = getHeaderPositionId();
if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) {
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
} else if (currentlyAtBottom && !wasAtBottom) {
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
}
// if (positionId != lastPositionId) {
// bindScrollHeader(conversationDateHeader, positionId);
// }
wasAtBottom = currentlyAtBottom;
wasAtZoomScrollHeight = currentlyAtZoomScrollHeight;
// lastPositionId = positionId;
markseenDebouncer.publish(() -> manageMessageSeenState());
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
// conversationDateHeader.show();
// } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// conversationDateHeader.hide();
// }
}
private boolean isAtBottom() {
if (list.getChildCount() == 0) return true;
View bottomView = list.getChildAt(0);
int firstVisibleItem = ((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition();
boolean isAtBottom = (firstVisibleItem == 0);
return isAtBottom && bottomView.getBottom() <= list.getHeight();
}
private boolean isAtZoomScrollHeight() {
return ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 4;
}
// private int getHeaderPositionId() {
// return ((LinearLayoutManager)list.getLayoutManager()).findLastVisibleItemPosition();
// }
// private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) {
// if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) {
// ((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId);
// }
// }
}
private void manageMessageSeenState() {
LinearLayoutManager layoutManager = (LinearLayoutManager)list.getLayoutManager();
int firstPos = layoutManager.findFirstVisibleItemPosition();
int lastPos = layoutManager.findLastVisibleItemPosition();
if(firstPos == RecyclerView.NO_POSITION || lastPos == RecyclerView.NO_POSITION) {
return;
}
int[] ids = new int[lastPos - firstPos + 1];
int index = 0;
for(int pos = firstPos; pos <= lastPos; pos++) {
DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos);
if (message.getFromId() != DC_CONTACT_ID_SELF) {
ids[index] = message.getId();
index++;
}
}
dcContext.markseenMsgs(ids);
}
void querySetupCode(final DcMsg dcMsg, String[] preload)
{
if( !dcMsg.isSetupMessage()) {
return;
}
View gl = View.inflate(getActivity(), R.layout.setup_code_grid, null);
final EditText[] editTexts = {
gl.findViewById(R.id.setupCode0), gl.findViewById(R.id.setupCode1), gl.findViewById(R.id.setupCode2),
gl.findViewById(R.id.setupCode3), gl.findViewById(R.id.setupCode4), gl.findViewById(R.id.setupCode5),
gl.findViewById(R.id.setupCode6), gl.findViewById(R.id.setupCode7), gl.findViewById(R.id.setupCode8)
};
AlertDialog.Builder builder1 = new AlertDialog.Builder(getActivity());
builder1.setView(gl);
editTexts[0].setText(dcMsg.getSetupCodeBegin());
editTexts[0].setSelection(editTexts[0].getText().length());
for( int i = 0; i < 9; i++ ) {
if( preload != null && i < preload.length ) {
editTexts[i].setText(preload[i]);
editTexts[i].setSelection(editTexts[i].getText().length());
}
editTexts[i].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) {
if( s.length()==4 ) {
for ( int i = 0; i < 8; i++ ) {
if( editTexts[i].hasFocus() && editTexts[i+1].getText().length()<4 ) {
editTexts[i+1].requestFocus();
break;
}
}
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
builder1.setTitle(getActivity().getString(R.string.autocrypt_continue_transfer_title));
builder1.setMessage(getActivity().getString(R.string.autocrypt_continue_transfer_please_enter_code));
builder1.setNegativeButton(android.R.string.cancel, null);
builder1.setCancelable(false); // prevent the dialog from being dismissed accidentally (when the dialog is closed, the setup code is gone forever and the user has to create a new setup message)
builder1.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String setup_code = "";
final String[] preload1 = new String[9];
for ( int i = 0; i < 9; i++ ) {
preload1[i] = editTexts[i].getText().toString();
setup_code += preload1[i];
}
boolean success = dcContext.continueKeyTransfer(dcMsg.getId(), setup_code);
AlertDialog.Builder builder2 = new AlertDialog.Builder(getActivity());
builder2.setTitle(getActivity().getString(R.string.autocrypt_continue_transfer_title));
builder2.setMessage(getActivity().getString(success? R.string.autocrypt_continue_transfer_succeeded : R.string.autocrypt_bad_setup_code));
if( success ) {
builder2.setPositiveButton(android.R.string.ok, null);
}
else {
builder2.setNegativeButton(android.R.string.cancel, null);
builder2.setPositiveButton(R.string.autocrypt_continue_transfer_retry, (dialog1, which1) -> querySetupCode(dcMsg, preload1));
}
builder2.show();
});
builder1.show();
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
public void onItemClick(DcMsg messageRecord) {
if (actionMode != null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
if (getListAdapter().getSelectedItems().size() == 0) {
actionMode.finish();
} else {
Menu menu = actionMode.getMenu();
setCorrectMenuVisibility(menu);
ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
actionMode.setTitleOptionalHint(false); // the title represents important information, also indicating implicitly, more items can be selected
}
}
else if(messageRecord.isSetupMessage()) {
querySetupCode(messageRecord,null);
}
else if (messageRecord.getType()==DcMsg.DC_MSG_VIDEOCHAT_INVITATION) {
new VideochatUtil().join(getActivity(), messageRecord.getId());
}
else if(DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) {
DozeReminder.dozeReminderTapped(getContext());
}
else if(messageRecord.isInfo() && messageRecord.getParent() != null && messageRecord.getParent().getType() == DcMsg.DC_MSG_WEBXDC) {
scrollMaybeSmoothToMsgId(messageRecord.getParent().getId());
}
else {
String self_mail = dcContext.getConfig("configured_mail_user");
if (self_mail != null && !self_mail.isEmpty()
&& messageRecord.getText().contains(self_mail)
&& getListAdapter().getChat().isDeviceTalk()) {
// This is a device message informing the user that the password is wrong
startActivity(new Intent(getActivity(), RegistrationActivity.class));
}
}
}
@Override
public void onItemLongClick(DcMsg messageRecord) {
if (actionMode == null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
}
@Override
public void onQuoteClicked(DcMsg messageRecord) {
DcMsg quoted = messageRecord.getQuotedMsg();
if (quoted == null) {
Log.i(TAG, "Clicked on a quote whose original message we never had.");
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show();
return;
}
int foreignChatId = quoted.getChatId();
if (foreignChatId != 0 && foreignChatId != chatId) {
Intent intent = new Intent(getActivity(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId);
int start = DcMsg.getMessagePosition(quoted, dcContext);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start);
((ConversationActivity) getActivity()).hideSoftKeyboard();
if (getActivity() != null) {
getActivity().startActivity(intent);
getActivity().finish();
} else {
Log.e(TAG, "Activity was null");
}
} else {
scrollMaybeSmoothToMsgId(quoted.getId());
}
}
@Override
public void onShowFullClicked(DcMsg messageRecord) {
Intent intent = new Intent(getActivity(), FullMsgActivity.class);
intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId());
startActivity(intent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@Override
public void onDownloadClicked(DcMsg messageRecord) {
dcContext.downloadFullMsg(messageRecord.getId());
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
// ApplicationContext.getInstance(getContext().getApplicationContext())
// .getJobManager()
// .add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
}
}
private class ActionModeCallback implements ActionMode.Callback {
private int statusBarColor;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.conversation_context, menu);
mode.setTitle("1");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getActivity().getWindow();
statusBarColor = window.getStatusBarColor();
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
}
setCorrectMenuVisibility(menu);
ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
((ConversationAdapter)list.getAdapter()).clearSelection();
list.getAdapter().notifyDataSetChanged();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getActivity().getWindow().setStatusBarColor(statusBarColor);
}
actionMode = null;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_context_copy:
handleCopyMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
case R.id.menu_context_delete_message:
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
return true;
case R.id.menu_context_share:
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
return true;
case R.id.menu_context_details:
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
actionMode.finish();
return true;
case R.id.menu_context_forward:
handleForwardMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
case R.id.menu_add_to_home_screen:
WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId());
actionMode.finish();
return true;
case R.id.menu_context_save_attachment:
handleSaveAttachment(getListAdapter().getSelectedItems());
return true;
case R.id.menu_context_reply:
handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
actionMode.finish();
return true;
case R.id.menu_context_reply_privately:
handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
return true;
case R.id.menu_resend:
handleResendMessage(getListAdapter().getSelectedItems());
return true;
}
return false;
}
}
@Override
public void handleEvent(@NonNull DcEvent event) {
switch (event.getId()) {
case DcContext.DC_EVENT_MSGS_CHANGED:
if (event.getData1Int() == 0 // deleted messages or batch insert
|| event.getData1Int() == chatId) {
reloadList();
}
break;
case DcContext.DC_EVENT_INCOMING_MSG:
case DcContext.DC_EVENT_MSG_DELIVERED:
case DcContext.DC_EVENT_MSG_FAILED:
case DcContext.DC_EVENT_MSG_READ:
if (event.getData1Int() == chatId) {
reloadList();
}
break;
case DcContext.DC_EVENT_CHAT_MODIFIED:
if (event.getData1Int() == chatId) {
updateLocationButton();
reloadList(true);
}
break;
}
// removing the "new message" marker on incoming messages may be a bit unexpected,
// esp. when a series of message is coming in and after the first, the screen is turned on,
// the "new message" marker will flash for a short moment and disappear.
/*if (eventId == DcContext.DC_EVENT_INCOMING_MSG && isResumed()) {
setLastSeen(-1);
}*/
}
}