request bluetooth connect permission

Daniel Gultsch created

fixes #4338

Change summary

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 853 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java   |  19 
src/main/java/eu/siacs/conversations/utils/PermissionUtils.java   |  39 
3 files changed, 549 insertions(+), 362 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1,5 +1,12 @@
 package eu.siacs.conversations.ui;
 
+import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
+import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
+import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
 import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
@@ -55,6 +62,9 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
 import androidx.databinding.DataBindingUtil;
 
 import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -114,6 +124,7 @@ import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.MessageUtils;
 import eu.siacs.conversations.utils.NickValidityChecker;
 import eu.siacs.conversations.utils.Patterns;
+import eu.siacs.conversations.utils.PermissionUtils;
 import eu.siacs.conversations.utils.QuickLoader;
 import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.TimeFrameUtils;
@@ -129,18 +140,10 @@ import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
 import eu.siacs.conversations.xmpp.jingle.RtpCapability;
 
-import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
-import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
-import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
-import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
-import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
-import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
-
-import org.jetbrains.annotations.NotNull;
-
-
-public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
-
+public class ConversationFragment extends XmppFragment
+        implements EditMessage.KeyboardListener,
+                MessageAdapter.OnContactPictureLongClicked,
+                MessageAdapter.OnContactPictureClicked {
 
     public static final int REQUEST_SEND_MESSAGE = 0x0201;
     public static final int REQUEST_DECRYPT_PGP = 0x0202;
@@ -161,10 +164,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
     public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
 
     public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
-    public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid";
-    public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position";
-    public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews";
-    public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri";
+    public static final String STATE_CONVERSATION_UUID =
+            ConversationFragment.class.getName() + ".uuid";
+    public static final String STATE_SCROLL_POSITION =
+            ConversationFragment.class.getName() + ".scroll_position";
+    public static final String STATE_PHOTO_URI =
+            ConversationFragment.class.getName() + ".media_previews";
+    public static final String STATE_MEDIA_PREVIEWS =
+            ConversationFragment.class.getName() + ".take_photo_uri";
     private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid";
 
     private final List<Message> messageList = new ArrayList<>();
@@ -185,282 +192,376 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
     private Toast messageLoaderToast;
     private ConversationsActivity activity;
     private boolean reInitRequiredOnStart = true;
-    private final OnClickListener clickToMuc = new OnClickListener() {
+    private final OnClickListener clickToMuc =
+            new OnClickListener() {
 
-        @Override
-        public void onClick(View v) {
-            ConferenceDetailsActivity.open(getActivity(), conversation);
-        }
-    };
-    private final OnClickListener leaveMuc = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            activity.xmppConnectionService.archiveConversation(conversation);
-        }
-    };
-    private final OnClickListener joinMuc = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            activity.xmppConnectionService.joinMuc(conversation);
-        }
-    };
-
-    private final OnClickListener acceptJoin = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            conversation.setAttribute("accept_non_anonymous", true);
-            activity.xmppConnectionService.updateConversation(conversation);
-            activity.xmppConnectionService.joinMuc(conversation);
-        }
-    };
+                @Override
+                public void onClick(View v) {
+                    ConferenceDetailsActivity.open(getActivity(), conversation);
+                }
+            };
+    private final OnClickListener leaveMuc =
+            new OnClickListener() {
 
-    private final OnClickListener enterPassword = new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    activity.xmppConnectionService.archiveConversation(conversation);
+                }
+            };
+    private final OnClickListener joinMuc =
+            new OnClickListener() {
 
-        @Override
-        public void onClick(View v) {
-            MucOptions muc = conversation.getMucOptions();
-            String password = muc.getPassword();
-            if (password == null) {
-                password = "";
-            }
-            activity.quickPasswordEdit(password, value -> {
-                activity.xmppConnectionService.providePasswordForMuc(conversation, value);
-                return null;
-            });
-        }
-    };
-    private final OnScrollListener mOnScrollListener = new OnScrollListener() {
+                @Override
+                public void onClick(View v) {
+                    activity.xmppConnectionService.joinMuc(conversation);
+                }
+            };
+
+    private final OnClickListener acceptJoin =
+            new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    conversation.setAttribute("accept_non_anonymous", true);
+                    activity.xmppConnectionService.updateConversation(conversation);
+                    activity.xmppConnectionService.joinMuc(conversation);
+                }
+            };
 
-        @Override
-        public void onScrollStateChanged(AbsListView view, int scrollState) {
-            if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) {
-                fireReadEvent();
-            }
-        }
+    private final OnClickListener enterPassword =
+            new OnClickListener() {
 
-        @Override
-        public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
-            toggleScrollDownButton(view);
-            synchronized (ConversationFragment.this.messageList) {
-                if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
-                    long timestamp;
-                    if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
-                        timestamp = messageList.get(1).getTimeSent();
-                    } else {
-                        timestamp = messageList.get(0).getTimeSent();
+                @Override
+                public void onClick(View v) {
+                    MucOptions muc = conversation.getMucOptions();
+                    String password = muc.getPassword();
+                    if (password == null) {
+                        password = "";
                     }
-                    activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
-                        @Override
-                        public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
-                            if (ConversationFragment.this.conversation != conversation) {
-                                conversation.messagesLoaded.set(true);
-                                return;
-                            }
-                            runOnUiThread(() -> {
-                                synchronized (messageList) {
-                                    final int oldPosition = binding.messagesView.getFirstVisiblePosition();
-                                    Message message = null;
-                                    int childPos;
-                                    for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
-                                        message = messageList.get(oldPosition + childPos);
-                                        if (message.getType() != Message.TYPE_STATUS) {
-                                            break;
-                                        }
-                                    }
-                                    final String uuid = message != null ? message.getUuid() : null;
-                                    View v = binding.messagesView.getChildAt(childPos);
-                                    final int pxOffset = (v == null) ? 0 : v.getTop();
-                                    ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
-                                    try {
-                                        updateStatusMessages();
-                                    } catch (IllegalStateException e) {
-                                        Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
-                                    }
-                                    messageListAdapter.notifyDataSetChanged();
-                                    int pos = Math.max(getIndexOf(uuid, messageList), 0);
-                                    binding.messagesView.setSelectionFromTop(pos, pxOffset);
-                                    if (messageLoaderToast != null) {
-                                        messageLoaderToast.cancel();
-                                    }
-                                    conversation.messagesLoaded.set(true);
-                                }
+                    activity.quickPasswordEdit(
+                            password,
+                            value -> {
+                                activity.xmppConnectionService.providePasswordForMuc(
+                                        conversation, value);
+                                return null;
                             });
-                        }
-
-                        @Override
-                        public void informUser(final int resId) {
+                }
+            };
+    private final OnScrollListener mOnScrollListener =
+            new OnScrollListener() {
+
+                @Override
+                public void onScrollStateChanged(AbsListView view, int scrollState) {
+                    if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) {
+                        fireReadEvent();
+                    }
+                }
 
-                            runOnUiThread(() -> {
-                                if (messageLoaderToast != null) {
-                                    messageLoaderToast.cancel();
-                                }
-                                if (ConversationFragment.this.conversation != conversation) {
-                                    return;
-                                }
-                                messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
-                                messageLoaderToast.show();
-                            });
+                @Override
+                public void onScroll(
+                        final AbsListView view,
+                        int firstVisibleItem,
+                        int visibleItemCount,
+                        int totalItemCount) {
+                    toggleScrollDownButton(view);
+                    synchronized (ConversationFragment.this.messageList) {
+                        if (firstVisibleItem < 5
+                                && conversation != null
+                                && conversation.messagesLoaded.compareAndSet(true, false)
+                                && messageList.size() > 0) {
+                            long timestamp;
+                            if (messageList.get(0).getType() == Message.TYPE_STATUS
+                                    && messageList.size() >= 2) {
+                                timestamp = messageList.get(1).getTimeSent();
+                            } else {
+                                timestamp = messageList.get(0).getTimeSent();
+                            }
+                            activity.xmppConnectionService.loadMoreMessages(
+                                    conversation,
+                                    timestamp,
+                                    new XmppConnectionService.OnMoreMessagesLoaded() {
+                                        @Override
+                                        public void onMoreMessagesLoaded(
+                                                final int c, final Conversation conversation) {
+                                            if (ConversationFragment.this.conversation
+                                                    != conversation) {
+                                                conversation.messagesLoaded.set(true);
+                                                return;
+                                            }
+                                            runOnUiThread(
+                                                    () -> {
+                                                        synchronized (messageList) {
+                                                            final int oldPosition =
+                                                                    binding.messagesView
+                                                                            .getFirstVisiblePosition();
+                                                            Message message = null;
+                                                            int childPos;
+                                                            for (childPos = 0;
+                                                                    childPos + oldPosition
+                                                                            < messageList.size();
+                                                                    ++childPos) {
+                                                                message =
+                                                                        messageList.get(
+                                                                                oldPosition
+                                                                                        + childPos);
+                                                                if (message.getType()
+                                                                        != Message.TYPE_STATUS) {
+                                                                    break;
+                                                                }
+                                                            }
+                                                            final String uuid =
+                                                                    message != null
+                                                                            ? message.getUuid()
+                                                                            : null;
+                                                            View v =
+                                                                    binding.messagesView.getChildAt(
+                                                                            childPos);
+                                                            final int pxOffset =
+                                                                    (v == null) ? 0 : v.getTop();
+                                                            ConversationFragment.this.conversation
+                                                                    .populateWithMessages(
+                                                                            ConversationFragment
+                                                                                    .this
+                                                                                    .messageList);
+                                                            try {
+                                                                updateStatusMessages();
+                                                            } catch (IllegalStateException e) {
+                                                                Log.d(
+                                                                        Config.LOGTAG,
+                                                                        "caught illegal state exception while updating status messages");
+                                                            }
+                                                            messageListAdapter
+                                                                    .notifyDataSetChanged();
+                                                            int pos =
+                                                                    Math.max(
+                                                                            getIndexOf(
+                                                                                    uuid,
+                                                                                    messageList),
+                                                                            0);
+                                                            binding.messagesView
+                                                                    .setSelectionFromTop(
+                                                                            pos, pxOffset);
+                                                            if (messageLoaderToast != null) {
+                                                                messageLoaderToast.cancel();
+                                                            }
+                                                            conversation.messagesLoaded.set(true);
+                                                        }
+                                                    });
+                                        }
 
+                                        @Override
+                                        public void informUser(final int resId) {
+
+                                            runOnUiThread(
+                                                    () -> {
+                                                        if (messageLoaderToast != null) {
+                                                            messageLoaderToast.cancel();
+                                                        }
+                                                        if (ConversationFragment.this.conversation
+                                                                != conversation) {
+                                                            return;
+                                                        }
+                                                        messageLoaderToast =
+                                                                Toast.makeText(
+                                                                        view.getContext(),
+                                                                        resId,
+                                                                        Toast.LENGTH_LONG);
+                                                        messageLoaderToast.show();
+                                                    });
+                                        }
+                                    });
                         }
-                    });
-
+                    }
                 }
-            }
-        }
-    };
-    private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
-        @Override
-        public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
-            // try to get permission to read the image, if applicable
-            if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
-                try {
-                    inputContentInfo.requestPermission();
-                } catch (Exception e) {
-                    Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
-                    Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG
-                    ).show();
-                    return false;
+            };
+    private final EditMessage.OnCommitContentListener mEditorContentListener =
+            new EditMessage.OnCommitContentListener() {
+                @Override
+                public boolean onCommitContent(
+                        InputContentInfoCompat inputContentInfo,
+                        int flags,
+                        Bundle opts,
+                        String[] contentMimeTypes) {
+                    // try to get permission to read the image, if applicable
+                    if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION)
+                            != 0) {
+                        try {
+                            inputContentInfo.requestPermission();
+                        } catch (Exception e) {
+                            Log.e(
+                                    Config.LOGTAG,
+                                    "InputContentInfoCompat#requestPermission() failed.",
+                                    e);
+                            Toast.makeText(
+                                            getActivity(),
+                                            activity.getString(
+                                                    R.string.no_permission_to_access_x,
+                                                    inputContentInfo.getDescription()),
+                                            Toast.LENGTH_LONG)
+                                    .show();
+                            return false;
+                        }
+                    }
+                    if (hasPermissions(
+                            REQUEST_ADD_EDITOR_CONTENT,
+                            Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+                        attachEditorContentToConversation(inputContentInfo.getContentUri());
+                    } else {
+                        mPendingEditorContent = inputContentInfo.getContentUri();
+                    }
+                    return true;
                 }
-            }
-            if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
-                attachEditorContentToConversation(inputContentInfo.getContentUri());
-            } else {
-                mPendingEditorContent = inputContentInfo.getContentUri();
-            }
-            return true;
-        }
-    };
+            };
     private Message selectedMessage;
-    private final OnClickListener mEnableAccountListener = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final Account account = conversation == null ? null : conversation.getAccount();
-            if (account != null) {
-                account.setOption(Account.OPTION_DISABLED, false);
-                activity.xmppConnectionService.updateAccount(account);
-            }
-        }
-    };
-    private final OnClickListener mUnblockClickListener = new OnClickListener() {
-        @Override
-        public void onClick(final View v) {
-            v.post(() -> v.setVisibility(View.INVISIBLE));
-            if (conversation.isDomainBlocked()) {
-                BlockContactDialog.show(activity, conversation);
-            } else {
-                unblockConversation(conversation);
-            }
-        }
-    };
+    private final OnClickListener mEnableAccountListener =
+            new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    final Account account = conversation == null ? null : conversation.getAccount();
+                    if (account != null) {
+                        account.setOption(Account.OPTION_DISABLED, false);
+                        activity.xmppConnectionService.updateAccount(account);
+                    }
+                }
+            };
+    private final OnClickListener mUnblockClickListener =
+            new OnClickListener() {
+                @Override
+                public void onClick(final View v) {
+                    v.post(() -> v.setVisibility(View.INVISIBLE));
+                    if (conversation.isDomainBlocked()) {
+                        BlockContactDialog.show(activity, conversation);
+                    } else {
+                        unblockConversation(conversation);
+                    }
+                }
+            };
     private final OnClickListener mBlockClickListener = this::showBlockSubmenu;
-    private final OnClickListener mAddBackClickListener = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            final Contact contact = conversation == null ? null : conversation.getContact();
-            if (contact != null) {
-                activity.xmppConnectionService.createContact(contact, true);
-                activity.switchToContactDetails(contact);
-            }
-        }
-    };
+    private final OnClickListener mAddBackClickListener =
+            new OnClickListener() {
+
+                @Override
+                public void onClick(View v) {
+                    final Contact contact = conversation == null ? null : conversation.getContact();
+                    if (contact != null) {
+                        activity.xmppConnectionService.createContact(contact, true);
+                        activity.switchToContactDetails(contact);
+                    }
+                }
+            };
     private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
-    private final OnClickListener mAllowPresenceSubscription = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final Contact contact = conversation == null ? null : conversation.getContact();
-            if (contact != null) {
-                activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
-                        activity.xmppConnectionService.getPresenceGenerator()
-                                .sendPresenceUpdatesTo(contact));
-                hideSnackbar();
-            }
-        }
-    };
-    protected OnClickListener clickToDecryptListener = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
-            if (pendingIntent != null) {
-                try {
-                    getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(),
-                            REQUEST_DECRYPT_PGP,
-                            null,
-                            0,
-                            0,
-                            0);
-                } catch (SendIntentException e) {
-                    Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
-                    conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
+    private final OnClickListener mAllowPresenceSubscription =
+            new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    final Contact contact = conversation == null ? null : conversation.getContact();
+                    if (contact != null) {
+                        activity.xmppConnectionService.sendPresencePacket(
+                                contact.getAccount(),
+                                activity.xmppConnectionService
+                                        .getPresenceGenerator()
+                                        .sendPresenceUpdatesTo(contact));
+                        hideSnackbar();
+                    }
                 }
-            }
-            updateSnackBar(conversation);
-        }
-    };
+            };
+    protected OnClickListener clickToDecryptListener =
+            new OnClickListener() {
+
+                @Override
+                public void onClick(View v) {
+                    PendingIntent pendingIntent =
+                            conversation.getAccount().getPgpDecryptionService().getPendingIntent();
+                    if (pendingIntent != null) {
+                        try {
+                            getActivity()
+                                    .startIntentSenderForResult(
+                                            pendingIntent.getIntentSender(),
+                                            REQUEST_DECRYPT_PGP,
+                                            null,
+                                            0,
+                                            0,
+                                            0);
+                        } catch (SendIntentException e) {
+                            Toast.makeText(
+                                            getActivity(),
+                                            R.string.unable_to_connect_to_keychain,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                            conversation
+                                    .getAccount()
+                                    .getPgpDecryptionService()
+                                    .continueDecryption(true);
+                        }
+                    }
+                    updateSnackBar(conversation);
+                }
+            };
     private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
-    private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
-        if (actionId == EditorInfo.IME_ACTION_SEND) {
-            InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
-            if (imm != null && imm.isFullscreenMode()) {
-                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
-            }
-            sendMessage();
-            return true;
-        } else {
-            return false;
-        }
-    };
-    private final OnClickListener mScrollButtonListener = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            stopScrolling();
-            setSelection(binding.messagesView.getCount() - 1, true);
-        }
-    };
-    private final OnClickListener mSendButtonListener = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            Object tag = v.getTag();
-            if (tag instanceof SendButtonAction) {
-                SendButtonAction action = (SendButtonAction) tag;
-                switch (action) {
-                    case TAKE_PHOTO:
-                    case RECORD_VIDEO:
-                    case SEND_LOCATION:
-                    case RECORD_VOICE:
-                    case CHOOSE_PICTURE:
-                        attachFile(action.toChoice());
-                        break;
-                    case CANCEL:
-                        if (conversation != null) {
-                            if (conversation.setCorrectingMessage(null)) {
-                                binding.textinput.setText("");
-                                binding.textinput.append(conversation.getDraftMessage());
-                                conversation.setDraftMessage(null);
-                            } else if (conversation.getMode() == Conversation.MODE_MULTI) {
-                                conversation.setNextCounterpart(null);
-                                binding.textinput.setText("");
-                            } else {
-                                binding.textinput.setText("");
-                            }
-                            updateChatMsgHint();
-                            updateSendButton();
-                            updateEditablity();
+    private final OnEditorActionListener mEditorActionListener =
+            (v, actionId, event) -> {
+                if (actionId == EditorInfo.IME_ACTION_SEND) {
+                    InputMethodManager imm =
+                            (InputMethodManager)
+                                    activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+                    if (imm != null && imm.isFullscreenMode()) {
+                        imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+                    }
+                    sendMessage();
+                    return true;
+                } else {
+                    return false;
+                }
+            };
+    private final OnClickListener mScrollButtonListener =
+            new OnClickListener() {
+
+                @Override
+                public void onClick(View v) {
+                    stopScrolling();
+                    setSelection(binding.messagesView.getCount() - 1, true);
+                }
+            };
+    private final OnClickListener mSendButtonListener =
+            new OnClickListener() {
+
+                @Override
+                public void onClick(View v) {
+                    Object tag = v.getTag();
+                    if (tag instanceof SendButtonAction) {
+                        SendButtonAction action = (SendButtonAction) tag;
+                        switch (action) {
+                            case TAKE_PHOTO:
+                            case RECORD_VIDEO:
+                            case SEND_LOCATION:
+                            case RECORD_VOICE:
+                            case CHOOSE_PICTURE:
+                                attachFile(action.toChoice());
+                                break;
+                            case CANCEL:
+                                if (conversation != null) {
+                                    if (conversation.setCorrectingMessage(null)) {
+                                        binding.textinput.setText("");
+                                        binding.textinput.append(conversation.getDraftMessage());
+                                        conversation.setDraftMessage(null);
+                                    } else if (conversation.getMode() == Conversation.MODE_MULTI) {
+                                        conversation.setNextCounterpart(null);
+                                        binding.textinput.setText("");
+                                    } else {
+                                        binding.textinput.setText("");
+                                    }
+                                    updateChatMsgHint();
+                                    updateSendButton();
+                                    updateEditablity();
+                                }
+                                break;
+                            default:
+                                sendMessage();
                         }
-                        break;
-                    default:
+                    } else {
                         sendMessage();
+                    }
                 }
-            } else {
-                sendMessage();
-            }
-        }
-    };
+            };
     private int completionIndex = 0;
     private int lastCompletionLength = 0;
     private String incomplete;
@@ -531,7 +632,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             return (ConversationFragment) fragment;
         } else {
             fragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
-            return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null;
+            return fragment instanceof ConversationFragment
+                    ? (ConversationFragment) fragment
+                    : null;
         }
     }
 
@@ -593,7 +696,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                     }
                     next = next.next();
                 }
-
             }
         }
         return -1;
@@ -601,7 +703,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 
     private ScrollState getScrollPosition() {
         final ListView listView = this.binding == null ? null : this.binding.messagesView;
-        if (listView == null || listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) {
+        if (listView == null
+                || listView.getCount() == 0
+                || listView.getLastVisiblePosition() == listView.getCount() - 1) {
             return null;
         } else {
             final int pos = listView.getFirstVisiblePosition();
@@ -619,10 +723,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 
             this.lastMessageUuid = lastMessageUuid;
             if (lastMessageUuid != null) {
-                binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
+                binding.unreadCountCustomView.setUnreadCount(
+                        conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
             }
-            //TODO maybe this needs a 'post'
-            this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset);
+            // TODO maybe this needs a 'post'
+            this.binding.messagesView.setSelectionFromTop(
+                    scrollPosition.position, scrollPosition.offset);
             toggleScrollDownButton();
         }
     }
@@ -631,61 +737,65 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         if (conversation == null) {
             return;
         }
-        activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
-
-            @Override
-            public void success(Message message) {
-
-            }
+        activity.xmppConnectionService.attachLocationToConversation(
+                conversation,
+                uri,
+                new UiCallback<Message>() {
 
-            @Override
-            public void error(int errorCode, Message object) {
-                //TODO show possible pgp error
-            }
+                    @Override
+                    public void success(Message message) {}
 
-            @Override
-            public void userInputRequired(PendingIntent pi, Message object) {
+                    @Override
+                    public void error(int errorCode, Message object) {
+                        // TODO show possible pgp error
+                    }
 
-            }
-        });
+                    @Override
+                    public void userInputRequired(PendingIntent pi, Message object) {}
+                });
     }
 
     private void attachFileToConversation(Conversation conversation, Uri uri, String type) {
         if (conversation == null) {
             return;
         }
-        final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
+        final Toast prepareFileToast =
+                Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
         prepareFileToast.show();
         activity.delegateUriPermissionsToService(uri);
-        activity.xmppConnectionService.attachFileToConversation(conversation, uri, type, new UiInformableCallback<Message>() {
-            @Override
-            public void inform(final String text) {
-                hidePrepareFileToast(prepareFileToast);
-                runOnUiThread(() -> activity.replaceToast(text));
-            }
-
-            @Override
-            public void success(Message message) {
-                runOnUiThread(() -> activity.hideToast());
-                hidePrepareFileToast(prepareFileToast);
-            }
+        activity.xmppConnectionService.attachFileToConversation(
+                conversation,
+                uri,
+                type,
+                new UiInformableCallback<Message>() {
+                    @Override
+                    public void inform(final String text) {
+                        hidePrepareFileToast(prepareFileToast);
+                        runOnUiThread(() -> activity.replaceToast(text));
+                    }
 
-            @Override
-            public void error(final int errorCode, Message message) {
-                hidePrepareFileToast(prepareFileToast);
-                runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
+                    @Override
+                    public void success(Message message) {
+                        runOnUiThread(() -> activity.hideToast());
+                        hidePrepareFileToast(prepareFileToast);
+                    }
 
-            }
+                    @Override
+                    public void error(final int errorCode, Message message) {
+                        hidePrepareFileToast(prepareFileToast);
+                        runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
+                    }
 
-            @Override
-            public void userInputRequired(PendingIntent pi, Message message) {
-                hidePrepareFileToast(prepareFileToast);
-            }
-        });
+                    @Override
+                    public void userInputRequired(PendingIntent pi, Message message) {
+                        hidePrepareFileToast(prepareFileToast);
+                    }
+                });
     }
 
     public void attachEditorContentToConversation(Uri uri) {
-        mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE));
+        mediaPreviewAdapter.addMediaPreviews(
+                Attachment.of(getActivity(), uri, Attachment.Type.FILE));
         toggleInputMethod();
     }
 
@@ -693,10 +803,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         if (conversation == null) {
             return;
         }
-        final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
+        final Toast prepareFileToast =
+                Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
         prepareFileToast.show();
         activity.delegateUriPermissionsToService(uri);
-        activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
+        activity.xmppConnectionService.attachImageToConversation(
+                conversation,
+                uri,
+                type,
                 new UiCallback<Message>() {
 
                     @Override
@@ -762,19 +876,31 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
     }
 
     private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
-        return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode);
+        return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL
+                && trustKeysIfNeeded(requestCode);
     }
 
     protected boolean trustKeysIfNeeded(int requestCode) {
         AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
         final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
         boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets);
-        boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
-        boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
+        boolean hasUndecidedOwn =
+                !axolotlService
+                        .getKeysWithTrust(FingerprintStatus.createActiveUndecided())
+                        .isEmpty();
+        boolean hasUndecidedContacts =
+                !axolotlService
+                        .getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets)
+                        .isEmpty();
         boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty();
         boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
         boolean downloadInProgress = axolotlService.hasPendingKeyFetches(targets);
-        if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted || downloadInProgress) {
+        if (hasUndecidedOwn
+                || hasUndecidedContacts
+                || hasPendingKeys
+                || hasNoTrustedKeys
+                || hasUnaccepted
+                || downloadInProgress) {
             axolotlService.createSessionsIfNeeded(conversation);
             Intent intent = new Intent(getActivity(), TrustKeysActivity.class);
             String[] contacts = new String[targets.size()];
@@ -782,7 +908,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                 contacts[i] = targets.get(i).toString();
             }
             intent.putExtra("contacts", contacts);
-            intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString());
+            intent.putExtra(
+                    EXTRA_ACCOUNT,
+                    conversation.getAccount().getJid().asBareJid().toEscapedString());
             intent.putExtra("conversation", conversation.getUuid());
             startActivityForResult(intent, requestCode);
             return true;
@@ -799,9 +927,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         } else if (multi && conversation.getNextCounterpart() != null) {
             this.binding.textinput.setHint(R.string.send_unencrypted_message);
             this.binding.textInputHint.setVisibility(View.VISIBLE);
-            this.binding.textInputHint.setText(getString(
-                    R.string.send_private_message_to,
-                    conversation.getNextCounterpart().getResource()));
+            this.binding.textInputHint.setText(
+                    getString(
+                            R.string.send_private_message_to,
+                            conversation.getNextCounterpart().getResource()));
         } else if (multi && !conversation.getMucOptions().participating()) {
             this.binding.textInputHint.setVisibility(View.GONE);
             this.binding.textinput.setHint(R.string.you_are_not_participating);
@@ -839,14 +968,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                 triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
                 break;
             case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
-                final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
+                final List<Attachment> imageUris =
+                        Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
                 mediaPreviewAdapter.addMediaPreviews(imageUris);
                 toggleInputMethod();
                 break;
             case ATTACHMENT_CHOICE_TAKE_PHOTO:
                 final Uri takePhotoUri = pendingTakePhotoUri.pop();
                 if (takePhotoUri != null) {
-                    mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE));
+                    mediaPreviewAdapter.addMediaPreviews(
+                            Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE));
                     toggleInputMethod();
                 } else {
                     Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach");
@@ -855,8 +986,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             case ATTACHMENT_CHOICE_CHOOSE_FILE:
             case ATTACHMENT_CHOICE_RECORD_VIDEO:
             case ATTACHMENT_CHOICE_RECORD_VOICE:
-                final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE;
-                final List<Attachment> fileUris = Attachment.extractAttachments(getActivity(), data, type);
+                final Attachment.Type type =
+                        requestCode == ATTACHMENT_CHOICE_RECORD_VOICE
+                                ? Attachment.Type.RECORDING
+                                : Attachment.Type.FILE;
+                final List<Attachment> fileUris =
+                        Attachment.extractAttachments(getActivity(), data, type);
                 mediaPreviewAdapter.addMediaPreviews(fileUris);
                 toggleInputMethod();
                 break;
@@ -870,14 +1005,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                 } else {
                     geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
                 }
-                mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
+                mediaPreviewAdapter.addMediaPreviews(
+                        Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
                 toggleInputMethod();
                 break;
             case REQUEST_INVITE_TO_CONVERSATION:
                 XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data);
                 if (invite != null) {
                     if (invite.execute(activity)) {
-                        activity.mToast = Toast.makeText(activity, R.string.creating_conference, Toast.LENGTH_LONG);
+                        activity.mToast =
+                                Toast.makeText(
+                                        activity, R.string.creating_conference, Toast.LENGTH_LONG);
                         activity.mToast.show();
                     }
                 }
@@ -887,40 +1025,51 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 
     private void commitAttachments() {
         final List<Attachment> attachments = mediaPreviewAdapter.getAttachments();
-        if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if (anyNeedsExternalStoragePermission(attachments)
+                && !hasPermissions(
+                        REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
             return;
         }
         if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) {
             return;
         }
-        final PresenceSelector.OnPresenceSelected callback = () -> {
-            for (Iterator<Attachment> i = attachments.iterator(); i.hasNext(); i.remove()) {
-                final Attachment attachment = i.next();
-                if (attachment.getType() == Attachment.Type.LOCATION) {
-                    attachLocationToConversation(conversation, attachment.getUri());
-                } else if (attachment.getType() == Attachment.Type.IMAGE) {
-                    Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
-                    attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
-                } else {
-                    Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
-                    attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
-                }
-            }
-            mediaPreviewAdapter.notifyDataSetChanged();
-            toggleInputMethod();
-        };
+        final PresenceSelector.OnPresenceSelected callback =
+                () -> {
+                    for (Iterator<Attachment> i = attachments.iterator(); i.hasNext(); i.remove()) {
+                        final Attachment attachment = i.next();
+                        if (attachment.getType() == Attachment.Type.LOCATION) {
+                            attachLocationToConversation(conversation, attachment.getUri());
+                        } else if (attachment.getType() == Attachment.Type.IMAGE) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
+                            attachImageToConversation(
+                                    conversation, attachment.getUri(), attachment.getMime());
+                        } else {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
+                            attachFileToConversation(
+                                    conversation, attachment.getUri(), attachment.getMime());
+                        }
+                    }
+                    mediaPreviewAdapter.notifyDataSetChanged();
+                    toggleInputMethod();
+                };
         if (conversation == null
                 || conversation.getMode() == Conversation.MODE_MULTI
                 || Attachment.canBeSendInband(attachments)
-                || (conversation.getAccount().httpUploadAvailable() && FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation)))) {
+                || (conversation.getAccount().httpUploadAvailable()
+                        && FileBackend.allFilesUnderSize(
+                                getActivity(), attachments, getMaxHttpUploadSize(conversation)))) {
             callback.onPresenceSelected();
         } else {
             activity.selectPresence(conversation, callback);
         }
     }
 
-
-    private static boolean anyNeedsExternalStoragePermission(final Collection<Attachment> attachments) {
+    private static boolean anyNeedsExternalStoragePermission(
+            final Collection<Attachment> attachments) {
         for (final Attachment attachment : attachments) {
             if (attachment.getType() != Attachment.Type.LOCATION) {
                 return true;

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -272,14 +272,16 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void requestPermissionsAndAcceptCall() {
-        final List<String> permissions;
+        final ImmutableList.Builder<String> permissions = ImmutableList.builder();
         if (getMedia().contains(Media.VIDEO)) {
-            permissions =
-                    ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
+            permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
         } else {
-            permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO);
+            permissions.add(Manifest.permission.RECORD_AUDIO);
         }
-        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
+        }
+        if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) {
             putScreenInCallMode();
             checkRecorderAndAcceptCall();
         }
@@ -491,13 +493,16 @@ public class RtpSessionActivity extends XmppActivity
     public void onRequestPermissionsResult(
             int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-        if (PermissionUtils.allGranted(grantResults)) {
+        final PermissionUtils.PermissionResult permissionResult =
+                PermissionUtils.removeBluetoothConnect(permissions, grantResults);
+        if (PermissionUtils.allGranted(permissionResult.grantResults)) {
             if (requestCode == REQUEST_ACCEPT_CALL) {
                 checkRecorderAndAcceptCall();
             }
         } else {
             @StringRes int res;
-            final String firstDenied = getFirstDenied(grantResults, permissions);
+            final String firstDenied =
+                    getFirstDenied(permissionResult.grantResults, permissionResult.permissions);
             if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
                 res = R.string.no_microphone_permission;
             } else if (Manifest.permission.CAMERA.equals(firstDenied)) {

src/main/java/eu/siacs/conversations/utils/PermissionUtils.java 🔗

@@ -8,7 +8,9 @@ import android.os.Build;
 import androidx.core.app.ActivityCompat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class PermissionUtils {
@@ -40,11 +42,41 @@ public class PermissionUtils {
         return null;
     }
 
-    public static boolean hasPermission(final Activity activity, final List<String> permissions, final int requestCode) {
+    public static class PermissionResult {
+        public final String[] permissions;
+        public final int[] grantResults;
+
+        public PermissionResult(String[] permissions, int[] grantResults) {
+            this.permissions = permissions;
+            this.grantResults = grantResults;
+        }
+    }
+
+    public static PermissionResult removeBluetoothConnect(
+            final String[] inPermissions, final int[] inGrantResults) {
+        final List<String> outPermissions = new ArrayList<>();
+        final List<Integer> outGrantResults = new ArrayList<>();
+        for (int i = 0; i < Math.min(inPermissions.length, inGrantResults.length); ++i) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                if (inPermissions[i].equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+                    continue;
+                }
+            }
+            outPermissions.add(inPermissions[i]);
+            outGrantResults.add(inGrantResults[i]);
+        }
+
+        return new PermissionResult(
+                outPermissions.toArray(new String[0]), Ints.toArray(outGrantResults));
+    }
+
+    public static boolean hasPermission(
+            final Activity activity, final List<String> permissions, final int requestCode) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             final ImmutableList.Builder<String> missingPermissions = new ImmutableList.Builder<>();
             for (final String permission : permissions) {
-                if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+                if (ActivityCompat.checkSelfPermission(activity, permission)
+                        != PackageManager.PERMISSION_GRANTED) {
                     missingPermissions.add(permission);
                 }
             }
@@ -52,7 +84,8 @@ public class PermissionUtils {
             if (missing.size() == 0) {
                 return true;
             }
-            ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), requestCode);
+            ActivityCompat.requestPermissions(
+                    activity, missing.toArray(new String[0]), requestCode);
             return false;
         } else {
             return true;