From 3460e7e405d4acc24ade47161987cb0fb7b2ad34 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 11 Mar 2026 17:10:29 +0100 Subject: [PATCH] apply code formatter, part 4 --- src/main/java/com/b44t/messenger/DcChat.java | 159 +- .../java/com/b44t/messenger/DcContact.java | 121 +- .../java/com/b44t/messenger/DcContext.java | 597 +++-- src/main/java/com/b44t/messenger/DcMsg.java | 514 +++-- .../securesms/ConversationAdapter.java | 261 ++- .../securesms/ConversationFragment.java | 2009 +++++++++-------- .../securesms/components/CallItemView.java | 42 +- .../securesms/notifications/InChatSounds.java | 68 +- .../notifications/RemoteReplyReceiver.java | 37 +- .../service/FetchForegroundService.java | 35 +- .../service/GenericForegroundService.java | 172 +- .../service/NotificationController.java | 68 +- src/main/res/layout/call_item_view.xml | 65 +- .../res/layout/conversation_item_call.xml | 14 +- .../res/layout/conversation_item_received.xml | 482 ++-- .../res/layout/conversation_item_sent.xml | 37 +- .../res/layout/conversation_item_update.xml | 8 +- src/main/res/values-v29/arrays.xml | 25 +- 18 files changed, 2530 insertions(+), 2184 deletions(-) diff --git a/src/main/java/com/b44t/messenger/DcChat.java b/src/main/java/com/b44t/messenger/DcChat.java index 7fa460f54..8f32f9725 100644 --- a/src/main/java/com/b44t/messenger/DcChat.java +++ b/src/main/java/com/b44t/messenger/DcChat.java @@ -4,85 +4,106 @@ import org.thoughtcrime.securesms.util.Util; public class DcChat { - public static final int DC_CHAT_TYPE_UNDEFINED = 0; - public static final int DC_CHAT_TYPE_SINGLE = 100; - public static final int DC_CHAT_TYPE_GROUP = 120; - public static final int DC_CHAT_TYPE_MAILINGLIST = 140; - public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160; - public static final int DC_CHAT_TYPE_IN_BROADCAST = 165; + public static final int DC_CHAT_TYPE_UNDEFINED = 0; + public static final int DC_CHAT_TYPE_SINGLE = 100; + public static final int DC_CHAT_TYPE_GROUP = 120; + public static final int DC_CHAT_TYPE_MAILINGLIST = 140; + public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160; + public static final int DC_CHAT_TYPE_IN_BROADCAST = 165; - public static final int DC_CHAT_NO_CHAT = 0; - public final static int DC_CHAT_ID_ARCHIVED_LINK = 6; - public final static int DC_CHAT_ID_ALLDONE_HINT = 7; - public final static int DC_CHAT_ID_LAST_SPECIAL = 9; + public static final int DC_CHAT_NO_CHAT = 0; + public static final int DC_CHAT_ID_ARCHIVED_LINK = 6; + public static final int DC_CHAT_ID_ALLDONE_HINT = 7; + public static final int DC_CHAT_ID_LAST_SPECIAL = 9; - public final static int DC_CHAT_VISIBILITY_NORMAL = 0; - public final static int DC_CHAT_VISIBILITY_ARCHIVED = 1; - public final static int DC_CHAT_VISIBILITY_PINNED = 2; + public static final int DC_CHAT_VISIBILITY_NORMAL = 0; + public static final int DC_CHAT_VISIBILITY_ARCHIVED = 1; + public static final int DC_CHAT_VISIBILITY_PINNED = 2; - private int accountId; + private int accountId; - public DcChat(int accountId, long chatCPtr) { - this.accountId = accountId; - this.chatCPtr = chatCPtr; + public DcChat(int accountId, long chatCPtr) { + this.accountId = accountId; + this.chatCPtr = chatCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefChatCPtr(); + chatCPtr = 0; + } + + public int getAccountId() { + return accountId; + } + + public native int getId(); + + public native int getType(); + + public native int getVisibility(); + + public native String getName(); + + public native String getMailinglistAddr(); + + public native String getProfileImage(); + + public native int getColor(); + + public native boolean isEncrypted(); + + public native boolean isUnpromoted(); + + public native boolean isSelfTalk(); + + public native boolean isDeviceTalk(); + + public native boolean canSend(); + + public native boolean isSendingLocations(); + + public native boolean isMuted(); + + public native boolean isContactRequest(); + + // aliases and higher-level tools + + public boolean isMultiUser() { + int type = getType(); + return type != DC_CHAT_TYPE_SINGLE; + } + + public boolean shallLeaveBeforeDelete(DcContext dcContext) { + if (isInBroadcast()) { + final int[] members = dcContext.getChatContacts(getId()); + return Util.contains(members, DcContact.DC_CONTACT_ID_SELF); + } else if (isMultiUser() && isEncrypted() && canSend() && !isOutBroadcast()) { + return true; } + return false; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefChatCPtr(); - chatCPtr = 0; - } + public boolean isMailingList() { + return getType() == DC_CHAT_TYPE_MAILINGLIST; + } - public int getAccountId () { return accountId; } - public native int getId (); - public native int getType (); - public native int getVisibility (); - public native String getName (); - public native String getMailinglistAddr(); - public native String getProfileImage (); - public native int getColor (); - public native boolean isEncrypted (); - public native boolean isUnpromoted (); - public native boolean isSelfTalk (); - public native boolean isDeviceTalk (); - public native boolean canSend (); - public native boolean isSendingLocations(); - public native boolean isMuted (); - public native boolean isContactRequest (); + public boolean isInBroadcast() { + return getType() == DC_CHAT_TYPE_IN_BROADCAST; + } + public boolean isOutBroadcast() { + return getType() == DC_CHAT_TYPE_OUT_BROADCAST; + } - // aliases and higher-level tools + // working with raw c-data - public boolean isMultiUser() { - int type = getType(); - return type != DC_CHAT_TYPE_SINGLE; - } + private long chatCPtr; // CAVE: the name is referenced in the JNI - public boolean shallLeaveBeforeDelete(DcContext dcContext) { - if (isInBroadcast()) { - final int[] members = dcContext.getChatContacts(getId()); - return Util.contains(members, DcContact.DC_CONTACT_ID_SELF); - } else if (isMultiUser() && isEncrypted() && canSend() && !isOutBroadcast()) { - return true; - } - return false; - } - - public boolean isMailingList() { - return getType() == DC_CHAT_TYPE_MAILINGLIST; - } - - public boolean isInBroadcast() { - return getType() == DC_CHAT_TYPE_IN_BROADCAST; - } - public boolean isOutBroadcast() { - return getType() == DC_CHAT_TYPE_OUT_BROADCAST; - } - - // working with raw c-data - - private long chatCPtr; // CAVE: the name is referenced in the JNI - private native void unrefChatCPtr(); - public long getChatCPtr () { return chatCPtr; } + private native void unrefChatCPtr(); + public long getChatCPtr() { + return chatCPtr; + } } diff --git a/src/main/java/com/b44t/messenger/DcContact.java b/src/main/java/com/b44t/messenger/DcContact.java index c1db787df..b0b358dec 100644 --- a/src/main/java/com/b44t/messenger/DcContact.java +++ b/src/main/java/com/b44t/messenger/DcContact.java @@ -2,67 +2,82 @@ package com.b44t.messenger; public class DcContact { - public final static int DC_CONTACT_ID_SELF = 1; - public final static int DC_CONTACT_ID_INFO = 2; - public final static int DC_CONTACT_ID_DEVICE = 5; - public final static int DC_CONTACT_ID_LAST_SPECIAL = 9; - public final static int DC_CONTACT_ID_NEW_CLASSIC_CONTACT= -1; // used by the UI, not valid to the core - public final static int DC_CONTACT_ID_NEW_GROUP = -2; // - " - - public final static int DC_CONTACT_ID_ADD_MEMBER = -3; // - " - - public final static int DC_CONTACT_ID_QR_INVITE = -4; // - " - - public final static int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " - - public final static int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " - - public final static int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " - + public static final int DC_CONTACT_ID_SELF = 1; + public static final int DC_CONTACT_ID_INFO = 2; + public static final int DC_CONTACT_ID_DEVICE = 5; + public static final int DC_CONTACT_ID_LAST_SPECIAL = 9; + public static final int DC_CONTACT_ID_NEW_CLASSIC_CONTACT = + -1; // used by the UI, not valid to the core + public static final int DC_CONTACT_ID_NEW_GROUP = -2; // - " - + public static final int DC_CONTACT_ID_ADD_MEMBER = -3; // - " - + public static final int DC_CONTACT_ID_QR_INVITE = -4; // - " - + public static final int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " - + public static final int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " - + public static final int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " - - public DcContact(long contactCPtr) { - this.contactCPtr = contactCPtr; + public DcContact(long contactCPtr) { + this.contactCPtr = contactCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefContactCPtr(); + contactCPtr = 0; + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof DcContact)) { + return false; } - @Override - protected void finalize() throws Throwable { - super.finalize(); - unrefContactCPtr(); - contactCPtr = 0; - } + DcContact that = (DcContact) other; + return this.getId() == that.getId(); + } + @Override + public int hashCode() { + return this.getId(); + } - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof DcContact)) { - return false; - } + @Override + public String toString() { + return getAddr(); + } - DcContact that = (DcContact) other; - return this.getId()==that.getId(); - } + public native int getId(); - @Override - public int hashCode() { - return this.getId(); - } + public native String getName(); - @Override - public String toString() { - return getAddr(); - } + public native String getAuthName(); - public native int getId (); - public native String getName (); - public native String getAuthName (); - public native String getDisplayName (); - public native String getAddr (); - public native String getProfileImage(); - public native int getColor (); - public native String getStatus (); - public native long getLastSeen (); - public native boolean wasSeenRecently(); - public native boolean isBlocked (); - public native boolean isVerified (); - public native boolean isKeyContact (); - public native int getVerifierId (); - public native boolean isBot (); + public native String getDisplayName(); - // working with raw c-data - private long contactCPtr; // CAVE: the name is referenced in the JNI - private native void unrefContactCPtr(); + public native String getAddr(); + + public native String getProfileImage(); + + public native int getColor(); + + public native String getStatus(); + + public native long getLastSeen(); + + public native boolean wasSeenRecently(); + + public native boolean isBlocked(); + + public native boolean isVerified(); + + public native boolean isKeyContact(); + + public native int getVerifierId(); + + public native boolean isBot(); + + // working with raw c-data + private long contactCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefContactCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcContext.java b/src/main/java/com/b44t/messenger/DcContext.java index ef993ccee..2dc7726e8 100644 --- a/src/main/java/com/b44t/messenger/DcContext.java +++ b/src/main/java/com/b44t/messenger/DcContext.java @@ -2,273 +2,396 @@ package com.b44t.messenger; public class DcContext { - public final static int DC_EVENT_INFO = 100; - public final static int DC_EVENT_WARNING = 300; - public final static int DC_EVENT_ERROR = 400; - public final static int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410; - public final static int DC_EVENT_MSGS_CHANGED = 2000; - public final static int DC_EVENT_REACTIONS_CHANGED = 2001; - public final static int DC_EVENT_INCOMING_REACTION = 2002; - public final static int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003; - public final static int DC_EVENT_INCOMING_MSG = 2005; - public final static int DC_EVENT_MSGS_NOTICED = 2008; - public final static int DC_EVENT_MSG_DELIVERED = 2010; - public final static int DC_EVENT_MSG_FAILED = 2012; - public final static int DC_EVENT_MSG_READ = 2015; - public final static int DC_EVENT_MSG_DELETED = 2016; - public final static int DC_EVENT_CHAT_MODIFIED = 2020; - public final static int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021; - public final static int DC_EVENT_CHAT_DELETED = 2023; - public final static int DC_EVENT_CONTACTS_CHANGED = 2030; - public final static int DC_EVENT_LOCATION_CHANGED = 2035; - public final static int DC_EVENT_CONFIGURE_PROGRESS = 2041; - public final static int DC_EVENT_IMEX_PROGRESS = 2051; - public final static int DC_EVENT_IMEX_FILE_WRITTEN = 2052; - public final static int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060; - public final static int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061; - public final static int DC_EVENT_CONNECTIVITY_CHANGED = 2100; - public final static int DC_EVENT_SELFAVATAR_CHANGED = 2110; - public final static int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120; - public final static int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121; - public final static int DC_EVENT_WEBXDC_REALTIME_DATA = 2150; - public final static int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200; - public final static int DC_EVENT_INCOMING_CALL = 2550; - public final static int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560; - public final static int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570; - public final static int DC_EVENT_CALL_ENDED = 2580; - public final static int DC_EVENT_TRANSPORTS_MODIFIED = 2600; + public static final int DC_EVENT_INFO = 100; + public static final int DC_EVENT_WARNING = 300; + public static final int DC_EVENT_ERROR = 400; + public static final int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410; + public static final int DC_EVENT_MSGS_CHANGED = 2000; + public static final int DC_EVENT_REACTIONS_CHANGED = 2001; + public static final int DC_EVENT_INCOMING_REACTION = 2002; + public static final int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003; + public static final int DC_EVENT_INCOMING_MSG = 2005; + public static final int DC_EVENT_MSGS_NOTICED = 2008; + public static final int DC_EVENT_MSG_DELIVERED = 2010; + public static final int DC_EVENT_MSG_FAILED = 2012; + public static final int DC_EVENT_MSG_READ = 2015; + public static final int DC_EVENT_MSG_DELETED = 2016; + public static final int DC_EVENT_CHAT_MODIFIED = 2020; + public static final int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021; + public static final int DC_EVENT_CHAT_DELETED = 2023; + public static final int DC_EVENT_CONTACTS_CHANGED = 2030; + public static final int DC_EVENT_LOCATION_CHANGED = 2035; + public static final int DC_EVENT_CONFIGURE_PROGRESS = 2041; + public static final int DC_EVENT_IMEX_PROGRESS = 2051; + public static final int DC_EVENT_IMEX_FILE_WRITTEN = 2052; + public static final int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060; + public static final int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061; + public static final int DC_EVENT_CONNECTIVITY_CHANGED = 2100; + public static final int DC_EVENT_SELFAVATAR_CHANGED = 2110; + public static final int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120; + public static final int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121; + public static final int DC_EVENT_WEBXDC_REALTIME_DATA = 2150; + public static final int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200; + public static final int DC_EVENT_INCOMING_CALL = 2550; + public static final int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560; + public static final int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570; + public static final int DC_EVENT_CALL_ENDED = 2580; + public static final int DC_EVENT_TRANSPORTS_MODIFIED = 2600; - public final static int DC_IMEX_EXPORT_SELF_KEYS = 1; - public final static int DC_IMEX_IMPORT_SELF_KEYS = 2; - public final static int DC_IMEX_EXPORT_BACKUP = 11; - public final static int DC_IMEX_IMPORT_BACKUP = 12; + public static final int DC_IMEX_EXPORT_SELF_KEYS = 1; + public static final int DC_IMEX_IMPORT_SELF_KEYS = 2; + public static final int DC_IMEX_EXPORT_BACKUP = 11; + public static final int DC_IMEX_IMPORT_BACKUP = 12; - public final static int DC_GCL_VERIFIED_ONLY = 1; - public final static int DC_GCL_ADD_SELF = 2; - public final static int DC_GCL_ADDRESS = 0x04; - public final static int DC_GCL_ARCHIVED_ONLY = 0x01; - public final static int DC_GCL_NO_SPECIALS = 0x02; - public final static int DC_GCL_ADD_ALLDONE_HINT = 0x04; - public final static int DC_GCL_FOR_FORWARDING = 0x08; + public static final int DC_GCL_VERIFIED_ONLY = 1; + public static final int DC_GCL_ADD_SELF = 2; + public static final int DC_GCL_ADDRESS = 0x04; + public static final int DC_GCL_ARCHIVED_ONLY = 0x01; + public static final int DC_GCL_NO_SPECIALS = 0x02; + public static final int DC_GCL_ADD_ALLDONE_HINT = 0x04; + public static final int DC_GCL_FOR_FORWARDING = 0x08; - public final static int DC_GCM_ADDDAYMARKER = 0x01; + public static final int DC_GCM_ADDDAYMARKER = 0x01; - public final static int DC_QR_ASK_VERIFYCONTACT = 200; - public final static int DC_QR_ASK_VERIFYGROUP = 202; - public final static int DC_QR_ASK_JOIN_BROADCAST= 204; - public final static int DC_QR_FPR_OK = 210; - public final static int DC_QR_FPR_MISMATCH = 220; - public final static int DC_QR_FPR_WITHOUT_ADDR = 230; - public final static int DC_QR_ACCOUNT = 250; - public final static int DC_QR_BACKUP2 = 252; - public final static int DC_QR_BACKUP_TOO_NEW = 255; - public final static int DC_QR_WEBRTC = 260; - public final static int DC_QR_PROXY = 271; - public final static int DC_QR_ADDR = 320; - public final static int DC_QR_TEXT = 330; - public final static int DC_QR_URL = 332; - public final static int DC_QR_ERROR = 400; - public final static int DC_QR_WITHDRAW_VERIFYCONTACT = 500; - public final static int DC_QR_WITHDRAW_VERIFYGROUP = 502; - public final static int DC_QR_WITHDRAW_JOINBROADCAST = 504; - public final static int DC_QR_REVIVE_VERIFYCONTACT = 510; - public final static int DC_QR_REVIVE_VERIFYGROUP = 512; - public final static int DC_QR_REVIVE_JOINBROADCAST = 514; - public final static int DC_QR_LOGIN = 520; + public static final int DC_QR_ASK_VERIFYCONTACT = 200; + public static final int DC_QR_ASK_VERIFYGROUP = 202; + public static final int DC_QR_ASK_JOIN_BROADCAST = 204; + public static final int DC_QR_FPR_OK = 210; + public static final int DC_QR_FPR_MISMATCH = 220; + public static final int DC_QR_FPR_WITHOUT_ADDR = 230; + public static final int DC_QR_ACCOUNT = 250; + public static final int DC_QR_BACKUP2 = 252; + public static final int DC_QR_BACKUP_TOO_NEW = 255; + public static final int DC_QR_WEBRTC = 260; + public static final int DC_QR_PROXY = 271; + public static final int DC_QR_ADDR = 320; + public static final int DC_QR_TEXT = 330; + public static final int DC_QR_URL = 332; + public static final int DC_QR_ERROR = 400; + public static final int DC_QR_WITHDRAW_VERIFYCONTACT = 500; + public static final int DC_QR_WITHDRAW_VERIFYGROUP = 502; + public static final int DC_QR_WITHDRAW_JOINBROADCAST = 504; + public static final int DC_QR_REVIVE_VERIFYCONTACT = 510; + public static final int DC_QR_REVIVE_VERIFYGROUP = 512; + public static final int DC_QR_REVIVE_JOINBROADCAST = 514; + public static final int DC_QR_LOGIN = 520; - public final static int DC_SOCKET_AUTO = 0; - public final static int DC_SOCKET_SSL = 1; - public final static int DC_SOCKET_STARTTLS = 2; - public final static int DC_SOCKET_PLAIN = 3; + public static final int DC_SOCKET_AUTO = 0; + public static final int DC_SOCKET_SSL = 1; + public static final int DC_SOCKET_STARTTLS = 2; + public static final int DC_SOCKET_PLAIN = 3; - public final static int DC_SHOW_EMAILS_OFF = 0; - public final static int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1; - public final static int DC_SHOW_EMAILS_ALL = 2; + public static final int DC_SHOW_EMAILS_OFF = 0; + public static final int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1; + public static final int DC_SHOW_EMAILS_ALL = 2; - public final static int DC_MEDIA_QUALITY_BALANCED = 0; - public final static int DC_MEDIA_QUALITY_WORSE = 1; + public static final int DC_MEDIA_QUALITY_BALANCED = 0; + public static final int DC_MEDIA_QUALITY_WORSE = 1; - public final static int DC_CONNECTIVITY_NOT_CONNECTED = 1000; - public final static int DC_CONNECTIVITY_CONNECTING = 2000; - public final static int DC_CONNECTIVITY_WORKING = 3000; - public final static int DC_CONNECTIVITY_CONNECTED = 4000; + public static final int DC_CONNECTIVITY_NOT_CONNECTED = 1000; + public static final int DC_CONNECTIVITY_CONNECTING = 2000; + public static final int DC_CONNECTIVITY_WORKING = 3000; + public static final int DC_CONNECTIVITY_CONNECTED = 4000; - private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted"; + private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted"; - // when using DcAccounts, use Rpc.addAccount() instead - public DcContext(String osName, String dbfile) { - contextCPtr = createContextCPtr(osName, dbfile); + // when using DcAccounts, use Rpc.addAccount() instead + public DcContext(String osName, String dbfile) { + contextCPtr = createContextCPtr(osName, dbfile); + } + + public DcContext(long contextCPtr) { + this.contextCPtr = contextCPtr; + } + + public boolean isOk() { + return contextCPtr != 0; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (contextCPtr != 0) { + unrefContextCPtr(); + contextCPtr = 0; } + } - public DcContext(long contextCPtr) { - this.contextCPtr = contextCPtr; + public native int getAccountId(); + + // when using DcAccounts, use DcAccounts.getEventEmitter() instead + public DcEventEmitter getEventEmitter() { + return new DcEventEmitter(getEventEmitterCPtr()); + } + + public native void setStockTranslation(int stockId, String translation); + + public native String getBlobdir(); + + public native String getLastError(); + + public native void stopOngoingProcess(); + + public native int isConfigured(); + + public native boolean open(String passphrase); + + public native boolean isOpen(); + + // when using DcAccounts, use DcAccounts.startIo() instead + public native void startIo(); + + // when using DcAccounts, use DcAccounts.stopIo() instead + public native void stopIo(); + + // when using DcAccounts, use DcAccounts.maybeNetwork() instead + public native void maybeNetwork(); + + public native void setConfig(String key, String value); + + public void setConfigInt(String key, int value) { + setConfig(key, Integer.toString(value)); + } + + public native boolean setConfigFromQr(String qr); + + public native String getConfig(String key); + + public int getConfigInt(String key) { + try { + return Integer.parseInt(getConfig(key)); + } catch (Exception e) { } + return 0; + } - public boolean isOk() { - return contextCPtr != 0; + public native String getInfo(); + + public native int getConnectivity(); + + public native String getConnectivityHtml(); + + public native String initiateKeyTransfer(); + + public native void imex(int what, String dir); + + public native String imexHasBackup(String dir); + + public DcBackupProvider newBackupProvider() { + return new DcBackupProvider(newBackupProviderCPtr()); + } + + public native boolean receiveBackup(String qr); + + public native boolean mayBeValidAddr(String addr); + + public native int lookupContactIdByAddr(String addr); + + public native int[] getContacts(int flags, String query); + + public native int[] getBlockedContacts(); + + public DcContact getContact(int contact_id) { + return new DcContact(getContactCPtr(contact_id)); + } + + public native int createContact(String name, String addr); + + public native void blockContact(int id, int block); + + public native String getContactEncrInfo(int contact_id); + + public native boolean deleteContact(int id); + + public native int addAddressBook(String adrbook); + + public DcChatlist getChatlist(int listflags, String query, int queryId) { + return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId)); + } + + public DcChat getChat(int chat_id) { + return new DcChat(getAccountId(), getChatCPtr(chat_id)); + } + + public native String getChatEncrInfo(int chat_id); + + public native void markseenMsgs(int msg_ids[]); + + public native void marknoticedChat(int chat_id); + + public native void setChatVisibility(int chat_id, int visibility); + + public native int getChatIdByContactId(int contact_id); + + public native int createChatByContactId(int contact_id); + + public native int createGroupChat(String name); + + public native int createBroadcastList(); + + public native boolean isContactInChat(int chat_id, int contact_id); + + public native int addContactToChat(int chat_id, int contact_id); + + public native int removeContactFromChat(int chat_id, int contact_id); + + public native void setDraft(int chat_id, DcMsg msg /*null=delete*/); + + public DcMsg getDraft(int chat_id) { + return new DcMsg(getDraftCPtr(chat_id)); + } + + public native int setChatName(int chat_id, String name); + + public native int setChatProfileImage(int chat_id, String name); + + public native int[] getChatMsgs(int chat_id, int flags, int marker1before); + + public native int[] searchMsgs(int chat_id, String query); + + public native int[] getFreshMsgs(); + + public native int[] getChatMedia(int chat_id, int type1, int type2, int type3); + + public native int[] getChatContacts(int chat_id); + + public native int getChatEphemeralTimer(int chat_id); + + public native boolean setChatEphemeralTimer(int chat_id, int timer); + + public native boolean setChatMuteDuration(int chat_id, long duration); + + public native void deleteChat(int chat_id); + + public native void blockChat(int chat_id); + + public native void acceptChat(int chat_id); + + public DcMsg getMsg(int msg_id) { + return new DcMsg(getMsgCPtr(msg_id)); + } + + public native void sendEditRequest(int msg_id, String text); + + public native String getMsgInfo(int id); + + public native String getMsgHtml(int msg_id); + + public native void downloadFullMsg(int msg_id); + + public native int getFreshMsgCount(int chat_id); + + public native int estimateDeletionCount(boolean from_server, long seconds); + + public native void deleteMsgs(int msg_ids[]); + + public native void sendDeleteRequest(int msg_ids[]); + + public native void forwardMsgs(int msg_ids[], int chat_id); + + public native void saveMsgs(int msg_ids[]); + + public native boolean resendMsgs(int msg_ids[]); + + public native int sendMsg(int chat_id, DcMsg msg); + + public native int sendTextMsg(int chat_id, String text); + + public native boolean sendWebxdcStatusUpdate(int msg_id, String payload); + + public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial); + + public native void setWebxdcIntegration(String file); + + public native int initWebxdcIntegration(int chat_id); + + public native int addDeviceMsg(String label, DcMsg msg); + + public native boolean wasDeviceMsgEverAdded(String label); + + public DcLot checkQr(String qr) { + return new DcLot(checkQrCPtr(qr)); + } + + public native String getSecurejoinQr(int chat_id); + + public native String getSecurejoinQrSvg(int chat_id); + + public native String createQrSvg(String payload); + + public native int joinSecurejoin(String qr); + + public native void sendLocationsToChat(int chat_id, int seconds); + + public native boolean isSendingLocationsToChat(int chat_id); + + public DcProvider getProviderFromEmailWithDns(String email) { + long cptr = getProviderFromEmailWithDnsCPtr(email); + return cptr != 0 ? new DcProvider(cptr) : null; + } + + public boolean isMentionsEnabled() { + return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1; + } + + public void setMentionsEnabled(boolean enabled) { + setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled ? 0 : 1); + } + + public String getName() { + String displayname = getConfig("displayname"); + if (displayname.isEmpty()) { + displayname = getConfig("addr"); } + return displayname; + } - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (contextCPtr != 0) { - unrefContextCPtr(); - contextCPtr = 0; - } - } + public boolean isChatmail() { + return getConfigInt("is_chatmail") == 1; + } - public native int getAccountId (); + public boolean isMuted() { + return getConfigInt("is_muted") == 1; + } - // when using DcAccounts, use DcAccounts.getEventEmitter() instead - public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); } + public void setMuted(boolean muted) { + setConfigInt("is_muted", muted ? 1 : 0); + } - public native void setStockTranslation (int stockId, String translation); - public native String getBlobdir (); - public native String getLastError (); - public native void stopOngoingProcess (); - public native int isConfigured (); - public native boolean open (String passphrase); - public native boolean isOpen (); + public void restartIo() { + stopIo(); + startIo(); + } - // when using DcAccounts, use DcAccounts.startIo() instead - public native void startIo (); + /** + * @return true if at least one chat has location streaming enabled + */ + public native boolean setLocation(float latitude, float longitude, float accuracy); - // when using DcAccounts, use DcAccounts.stopIo() instead - public native void stopIo (); + // working with raw c-data + private long contextCPtr; // CAVE: the name is referenced in the JNI - // when using DcAccounts, use DcAccounts.maybeNetwork() instead - public native void maybeNetwork (); + private native long createContextCPtr(String osName, String dbfile); - public native void setConfig (String key, String value); - public void setConfigInt (String key, int value) { setConfig(key, Integer.toString(value)); } - public native boolean setConfigFromQr (String qr); - public native String getConfig (String key); - public int getConfigInt (String key) { try{return Integer.parseInt(getConfig(key));} catch(Exception e) {} return 0; } - public native String getInfo (); - public native int getConnectivity (); - public native String getConnectivityHtml (); - public native String initiateKeyTransfer (); - public native void imex (int what, String dir); - public native String imexHasBackup (String dir); - public DcBackupProvider newBackupProvider () { return new DcBackupProvider(newBackupProviderCPtr()); } - public native boolean receiveBackup (String qr); - public native boolean mayBeValidAddr (String addr); - public native int lookupContactIdByAddr(String addr); - public native int[] getContacts (int flags, String query); - public native int[] getBlockedContacts (); - public DcContact getContact (int contact_id) { return new DcContact(getContactCPtr(contact_id)); } - public native int createContact (String name, String addr); - public native void blockContact (int id, int block); - public native String getContactEncrInfo (int contact_id); - public native boolean deleteContact (int id); - public native int addAddressBook (String adrbook); - public DcChatlist getChatlist (int listflags, String query, int queryId) { return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId)); } - public DcChat getChat (int chat_id) { return new DcChat(getAccountId(), getChatCPtr(chat_id)); } - public native String getChatEncrInfo (int chat_id); - public native void markseenMsgs (int msg_ids[]); - public native void marknoticedChat (int chat_id); - public native void setChatVisibility (int chat_id, int visibility); - public native int getChatIdByContactId (int contact_id); - public native int createChatByContactId(int contact_id); - public native int createGroupChat (String name); - public native int createBroadcastList (); - public native boolean isContactInChat (int chat_id, int contact_id); - public native int addContactToChat (int chat_id, int contact_id); - public native int removeContactFromChat(int chat_id, int contact_id); - public native void setDraft (int chat_id, DcMsg msg/*null=delete*/); - public DcMsg getDraft (int chat_id) { return new DcMsg(getDraftCPtr(chat_id)); } - public native int setChatName (int chat_id, String name); - public native int setChatProfileImage (int chat_id, String name); - public native int[] getChatMsgs (int chat_id, int flags, int marker1before); - public native int[] searchMsgs (int chat_id, String query); - public native int[] getFreshMsgs (); - public native int[] getChatMedia (int chat_id, int type1, int type2, int type3); - public native int[] getChatContacts (int chat_id); - public native int getChatEphemeralTimer (int chat_id); - public native boolean setChatEphemeralTimer (int chat_id, int timer); - public native boolean setChatMuteDuration (int chat_id, long duration); - public native void deleteChat (int chat_id); - public native void blockChat (int chat_id); - public native void acceptChat (int chat_id); - public DcMsg getMsg (int msg_id) { return new DcMsg(getMsgCPtr(msg_id)); } - public native void sendEditRequest (int msg_id, String text); - public native String getMsgInfo (int id); - public native String getMsgHtml (int msg_id); - public native void downloadFullMsg (int msg_id); - public native int getFreshMsgCount (int chat_id); - public native int estimateDeletionCount(boolean from_server, long seconds); - public native void deleteMsgs (int msg_ids[]); - public native void sendDeleteRequest (int msg_ids[]); - public native void forwardMsgs (int msg_ids[], int chat_id); - public native void saveMsgs (int msg_ids[]); - public native boolean resendMsgs (int msg_ids[]); - public native int sendMsg (int chat_id, DcMsg msg); - public native int sendTextMsg (int chat_id, String text); - public native boolean sendWebxdcStatusUpdate(int msg_id, String payload); - public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial); - public native void setWebxdcIntegration (String file); - public native int initWebxdcIntegration(int chat_id); - public native int addDeviceMsg (String label, DcMsg msg); - public native boolean wasDeviceMsgEverAdded(String label); - public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); } - public native String getSecurejoinQr (int chat_id); - public native String getSecurejoinQrSvg (int chat_id); - public native String createQrSvg (String payload); - public native int joinSecurejoin (String qr); - public native void sendLocationsToChat (int chat_id, int seconds); - public native boolean isSendingLocationsToChat(int chat_id); - public DcProvider getProviderFromEmailWithDns (String email) { long cptr = getProviderFromEmailWithDnsCPtr(email); return cptr!=0 ? new DcProvider(cptr) : null; } + private native void unrefContextCPtr(); - public boolean isMentionsEnabled() { - return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1; - } + private native long getEventEmitterCPtr(); - public void setMentionsEnabled(boolean enabled) { - setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled? 0 : 1); - } + public native long createMsgCPtr(int viewtype); - public String getName() { - String displayname = getConfig("displayname"); - if (displayname.isEmpty()) { - displayname = getConfig("addr"); - } - return displayname; - } + private native long getChatlistCPtr(int listflags, String query, int queryId); - public boolean isChatmail() { - return getConfigInt("is_chatmail") == 1; - } + private native long getChatCPtr(int chat_id); - public boolean isMuted() { - return getConfigInt("is_muted") == 1; - } + private native long getMsgCPtr(int id); - public void setMuted(boolean muted) { - setConfigInt("is_muted", muted? 1 : 0); - } + private native long getDraftCPtr(int id); - public void restartIo() { - stopIo(); - startIo(); - } + private native long getContactCPtr(int id); - /** - * @return true if at least one chat has location streaming enabled - */ - public native boolean setLocation (float latitude, float longitude, float accuracy); + private native long checkQrCPtr(String qr); - // working with raw c-data - private long contextCPtr; // CAVE: the name is referenced in the JNI - private native long createContextCPtr(String osName, String dbfile); - private native void unrefContextCPtr (); - private native long getEventEmitterCPtr(); - public native long createMsgCPtr (int viewtype); - private native long getChatlistCPtr (int listflags, String query, int queryId); - private native long getChatCPtr (int chat_id); - private native long getMsgCPtr (int id); - private native long getDraftCPtr (int id); - private native long getContactCPtr (int id); - private native long checkQrCPtr (String qr); - private native long getProviderFromEmailWithDnsCPtr (String addr); - private native long newBackupProviderCPtr(); + private native long getProviderFromEmailWithDnsCPtr(String addr); + + private native long newBackupProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcMsg.java b/src/main/java/com/b44t/messenger/DcMsg.java index 17288a026..a37446526 100644 --- a/src/main/java/com/b44t/messenger/DcMsg.java +++ b/src/main/java/com/b44t/messenger/DcMsg.java @@ -1,262 +1,320 @@ package com.b44t.messenger; import android.text.TextUtils; - -import org.json.JSONObject; - import java.io.File; import java.util.Set; +import org.json.JSONObject; public class DcMsg { - public final static int DC_MSG_UNDEFINED = 0; - public final static int DC_MSG_TEXT = 10; - public final static int DC_MSG_IMAGE = 20; - public final static int DC_MSG_GIF = 21; - public final static int DC_MSG_STICKER = 23; - public final static int DC_MSG_AUDIO = 40; - public final static int DC_MSG_VOICE = 41; - public final static int DC_MSG_VIDEO = 50; - public final static int DC_MSG_FILE = 60; - public final static int DC_MSG_CALL = 71; - public final static int DC_MSG_WEBXDC = 80; - public final static int DC_MSG_VCARD = 90; + public static final int DC_MSG_UNDEFINED = 0; + public static final int DC_MSG_TEXT = 10; + public static final int DC_MSG_IMAGE = 20; + public static final int DC_MSG_GIF = 21; + public static final int DC_MSG_STICKER = 23; + public static final int DC_MSG_AUDIO = 40; + public static final int DC_MSG_VOICE = 41; + public static final int DC_MSG_VIDEO = 50; + public static final int DC_MSG_FILE = 60; + public static final int DC_MSG_CALL = 71; + public static final int DC_MSG_WEBXDC = 80; + public static final int DC_MSG_VCARD = 90; - public final static int DC_INFO_UNKNOWN = 0; - public final static int DC_INFO_GROUP_NAME_CHANGED = 2; - public final static int DC_INFO_GROUP_IMAGE_CHANGED = 3; - public final static int DC_INFO_MEMBER_ADDED_TO_GROUP = 4; - public final static int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5; - public final static int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6; - public final static int DC_INFO_SECURE_JOIN_MESSAGE = 7; - public final static int DC_INFO_LOCATIONSTREAMING_ENABLED = 8; - public final static int DC_INFO_LOCATION_ONLY = 9; - public final static int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10; - public final static int DC_INFO_PROTECTION_ENABLED = 11; - public final static int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13; - public final static int DC_INFO_WEBXDC_INFO_MESSAGE = 32; - public final static int DC_INFO_CHAT_E2EE = 50; - public final static int DC_INFO_CHAT_DESCRIPTION_CHANGED = 70; + public static final int DC_INFO_UNKNOWN = 0; + public static final int DC_INFO_GROUP_NAME_CHANGED = 2; + public static final int DC_INFO_GROUP_IMAGE_CHANGED = 3; + public static final int DC_INFO_MEMBER_ADDED_TO_GROUP = 4; + public static final int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5; + public static final int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6; + public static final int DC_INFO_SECURE_JOIN_MESSAGE = 7; + public static final int DC_INFO_LOCATIONSTREAMING_ENABLED = 8; + public static final int DC_INFO_LOCATION_ONLY = 9; + public static final int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10; + public static final int DC_INFO_PROTECTION_ENABLED = 11; + public static final int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13; + public static final int DC_INFO_WEBXDC_INFO_MESSAGE = 32; + public static final int DC_INFO_CHAT_E2EE = 50; + public static final int DC_INFO_CHAT_DESCRIPTION_CHANGED = 70; - public final static int DC_STATE_UNDEFINED = 0; - public final static int DC_STATE_IN_FRESH = 10; - public final static int DC_STATE_IN_NOTICED = 13; - public final static int DC_STATE_IN_SEEN = 16; - public final static int DC_STATE_OUT_PREPARING = 18; - public final static int DC_STATE_OUT_DRAFT = 19; - public final static int DC_STATE_OUT_PENDING = 20; - public final static int DC_STATE_OUT_FAILED = 24; - public final static int DC_STATE_OUT_DELIVERED = 26; - public final static int DC_STATE_OUT_MDN_RCVD = 28; + public static final int DC_STATE_UNDEFINED = 0; + public static final int DC_STATE_IN_FRESH = 10; + public static final int DC_STATE_IN_NOTICED = 13; + public static final int DC_STATE_IN_SEEN = 16; + public static final int DC_STATE_OUT_PREPARING = 18; + public static final int DC_STATE_OUT_DRAFT = 19; + public static final int DC_STATE_OUT_PENDING = 20; + public static final int DC_STATE_OUT_FAILED = 24; + public static final int DC_STATE_OUT_DELIVERED = 26; + public static final int DC_STATE_OUT_MDN_RCVD = 28; - public final static int DC_DOWNLOAD_DONE = 0; - public final static int DC_DOWNLOAD_AVAILABLE = 10; - public final static int DC_DOWNLOAD_FAILURE = 20; - public final static int DC_DOWNLOAD_UNDECIPHERABLE = 30; - public final static int DC_DOWNLOAD_IN_PROGRESS = 1000; + public static final int DC_DOWNLOAD_DONE = 0; + public static final int DC_DOWNLOAD_AVAILABLE = 10; + public static final int DC_DOWNLOAD_FAILURE = 20; + public static final int DC_DOWNLOAD_UNDECIPHERABLE = 30; + public static final int DC_DOWNLOAD_IN_PROGRESS = 1000; - public static final int DC_MSG_NO_ID = 0; - public final static int DC_MSG_ID_MARKER1 = 1; - public final static int DC_MSG_ID_DAYMARKER = 9; + public static final int DC_MSG_NO_ID = 0; + public static final int DC_MSG_ID_MARKER1 = 1; + public static final int DC_MSG_ID_DAYMARKER = 9; - public final static int DC_VIDEOCHATTYPE_UNKNOWN = 0; - public final static int DC_VIDEOCHATTYPE_BASICWEBRTC = 1; + public static final int DC_VIDEOCHATTYPE_UNKNOWN = 0; + public static final int DC_VIDEOCHATTYPE_BASICWEBRTC = 1; - private static final String TAG = DcMsg.class.getSimpleName(); + private static final String TAG = DcMsg.class.getSimpleName(); - public DcMsg(DcContext context, int viewtype) { - msgCPtr = context.createMsgCPtr(viewtype); + public DcMsg(DcContext context, int viewtype) { + msgCPtr = context.createMsgCPtr(viewtype); + } + + public DcMsg(long msgCPtr) { + this.msgCPtr = msgCPtr; + } + + public boolean isOk() { + return msgCPtr != 0; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefMsgCPtr(); + msgCPtr = 0; + } + + @Override + public int hashCode() { + return this.getId(); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof DcMsg)) { + return false; } - public DcMsg(long msgCPtr) { - this.msgCPtr = msgCPtr; - } + DcMsg that = (DcMsg) other; + return this.getId() == that.getId() && this.getId() != 0; + } - public boolean isOk() { - return msgCPtr != 0; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - unrefMsgCPtr(); - msgCPtr = 0; - } - - @Override - public int hashCode() { - return this.getId(); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof DcMsg)) { - return false; - } - - DcMsg that = (DcMsg) other; - return this.getId()==that.getId() && this.getId()!=0; - } - - /** - * If given a message, calculates the position of the message in the chat - */ - public static int getMessagePosition(DcMsg msg, DcContext dcContext) { - int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0); - int startingPosition = -1; - int msgId = msg.getId(); - for (int i = 0; i < msgs.length; i++) { - if (msgs[i] == msgId) { - startingPosition = msgs.length - 1 - i; - break; - } - } - return startingPosition; - } - - public native int getId (); - public native String getText (); - public native String getSubject (); - public native long getTimestamp (); - public native long getSortTimestamp (); - public native boolean hasDeviatingTimestamp(); - public native boolean hasLocation (); - public native int getType (); - public native int getInfoType (); - public native int getInfoContactId (); - public native int getState (); - public native int getDownloadState (); - public native int getChatId (); - public native int getFromId (); - public native int getWidth (int def); - public native int getHeight (int def); - public native int getDuration (); - public native void lateFilingMediaSize(int width, int height, int duration); - public DcLot getSummary (DcChat chat) { return new DcLot(getSummaryCPtr(chat.getChatCPtr())); } - public native String getSummarytext (int approx_characters); - public native int showPadlock (); - public boolean hasFile () { String file = getFile(); return file!=null && !file.isEmpty(); } - public native String getFile (); - public native String getFilemime (); - public native String getFilename (); - public native long getFilebytes (); - public native byte[] getWebxdcBlob (String filename); - public JSONObject getWebxdcInfo () { - try { - String json = getWebxdcInfoJson(); - if (json != null && !json.isEmpty()) return new JSONObject(json); - } catch(Exception e) { - e.printStackTrace(); + /** If given a message, calculates the position of the message in the chat */ + public static int getMessagePosition(DcMsg msg, DcContext dcContext) { + int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0); + int startingPosition = -1; + int msgId = msg.getId(); + for (int i = 0; i < msgs.length; i++) { + if (msgs[i] == msgId) { + startingPosition = msgs.length - 1 - i; + break; } - return new JSONObject(); } - public native String getWebxdcHref (); - public native boolean isForwarded (); - public native boolean isInfo (); - public native boolean hasHtml (); - public native String getSetupCodeBegin (); - public native void setText (String text); - public native void setFileAndDeduplicate(String file, String name, String filemime); - public native void setDimension (int width, int height); - public native void setDuration (int duration); - public native void setLocation (float latitude, float longitude); - public void setQuote (DcMsg quote) { setQuoteCPtr(quote.msgCPtr); } - public native String getQuotedText (); - public native String getError (); - public native String getOverrideSenderName(); - public native boolean isEdited (); + return startingPosition; + } - public String getSenderName(DcContact dcContact) { - String overrideName = getOverrideSenderName(); - if (overrideName != null) { - return "~" + overrideName; - } else { - return dcContact.getDisplayName(); - } - } + public native int getId(); - public DcMsg getQuotedMsg () { - long cPtr = getQuotedMsgCPtr(); - return cPtr != 0 ? new DcMsg(cPtr) : null; - } + public native String getText(); - public DcMsg getParent() { - long cPtr = getParentCPtr(); - return cPtr != 0 ? new DcMsg(cPtr) : null; - } + public native String getSubject(); - public native int getOriginalMsgId (); - public native int getSavedMsgId (); + public native long getTimestamp(); - public boolean canSave() { - // saving info-messages out of context results in confusion, see https://github.com/deltachat/deltachat-ios/issues/2567 - return !isInfo(); - } + public native long getSortTimestamp(); - public File getFileAsFile() { - if(getFile()==null) - throw new AssertionError("expected a file to be present."); - return new File(getFile()); - } + public native boolean hasDeviatingTimestamp(); - // aliases and higher-level tools - public static int[] msgSetToIds(final Set dcMsgs) { - if (dcMsgs == null) { - return new int[0]; - } - int[] ids = new int[dcMsgs.size()]; - int i = 0; - for (DcMsg dcMsg : dcMsgs) { - ids[i++] = dcMsg.getId(); - } - return ids; - } + public native boolean hasLocation(); - public boolean isOutgoing() { - return getFromId() == DcContact.DC_CONTACT_ID_SELF; - } + public native int getType(); - public String getDisplayBody() { - return getText(); - } + public native int getInfoType(); - public String getBody() { - return getText(); - } + public native int getInfoContactId(); - public long getDateReceived() { - return getTimestamp(); - } + public native int getState(); - public boolean isFailed() { - return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError())); - } - public boolean isPreparing() { - return getState() == DC_STATE_OUT_PREPARING; - } - public boolean isSecure() { - return showPadlock()!=0; - } - public boolean isPending() { - return getState() == DC_STATE_OUT_PENDING; - } - public boolean isDelivered() { - return getState() == DC_STATE_OUT_DELIVERED; - } - public boolean isRemoteRead() { - return getState() == DC_STATE_OUT_MDN_RCVD; - } - public boolean isSeen() { - return getState() == DC_STATE_IN_SEEN; - } + public native int getDownloadState(); + public native int getChatId(); - // working with raw c-data - private long msgCPtr; // CAVE: the name is referenced in the JNI - private native void unrefMsgCPtr (); - private native long getSummaryCPtr (long chatCPtr); - private native void setQuoteCPtr (long quoteCPtr); - private native long getQuotedMsgCPtr (); - private native long getParentCPtr (); - private native String getWebxdcInfoJson (); -}; + public native int getFromId(); + + public native int getWidth(int def); + + public native int getHeight(int def); + + public native int getDuration(); + + public native void lateFilingMediaSize(int width, int height, int duration); + + public DcLot getSummary(DcChat chat) { + return new DcLot(getSummaryCPtr(chat.getChatCPtr())); + } + + public native String getSummarytext(int approx_characters); + + public native int showPadlock(); + + public boolean hasFile() { + String file = getFile(); + return file != null && !file.isEmpty(); + } + + public native String getFile(); + + public native String getFilemime(); + + public native String getFilename(); + + public native long getFilebytes(); + + public native byte[] getWebxdcBlob(String filename); + + public JSONObject getWebxdcInfo() { + try { + String json = getWebxdcInfoJson(); + if (json != null && !json.isEmpty()) return new JSONObject(json); + } catch (Exception e) { + e.printStackTrace(); + } + return new JSONObject(); + } + + public native String getWebxdcHref(); + + public native boolean isForwarded(); + + public native boolean isInfo(); + + public native boolean hasHtml(); + + public native String getSetupCodeBegin(); + + public native void setText(String text); + + public native void setFileAndDeduplicate(String file, String name, String filemime); + + public native void setDimension(int width, int height); + + public native void setDuration(int duration); + + public native void setLocation(float latitude, float longitude); + + public void setQuote(DcMsg quote) { + setQuoteCPtr(quote.msgCPtr); + } + + public native String getQuotedText(); + + public native String getError(); + + public native String getOverrideSenderName(); + + public native boolean isEdited(); + + public String getSenderName(DcContact dcContact) { + String overrideName = getOverrideSenderName(); + if (overrideName != null) { + return "~" + overrideName; + } else { + return dcContact.getDisplayName(); + } + } + + public DcMsg getQuotedMsg() { + long cPtr = getQuotedMsgCPtr(); + return cPtr != 0 ? new DcMsg(cPtr) : null; + } + + public DcMsg getParent() { + long cPtr = getParentCPtr(); + return cPtr != 0 ? new DcMsg(cPtr) : null; + } + + public native int getOriginalMsgId(); + + public native int getSavedMsgId(); + + public boolean canSave() { + // saving info-messages out of context results in confusion, see + // https://github.com/deltachat/deltachat-ios/issues/2567 + return !isInfo(); + } + + public File getFileAsFile() { + if (getFile() == null) throw new AssertionError("expected a file to be present."); + return new File(getFile()); + } + + // aliases and higher-level tools + public static int[] msgSetToIds(final Set dcMsgs) { + if (dcMsgs == null) { + return new int[0]; + } + int[] ids = new int[dcMsgs.size()]; + int i = 0; + for (DcMsg dcMsg : dcMsgs) { + ids[i++] = dcMsg.getId(); + } + return ids; + } + + public boolean isOutgoing() { + return getFromId() == DcContact.DC_CONTACT_ID_SELF; + } + + public String getDisplayBody() { + return getText(); + } + + public String getBody() { + return getText(); + } + + public long getDateReceived() { + return getTimestamp(); + } + + public boolean isFailed() { + return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError())); + } + + public boolean isPreparing() { + return getState() == DC_STATE_OUT_PREPARING; + } + + public boolean isSecure() { + return showPadlock() != 0; + } + + public boolean isPending() { + return getState() == DC_STATE_OUT_PENDING; + } + + public boolean isDelivered() { + return getState() == DC_STATE_OUT_DELIVERED; + } + + public boolean isRemoteRead() { + return getState() == DC_STATE_OUT_MDN_RCVD; + } + + public boolean isSeen() { + return getState() == DC_STATE_IN_SEEN; + } + + // working with raw c-data + private long msgCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefMsgCPtr(); + + private native long getSummaryCPtr(long chatCPtr); + + private native void setQuoteCPtr(long quoteCPtr); + + private native long getQuotedMsgCPtr(); + + private native long getParentCPtr(); + + private native String getWebxdcInfoJson(); +} +; diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java b/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java index 5a8c66a6c..c738cfbfe 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java @@ -23,16 +23,20 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.lang.ref.SoftReference; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.audioplay.AudioView; @@ -45,61 +49,50 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.lang.ref.SoftReference; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - /** - * A DC adapter for a conversation thread. Ultimately - * used by ConversationActivity to display a conversation - * thread in a ListActivity. + * A DC adapter for a conversation thread. Ultimately used by ConversationActivity to display a + * conversation thread in a ListActivity. * * @author Moxie Marlinspike - * */ // FIXME: this breaks type checks, that is why there are so many casts. -public class ConversationAdapter +public class ConversationAdapter extends RecyclerView.Adapter - implements StickyHeaderDecoration.StickyHeaderAdapter -{ + implements StickyHeaderDecoration.StickyHeaderAdapter { private static final int MAX_CACHE_SIZE = 40; - private final Map> recordCache = - Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); + private final Map> recordCache = + Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); - private static final int MESSAGE_TYPE_OUTGOING = 0; - private static final int MESSAGE_TYPE_INCOMING = 1; - private static final int MESSAGE_TYPE_INFO = 2; - private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3; - private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; + private static final int MESSAGE_TYPE_OUTGOING = 0; + private static final int MESSAGE_TYPE_INCOMING = 1; + private static final int MESSAGE_TYPE_INFO = 2; + private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3; + private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5; private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6; - private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7; - private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8; - private static final int MESSAGE_TYPE_STICKER_INCOMING = 9; - private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10; + private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7; + private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8; + private static final int MESSAGE_TYPE_STICKER_INCOMING = 9; + private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10; private final Set batchSelected = Collections.synchronizedSet(new HashSet()); private final @Nullable ItemClickListener clickListener; - private final @NonNull GlideRequests glideRequests; - private final @NonNull Recipient recipient; - private final @NonNull LayoutInflater inflater; - private final @NonNull Context context; - private final @NonNull Calendar calendar; + private final @NonNull GlideRequests glideRequests; + private final @NonNull Recipient recipient; + private final @NonNull LayoutInflater inflater; + private final @NonNull Context context; + private final @NonNull Calendar calendar; - private final DcContext dcContext; - private @NonNull DcChat dcChat; - private @NonNull int[] dcMsgList = new int[0]; - private int positionToPulseHighlight = -1; - private int positionCurrentlyPulseHighlighting = -1; - private long pulseHighlightingSince = -1; - private int lastSeenPosition = -1; - private long lastSeen = -1; + private final DcContext dcContext; + private @NonNull DcChat dcChat; + private @NonNull int[] dcMsgList = new int[0]; + private int positionToPulseHighlight = -1; + private int positionCurrentlyPulseHighlighting = -1; + private long pulseHighlightingSince = -1; + private int lastSeenPosition = -1; + private long lastSeen = -1; private AudioPlaybackViewModel playbackViewModel; private AudioView.OnActionListener audioPlayPauseListener; @@ -110,7 +103,7 @@ public class ConversationAdapter @SuppressWarnings("unchecked") public V getView() { - return (V)itemView; + return (V) itemView; } public BindableConversationItem getItem() { @@ -118,16 +111,14 @@ public class ConversationAdapter } } - public boolean isActive() { return dcMsgList.length > 0; } - public @NonNull DcChat getChat(){ + public @NonNull DcChat getChat() { return dcChat; } - public void setLastSeen(long timestamp) { lastSeen = timestamp; } @@ -151,14 +142,14 @@ public class ConversationAdapter @Override public long getItemId(int position) { - if (position<0 || position>=dcMsgList.length) { + if (position < 0 || position >= dcMsgList.length) { return 0; } - return dcMsgList[dcMsgList.length-1-position]; + return dcMsgList[dcMsgList.length - 1 - position]; } public @NonNull DcMsg getMsg(int position) { - if(position<0 || position>=dcMsgList.length) { + if (position < 0 || position >= dcMsgList.length) { return new DcMsg(0); } @@ -170,7 +161,7 @@ public class ConversationAdapter } } - final DcMsg fromDb = dcContext.getMsg((int)getItemId(position)); + final DcMsg fromDb = dcContext.getMsg((int) getItemId(position)); recordCache.put(position, new SoftReference<>(fromDb)); return fromDb; } @@ -183,12 +174,10 @@ public class ConversationAdapter this.audioPlayPauseListener = audioPlayPauseListener; } - /** - * Returns the position of the message with msgId in the chat list, counted from the top - */ + /** Returns the position of the message with msgId in the chat list, counted from the top */ public int msgIdToPosition(int msgId) { - for(int i=0; i } } - interface ItemClickListener extends BindableConversationItem.EventListener { void onItemClick(DcMsg item); + void onItemLongClick(DcMsg item, View view); } - public ConversationAdapter(@NonNull Context context, - @NonNull DcChat dcChat, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener, - @NonNull Recipient recipient) { + public ConversationAdapter( + @NonNull Context context, + @NonNull DcChat dcChat, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener, + @NonNull Recipient recipient) { this.dcChat = dcChat; this.glideRequests = glideRequests; this.clickListener = clickListener; @@ -231,14 +221,14 @@ public class ConversationAdapter this.context = context; this.inflater = LayoutInflater.from(context); this.calendar = Calendar.getInstance(); - this.dcContext = DcHelper.getContext(context); + this.dcContext = DcHelper.getContext(context); setHasStableIds(true); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { - ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder)viewHolder; + ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder) viewHolder; long now = System.currentTimeMillis(); if (position == positionToPulseHighlight) { @@ -248,14 +238,25 @@ public class ConversationAdapter } long elapsed = now - pulseHighlightingSince; - boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS); + boolean pulseHighlight = + (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS); - holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener); + holder + .getItem() + .bind( + getMsg(position), + dcChat, + glideRequests, + batchSelected, + recipient, + pulseHighlight, + playbackViewModel, + audioPlayPauseListener); } @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder viewHolder) { - if (viewHolder.itemView instanceof ConversationItem) { + if (viewHolder.itemView instanceof ConversationItem) { ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1); } } @@ -263,17 +264,19 @@ public class ConversationAdapter @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType)); - itemView.setOnClickListener(view -> { - if (clickListener != null) { - clickListener.onItemClick(itemView.getMessageRecord()); - } - }); - itemView.setOnLongClickListener(view -> { - if (clickListener != null) { - clickListener.onItemLongClick(itemView.getMessageRecord(), view); - } - return true; - }); + itemView.setOnClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemClick(itemView.getMessageRecord()); + } + }); + itemView.setOnLongClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemLongClick(itemView.getMessageRecord(), view); + } + return true; + }); itemView.setEventListener(clickListener); return new ViewHolder(itemView); } @@ -284,14 +287,19 @@ public class ConversationAdapter case MESSAGE_TYPE_THUMBNAIL_OUTGOING: case MESSAGE_TYPE_DOCUMENT_OUTGOING: case MESSAGE_TYPE_STICKER_OUTGOING: - case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent; + case MESSAGE_TYPE_OUTGOING: + return R.layout.conversation_item_sent; case MESSAGE_TYPE_AUDIO_INCOMING: case MESSAGE_TYPE_THUMBNAIL_INCOMING: case MESSAGE_TYPE_DOCUMENT_INCOMING: case MESSAGE_TYPE_STICKER_INCOMING: - case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received; - case MESSAGE_TYPE_INFO: return R.layout.conversation_item_update; - default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); + case MESSAGE_TYPE_INCOMING: + return R.layout.conversation_item_received; + case MESSAGE_TYPE_INFO: + return R.layout.conversation_item_update; + default: + throw new IllegalArgumentException( + "unsupported item view type given to ConversationAdapter"); } } @@ -301,21 +309,18 @@ public class ConversationAdapter int type = dcMsg.getType(); if (dcMsg.isInfo()) { return MESSAGE_TYPE_INFO; - } - else if (type==DcMsg.DC_MSG_AUDIO || type==DcMsg.DC_MSG_VOICE) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING; - } - else if (type==DcMsg.DC_MSG_FILE) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING; - } - else if (type==DcMsg.DC_MSG_IMAGE || type==DcMsg.DC_MSG_GIF || type==DcMsg.DC_MSG_VIDEO) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING; - } - else if (type == DcMsg.DC_MSG_STICKER) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING; - } - else { - return dcMsg.isOutgoing()? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING; + } else if (type == DcMsg.DC_MSG_AUDIO || type == DcMsg.DC_MSG_VOICE) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING; + } else if (type == DcMsg.DC_MSG_FILE) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING; + } else if (type == DcMsg.DC_MSG_IMAGE + || type == DcMsg.DC_MSG_GIF + || type == DcMsg.DC_MSG_VIDEO) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING; + } else if (type == DcMsg.DC_MSG_STICKER) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING; + } else { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING; } } @@ -338,16 +343,16 @@ public class ConversationAdapter } public void pulseHighlightItem(int position) { - if (position>=0 && position < getItemCount()) { + if (position >= 0 && position < getItemCount()) { positionToPulseHighlight = position; notifyItemChanged(position); } } public long getSortTimestamp(int position) { - if (!isActive()) return 0; + if (!isActive()) return 0; if (position >= getItemCount()) return 0; - if (position < 0) return 0; + if (position < 0) return 0; DcMsg msg = getMsg(position); return msg.getSortTimestamp(); @@ -361,7 +366,7 @@ public class ConversationAdapter @Override public long getHeaderId(int position) { if (position >= getItemCount()) return -1; - if (position < 0) return -1; + if (position < 0) return -1; calendar.setTime(new Date(getSortTimestamp(position))); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); @@ -369,18 +374,17 @@ public class ConversationAdapter @Override public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false)); + return new HeaderViewHolder( + LayoutInflater.from(getContext()) + .inflate(R.layout.conversation_item_header, parent, false)); } - /** - * date header view - */ + /** date header view */ @Override public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { viewHolder.setText(DateUtils.getRelativeDate(getContext(), getSortTimestamp(position))); } - public void changeData(@Nullable int[] dcMsgList) { // should be called when there are new messages this.dcMsgList = dcMsgList == null ? new int[0] : dcMsgList; @@ -400,12 +404,11 @@ public class ConversationAdapter } private int findLastSeenPosition(long lastSeen) { - if (lastSeen <= 0) return -1; - if (!isActive()) return -1; + if (lastSeen <= 0) return -1; + if (!isActive()) return -1; int count = getItemCount(); - for (int i = 0; i < count; i++) { DcMsg msg = getMsg(i); if (msg.isOutgoing() || msg.getTimestamp() <= lastSeen) { @@ -417,11 +420,16 @@ public class ConversationAdapter } public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false)); + return new HeaderViewHolder( + LayoutInflater.from(getContext()) + .inflate(R.layout.conversation_item_last_seen, parent, false)); } public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) { - viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1))); + viewHolder.setText( + getContext() + .getResources() + .getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1))); } static class LastSeenHeader extends StickyHeaderDecoration { @@ -429,32 +437,45 @@ public class ConversationAdapter LastSeenHeader(ConversationAdapter adapter) { super(adapter, false, false); - this.adapter = adapter; + this.adapter = adapter; } @Override - protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - return adapter.isActive() && position == adapter.getLastSeenPosition(); + protected boolean hasHeader( + RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { + return adapter.isActive() && position == adapter.getLastSeenPosition(); } @Override - protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { + protected int getHeaderTop( + RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { return parent.getLayoutManager().getDecoratedTop(child); } @Override - protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { + protected HeaderViewHolder getHeader( + RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent); adapter.onBindLastSeenViewHolder(viewHolder, position); - int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); + int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); + int heightSpec = + View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width); - int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height); + int childWidth = + ViewGroup.getChildMeasureSpec( + widthSpec, + parent.getPaddingLeft() + parent.getPaddingRight(), + viewHolder.itemView.getLayoutParams().width); + int childHeight = + ViewGroup.getChildMeasureSpec( + heightSpec, + parent.getPaddingTop() + parent.getPaddingBottom(), + viewHolder.itemView.getLayoutParams().height); viewHolder.itemView.measure(childWidth, childHeight); - viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); + viewHolder.itemView.layout( + 0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); return viewHolder; } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index 78878ea88..6d6270cd2 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -36,7 +36,6 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -45,13 +44,21 @@ import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnScrollListener; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; 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 java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.reminder.DozeReminder; @@ -70,1043 +77,1101 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - @SuppressLint("StaticFieldLeak") -public class ConversationFragment extends MessageSelectorFragment -{ - private static final String TAG = ConversationFragment.class.getSimpleName(); +public class ConversationFragment extends MessageSelectorFragment { + private static final String TAG = ConversationFragment.class.getSimpleName(); - private static final int SCROLL_ANIMATION_THRESHOLD = 50; + private static final int SCROLL_ANIMATION_THRESHOLD = 50; - private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); + private final ActionModeCallback actionModeCallback = new ActionModeCallback(); + private final ItemClickListener selectionClickListener = + new ConversationFragmentItemClickListener(); - private ConversationFragmentListener listener; + private ConversationFragmentListener listener; - private Recipient recipient; - private long chatId; - private int startingPosition; - private boolean firstLoad; - private RecyclerView list; - private RecyclerView.ItemDecoration lastSeenDecoration; - private StickyHeaderDecoration dateDecoration; - private View scrollToBottomButton; - private View floatingLocationButton; - private View bottomDivider; - private AddReactionView addReactionView; - private TextView noMessageTextView; - private Timer reloadTimer; + private Recipient recipient; + private long chatId; + private int startingPosition; + private boolean firstLoad; + private RecyclerView list; + private RecyclerView.ItemDecoration lastSeenDecoration; + private StickyHeaderDecoration dateDecoration; + private View scrollToBottomButton; + private View floatingLocationButton; + private View bottomDivider; + private AddReactionView addReactionView; + private TextView noMessageTextView; + private Timer reloadTimer; - public boolean isPaused; - private Debouncer markseenDebouncer; - private Rpc rpc; - private boolean pendingAddBottomInsets; - private boolean pendingRemoveBottomInsets; + public boolean isPaused; + private Debouncer markseenDebouncer; + private Rpc rpc; + private boolean pendingAddBottomInsets; + private boolean pendingRemoveBottomInsets; - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - rpc = DcHelper.getRpc(getContext()); + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + rpc = DcHelper.getRpc(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_REACTIONS_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); + 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_REACTIONS_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); + 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); + } + + @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); + addReactionView = ViewUtil.findById(view, R.id.add_reaction_view); + noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view); + bottomDivider = ViewUtil.findById(view, R.id.bottom_divider); + + 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); + + if (pendingAddBottomInsets) { + bottomDivider.setVisibility(View.GONE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, true); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true); + pendingAddBottomInsets = false; } - @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); - addReactionView = ViewUtil.findById(view, R.id.add_reaction_view); - noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view); - bottomDivider = ViewUtil.findById(view, R.id.bottom_divider); - - 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); - - if (pendingAddBottomInsets) { - bottomDivider.setVisibility(View.GONE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, true); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true); - pendingAddBottomInsets = false; - } - - if (pendingRemoveBottomInsets) { - bottomDivider.setVisibility(View.VISIBLE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, false); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false); - pendingRemoveBottomInsets = false; - } - - return view; + if (pendingRemoveBottomInsets) { + bottomDivider.setVisibility(View.VISIBLE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, false); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false); + pendingRemoveBottomInsets = false; } - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); + return view; + } - initializeResources(); - initializeListAdapter(); - } + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initializeResources(); + initializeListAdapter(); + } private void setNoMessageText() { - DcChat dcChat = getListAdapter().getChat(); - if(dcChat.isMultiUser()){ - if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) { - noMessageTextView.setText(R.string.chat_new_channel_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 if(!dcChat.isEncrypted()) { - noMessageTextView.setText(R.string.chat_unencrypted_explanation); - } - else { - String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName()); - noMessageTextView.setText(message); - } - } - - public void handleAddBottomInsets() { - if (bottomDivider != null) { - bottomDivider.setVisibility(View.GONE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, true); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true); - pendingAddBottomInsets = false; + DcChat dcChat = getListAdapter().getChat(); + if (dcChat.isMultiUser()) { + if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) { + noMessageTextView.setText(R.string.chat_new_channel_hint); + } else if (dcChat.isUnpromoted()) { + noMessageTextView.setText(R.string.chat_new_group_hint); } else { - pendingAddBottomInsets = true; + 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 if (!dcChat.isEncrypted()) { + noMessageTextView.setText(R.string.chat_unencrypted_explanation); + } else { + String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName()); + noMessageTextView.setText(message); } + } - public void handleRemoveBottomInsets() { - if (bottomDivider != null) { - bottomDivider.setVisibility(View.VISIBLE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, false); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false); - pendingRemoveBottomInsets = false; - } else { - pendingRemoveBottomInsets = true; - } + public void handleAddBottomInsets() { + if (bottomDivider != null) { + bottomDivider.setVisibility(View.GONE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, true); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true); + pendingAddBottomInsets = false; + } else { + pendingAddBottomInsets = true; } + } - @Override - public void onDestroy() { - DcHelper.getEventCenter(getContext()).removeObservers(this); - reloadTimer.cancel(); - super.onDestroy(); + public void handleRemoveBottomInsets() { + if (bottomDivider != null) { + bottomDivider.setVisibility(View.VISIBLE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, false); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false); + pendingRemoveBottomInsets = false; + } else { + pendingRemoveBottomInsets = true; } + } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - this.listener = (ConversationFragmentListener)activity; - } + @Override + public void onDestroy() { + DcHelper.getEventCenter(getContext()).removeObservers(this); + reloadTimer.cancel(); + super.onDestroy(); + } - @Override - public void onResume() { - super.onResume(); - Util.runOnBackground(() -> { + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.listener = (ConversationFragmentListener) activity; + } + + @Override + public void onResume() { + super.onResume(); + Util.runOnBackground( + () -> { try { rpc.marknoticedChat(rpc.getSelectedAccountId(), (int) chatId); } catch (RpcException e) { - Log.e(TAG, "RPC error", e); + Log.e(TAG, "RPC error", e); } }); - if (list.getAdapter() != null) { - list.getAdapter().notifyDataSetChanged(); - } - - if (isPaused) { - isPaused = false; - markseenDebouncer.publish(() -> manageMessageSeenState()); - } + 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 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(); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - dateDecoration.onConfigurationChanged(newConfig); + 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; } - public void onNewIntent() { - if (actionMode != null) { - actionMode.finish(); - } - - initializeResources(); - initializeListAdapter(); - - if (chatId == -1) { - reloadList(); - updateLocationButton(); - } + if (getListAdapter().getLastSeenPosition() < 0) { + return; } - - 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())); - } + final int lastSeenPosition = getListAdapter().getLastSeenPosition() + 1; + if (lastSeenPosition > 0) { + list.post( + () -> + ((LinearLayoutManager) list.getLayoutManager()) + .scrollToPositionWithOffset(lastSeenPosition, list.getHeight())); } + } - public void hideAddReactionView() { - addReactionView.hide(); - } + public void hideAddReactionView() { + addReactionView.hide(); + } - 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; + 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); - } + 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), selectionClickListener, this.recipient); - list.setAdapter(adapter); - AudioPlaybackViewModel playbackViewModel = - new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - adapter.setPlaybackViewModel(playbackViewModel); - adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity())); - - if (dateDecoration != null) { - list.removeItemDecoration(dateDecoration); - } - dateDecoration = new StickyHeaderDecoration(adapter, false, false); - list.addItemDecoration(dateDecoration); - - int freshMsgs = 0; - try { - freshMsgs = rpc.getFreshMsgCnt(rpc.getSelectedAccountId(), (int) chatId); - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } - SetStartingPositionLinearLayoutManager layoutManager = (SetStartingPositionLinearLayoutManager) list.getLayoutManager(); - if (startingPosition > -1) { - layoutManager.setStartingPosition(startingPosition); - } else if (freshMsgs > 0) { - layoutManager.setStartingPosition(freshMsgs - 1); - } - - reloadList(); - updateLocationButton(); - - if (lastSeenDecoration != null) { - list.removeItemDecoration(lastSeenDecoration); - } - if (freshMsgs > 0) { - getListAdapter().setLastSeenPosition(freshMsgs - 1); - lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter()); - list.addItemDecoration(lastSeenDecoration); - } - } - } - - @Override - protected void setCorrectMenuVisibility(Menu menu) { - Set 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_edit).setVisible(false); - menu.findItem(R.id.menu_context_reply_privately).setVisible(false); - menu.findItem(R.id.menu_add_to_home_screen).setVisible(false); - menu.findItem(R.id.menu_toggle_save).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); - menu.findItem(R.id.menu_context_edit).setVisible(chat.isEncrypted() && chat.canSend() && canEditMsg(messageRecord)); - 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); - - boolean saved = messageRecord.getSavedMsgId() != 0; - MenuItem toggleSave = menu.findItem(R.id.menu_toggle_save); - toggleSave.setVisible(messageRecord.canSave() && !chat.isSelfTalk()); - toggleSave.setIcon(saved? R.drawable.baseline_bookmark_remove_24 : R.drawable.baseline_bookmark_border_24); - toggleSave.setTitle(saved? R.string.unsave : R.string.save); - } - - // 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(); - } - - static boolean canEditMsg(DcMsg dcMsg) { - return dcMsg.isOutgoing() && !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_CALL && !dcMsg.hasHtml() && !dcMsg.getText().isEmpty(); - } - - public void handleClearChat() { - AudioPlaybackViewModel playbackViewModel = + private void initializeListAdapter() { + if (this.recipient != null && this.chatId != -1) { + ConversationAdapter adapter = + new ConversationAdapter( + getActivity(), + this.recipient.getChat(), + GlideApp.with(this), + selectionClickListener, + this.recipient); + list.setAdapter(adapter); + AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + adapter.setPlaybackViewModel(playbackViewModel); + adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity())); - handleDeleteMessages( - (int) chatId, - getListAdapter().getMessageIds(), - playbackViewModel::stopByIds, - playbackViewModel::stopByIds); - } + if (dateDecoration != null) { + list.removeItemDecoration(dateDecoration); + } + dateDecoration = new StickyHeaderDecoration(adapter, false, false); + list.addItemDecoration(dateDecoration); - 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) { - ConversationAdapter adapter = getListAdapter(); - if (adapter != null) { - adapter.setLastSeen(lastSeen); - if (lastSeenDecoration != null) { - list.removeItemDecoration(lastSeenDecoration); - } - if (lastSeen > 0) { - lastSeenDecoration = new ConversationAdapter.LastSeenHeader(adapter); - list.addItemDecoration(lastSeenDecoration); - } - } - } - - private void handleCopyMessage(final Set dcMsgsSet) { - List dcMsgsList = new LinkedList<>(dcMsgsSet); - Collections.sort(dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived())); - boolean singleMsg = dcMsgsList.size() == 1; - - StringBuilder result = new StringBuilder(); - - DcContext dcContext = DcHelper.getContext(getContext()); - 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)).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 messageRecords) { - Intent composeIntent = new Intent(); - int[] msgIds = DcMsg.msgSetToIds(messageRecords); - try { - setForwardingMessageIds(composeIntent, msgIds, rpc.getSelectedAccountId()); - ConversationListRelayingActivity.start(this, composeIntent); - getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } - } - - private void handleSaveSticker(final DcMsg message) { - if (message.getType() != DcMsg.DC_MSG_STICKER) { - return; - } - - File stickerFile = message.getFileAsFile(); - if (stickerFile == null || !stickerFile.exists()) { - Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); - return; - } - - // Create stickers directory in internal storage - File stickersDir = new File(getContext().getFilesDir(), "stickers"); - if (!stickersDir.exists()) { - stickersDir.mkdirs(); - } - - // Copy sticker to stickers directory - String extension = stickerFile.getName(); - int dotPos = extension.lastIndexOf('.'); - if (dotPos >= 0) { - extension = extension.substring(dotPos + 1); - } - String fileName = System.currentTimeMillis() + "." + extension; - File destFile = new File(stickersDir, fileName); - - try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile); - java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - Toast.makeText(getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); - - // Refresh sticker picker to show the newly added sticker - if (getActivity() instanceof ConversationActivity) { - ((ConversationActivity) getActivity()).refreshStickerPicker(); - } - } catch (Exception e) { - Log.e(TAG, "Error saving sticker", e); - Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); - } - } - - @SuppressLint("RestrictedApi") - private void handleReplyMessage(final DcMsg message) { - if (getActivity() != null) { - //noinspection ConstantConditions - ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); - } - - listener.handleReplyMessage(message); - } - - @SuppressLint("RestrictedApi") - private void handleEditMessage(final DcMsg message) { - if (getActivity() != null) { - //noinspection ConstantConditions - ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); - } - - listener.handleEditMessage(message); - } - - private void handleReplyMessagePrivately(final DcMsg msg) { - - if (getActivity() != null) { - DcContext dcContext = DcHelper.getContext(getActivity()); - 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); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - getActivity().startActivity(intent); - } else { - Log.e(TAG, "Activity was null"); - } - } - - private void handleToggleSave(final Set messageRecords) { - DcMsg msg = getSelectedMessageRecord(messageRecords); - try { - if (msg.getSavedMsgId() != 0) { - rpc.deleteMessages(rpc.getSelectedAccountId(), Collections.singletonList(msg.getSavedMsgId())); - } else { - rpc.saveMsgs(rpc.getSelectedAccountId(), Collections.singletonList(msg.getId())); - } - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } - } - - 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(DcHelper.getContext(getContext()).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); + int freshMsgs = 0; + try { + freshMsgs = rpc.getFreshMsgCnt(rpc.getSelectedAccountId(), (int) chatId); + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); + } + SetStartingPositionLinearLayoutManager layoutManager = + (SetStartingPositionLinearLayoutManager) list.getLayoutManager(); + if (startingPosition > -1) { + layoutManager.setStartingPosition(startingPosition); + } else if (freshMsgs > 0) { + layoutManager.setStartingPosition(freshMsgs - 1); } - if (position != -1) { - scrollAndHighlight(position, smooth); + reloadList(); + updateLocationButton(); + + if (lastSeenDecoration != null) { + list.removeItemDecoration(lastSeenDecoration); + } + if (freshMsgs > 0) { + getListAdapter().setLastSeenPosition(freshMsgs - 1); + lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter()); + list.addItemDecoration(lastSeenDecoration); + } + } + } + + @Override + protected void setCorrectMenuVisibility(Menu menu) { + Set 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_edit).setVisible(false); + menu.findItem(R.id.menu_context_reply_privately).setVisible(false); + menu.findItem(R.id.menu_add_to_home_screen).setVisible(false); + menu.findItem(R.id.menu_toggle_save).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); + menu.findItem(R.id.menu_context_edit) + .setVisible(chat.isEncrypted() && chat.canSend() && canEditMsg(messageRecord)); + 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); + + boolean saved = messageRecord.getSavedMsgId() != 0; + MenuItem toggleSave = menu.findItem(R.id.menu_toggle_save); + toggleSave.setVisible(messageRecord.canSave() && !chat.isSelfTalk()); + toggleSave.setIcon( + saved ? R.drawable.baseline_bookmark_remove_24 : R.drawable.baseline_bookmark_border_24); + toggleSave.setTitle(saved ? R.string.unsave : R.string.save); + } + + // 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(); + } + + static boolean canEditMsg(DcMsg dcMsg) { + return dcMsg.isOutgoing() + && !dcMsg.isInfo() + && dcMsg.getType() != DcMsg.DC_MSG_CALL + && !dcMsg.hasHtml() + && !dcMsg.getText().isEmpty(); + } + + public void handleClearChat() { + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + + handleDeleteMessages( + (int) chatId, + getListAdapter().getMessageIds(), + playbackViewModel::stopByIds, + playbackViewModel::stopByIds); + } + + 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) { + ConversationAdapter adapter = getListAdapter(); + if (adapter != null) { + adapter.setLastSeen(lastSeen); + if (lastSeenDecoration != null) { + list.removeItemDecoration(lastSeenDecoration); + } + if (lastSeen > 0) { + lastSeenDecoration = new ConversationAdapter.LastSeenHeader(adapter); + list.addItemDecoration(lastSeenDecoration); + } + } + } + + private void handleCopyMessage(final Set dcMsgsSet) { + List dcMsgsList = new LinkedList<>(dcMsgsSet); + Collections.sort( + dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived())); + boolean singleMsg = dcMsgsList.size() == 1; + + StringBuilder result = new StringBuilder(); + + DcContext dcContext = DcHelper.getContext(getContext()); + 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)).append(":\n"); + } + if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) { + result.append(msg.getText()); } else { - Log.e(TAG, "msgId not found for scrolling: " + msgId); + 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 messageRecords) { + Intent composeIntent = new Intent(); + int[] msgIds = DcMsg.msgSetToIds(messageRecords); + try { + setForwardingMessageIds(composeIntent, msgIds, rpc.getSelectedAccountId()); + ConversationListRelayingActivity.start(this, composeIntent); + getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); + } + } + + private void handleSaveSticker(final DcMsg message) { + if (message.getType() != DcMsg.DC_MSG_STICKER) { + return; + } + + File stickerFile = message.getFileAsFile(); + if (stickerFile == null || !stickerFile.exists()) { + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); + return; + } + + // Create stickers directory in internal storage + File stickersDir = new File(getContext().getFilesDir(), "stickers"); + if (!stickersDir.exists()) { + stickersDir.mkdirs(); + } + + // Copy sticker to stickers directory + String extension = stickerFile.getName(); + int dotPos = extension.lastIndexOf('.'); + if (dotPos >= 0) { + extension = extension.substring(dotPos + 1); + } + String fileName = System.currentTimeMillis() + "." + extension; + File destFile = new File(stickersDir, fileName); + + try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile); + java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + Toast.makeText(getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); + + // Refresh sticker picker to show the newly added sticker + if (getActivity() instanceof ConversationActivity) { + ((ConversationActivity) getActivity()).refreshStickerPicker(); + } + } catch (Exception e) { + Log.e(TAG, "Error saving sticker", e); + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); + } + } + + @SuppressLint("RestrictedApi") + private void handleReplyMessage(final DcMsg message) { + if (getActivity() != null) { + //noinspection ConstantConditions + ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); + } + + listener.handleReplyMessage(message); + } + + @SuppressLint("RestrictedApi") + private void handleEditMessage(final DcMsg message) { + if (getActivity() != null) { + //noinspection ConstantConditions + ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); + } + + listener.handleEditMessage(message); + } + + private void handleReplyMessagePrivately(final DcMsg msg) { + + if (getActivity() != null) { + DcContext dcContext = DcHelper.getContext(getActivity()); + 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); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + getActivity().startActivity(intent); + } else { + Log.e(TAG, "Activity was null"); + } + } + + private void handleToggleSave(final Set messageRecords) { + DcMsg msg = getSelectedMessageRecord(messageRecords); + try { + if (msg.getSavedMsgId() != 0) { + rpc.deleteMessages( + rpc.getSelectedAccountId(), Collections.singletonList(msg.getSavedMsgId())); + } else { + rpc.saveMsgs(rpc.getSelectedAccountId(), Collections.singletonList(msg.getId())); + } + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); + } + } + + 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( + DcHelper.getContext(getContext()).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); + + void handleEditMessage(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()); + + ConversationFragment.this.addReactionView.move(dy); + } + + @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; + } + + ArrayList ids = new ArrayList<>(lastPos - firstPos + 1); + for (int pos = firstPos; pos <= lastPos; pos++) { + DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos); + if (message.getFromId() != DC_CONTACT_ID_SELF) { + ids.add(message.getId()); } } - - public interface ConversationFragmentListener { - void handleReplyMessage(DcMsg messageRecord); - void handleEditMessage(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()); - - ConversationFragment.this.addReactionView.move(dy); - } - - @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; - } - - ArrayList ids = new ArrayList<>(lastPos - firstPos + 1); - for(int pos = firstPos; pos <= lastPos; pos++) { - DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos); - if (message.getFromId() != DC_CONTACT_ID_SELF) { - ids.add(message.getId()); - } - } - Util.runOnAnyBackgroundThread(() -> { + Util.runOnAnyBackgroundThread( + () -> { try { rpc.markseenMsgs(rpc.getSelectedAccountId(), ids); } catch (RpcException e) { Log.e(TAG, "RPC error", e); } }); - } + } - private class ConversationFragmentItemClickListener implements ItemClickListener { + private class ConversationFragmentItemClickListener implements ItemClickListener { - @Override - public void onItemClick(DcMsg messageRecord) { - if (actionMode != null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); + @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 { - hideAddReactionView(); - 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(DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) { - DozeReminder.dozeReminderTapped(getContext()); - } - else if(StatsSending.isStatsSendingDeviceMsg(getContext(), messageRecord)) { - StatsSending.statsDeviceMsgTapped(getActivity()); - } - else if(messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) { - if (messageRecord.getParent() != null) { - // if the parent webxdc message still exists - WebxdcActivity.openWebxdcActivity(getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref()); - } - } - else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) { - Intent intent = new Intent(getContext(), ProfileActivity.class); - intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId); - startActivity(intent); - } - else { - int infoContactId = messageRecord.getInfoContactId(); - if (infoContactId != 0 && infoContactId != DC_CONTACT_ID_SELF) { - Intent intent = new Intent(getContext(), ProfileActivity.class); - intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, infoContactId); - startActivity(intent); - } - else { - String self_mail = DcHelper.getContext(getContext()).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 - Intent intent = new Intent(getActivity(), EditRelayActivity.class); - intent.putExtra(EditRelayActivity.EXTRA_ADDR, self_mail); - startActivity(intent); - } - } - } + if (getListAdapter().getSelectedItems().size() == 0) { + actionMode.finish(); + } else { + hideAddReactionView(); + 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 } - - @Override - public void onItemLongClick(DcMsg messageRecord, View view) { - if (actionMode == null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); - - actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); - addReactionView.show(messageRecord, view, () -> { - if (actionMode != null) { - actionMode.finish(); - } - }); - } + } else if (DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) { + DozeReminder.dozeReminderTapped(getContext()); + } else if (StatsSending.isStatsSendingDeviceMsg(getContext(), messageRecord)) { + StatsSending.statsDeviceMsgTapped(getActivity()); + } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) { + if (messageRecord.getParent() != null) { + // if the parent webxdc message still exists + WebxdcActivity.openWebxdcActivity( + getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref()); } - - private void jumpToOriginal(DcMsg original) { - if (original == null) { - Log.i(TAG, "Clicked on a quote or jump-to-original whose original message was deleted/non-existing."); - Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - int foreignChatId = original.getChatId(); - if (foreignChatId != 0 && foreignChatId != chatId) { - Intent intent = new Intent(getActivity(), ConversationActivity.class); - intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId); - int start = DcMsg.getMessagePosition(original, DcHelper.getContext(getContext())); - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - ((ConversationActivity) getActivity()).hideSoftKeyboard(); - if (getActivity() != null) { - getActivity().startActivity(intent); - } else { - Log.e(TAG, "Activity was null"); - } - } else { - scrollMaybeSmoothToMsgId(original.getId()); - } - } - - @Override - public void onJumpToOriginalClicked(DcMsg messageRecord) { - jumpToOriginal(DcHelper.getContext(getContext()).getMsg(messageRecord.getOriginalMsgId())); - } - - @Override - public void onQuoteClicked(DcMsg messageRecord) { - jumpToOriginal(messageRecord.getQuotedMsg()); - } - - @Override - public void onShowFullClicked(DcMsg messageRecord) { - Intent intent = new Intent(getActivity(), FullMsgActivity.class); - intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId()); - intent.putExtra(FullMsgActivity.BLOCK_LOADING_REMOTE, getListAdapter().getChat().isContactRequest()); + } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) { + Intent intent = new Intent(getContext(), ProfileActivity.class); + intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId); startActivity(intent); - getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); - } - - @Override - public void onDownloadClicked(DcMsg messageRecord) { - DcHelper.getContext(getContext()).downloadFullMsg(messageRecord.getId()); - } - - @Override - public void onReactionClicked(DcMsg messageRecord) { - ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); - dialog.show(getActivity().getSupportFragmentManager(), null); - } - - @Override - public void onStickerClicked(DcMsg messageRecord) { - new AlertDialog.Builder(getContext()) - .setTitle(R.string.add_to_sticker_collection) - .setMessage(R.string.ask_add_sticker_to_collection) - .setPositiveButton(R.string.ok, (dialog, which) -> { - handleSaveSticker(messageRecord); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } - } - - private class ActionModeCallback implements ActionMode.Callback { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.conversation_context, menu); - - mode.setTitle("1"); - - Util.redMenuItem(menu, R.id.menu_context_delete_message); - 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(); - - actionMode = null; - hideAddReactionView(); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - hideAddReactionView(); - int itemId = item.getItemId(); - AudioPlaybackViewModel playbackViewModel = - new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - - if (itemId == R.id.menu_context_copy) { - handleCopyMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_delete_message) { - handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds); - return true; - } else if (itemId == R.id.menu_context_share) { - DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND); - return true; - } else if (itemId == R.id.menu_context_details) { - handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_forward) { - handleForwardMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_add_to_home_screen) { - WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_save_attachment) { - handleSaveAttachment(getListAdapter().getSelectedItems()); - return true; - } else if (itemId == R.id.menu_context_reply) { - handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_edit) { - handleEditMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_reply_privately) { - handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - return true; - } else if (itemId == R.id.menu_resend) { - handleResendMessage(getListAdapter().getSelectedItems()); - return true; - } else if (itemId == R.id.menu_toggle_save) { - handleToggleSave(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; + } else { + int infoContactId = messageRecord.getInfoContactId(); + if (infoContactId != 0 && infoContactId != DC_CONTACT_ID_SELF) { + Intent intent = new Intent(getContext(), ProfileActivity.class); + intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, infoContactId); + startActivity(intent); + } else { + String self_mail = DcHelper.getContext(getContext()).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 + Intent intent = new Intent(getActivity(), EditRelayActivity.class); + intent.putExtra(EditRelayActivity.EXTRA_ADDR, self_mail); + startActivity(intent); } - 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; + public void onItemLongClick(DcMsg messageRecord, View view) { + if (actionMode == null) { + ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + list.getAdapter().notifyDataSetChanged(); - case DcContext.DC_EVENT_REACTIONS_CHANGED: - 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); - }*/ + actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback); + addReactionView.show( + messageRecord, + view, + () -> { + if (actionMode != null) { + actionMode.finish(); + } + }); + } } + + private void jumpToOriginal(DcMsg original) { + if (original == null) { + Log.i( + TAG, + "Clicked on a quote or jump-to-original whose original message was deleted/non-existing."); + Toast.makeText( + getContext(), + R.string.ConversationFragment_quoted_message_not_found, + Toast.LENGTH_SHORT) + .show(); + return; + } + + int foreignChatId = original.getChatId(); + if (foreignChatId != 0 && foreignChatId != chatId) { + Intent intent = new Intent(getActivity(), ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId); + int start = DcMsg.getMessagePosition(original, DcHelper.getContext(getContext())); + intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + ((ConversationActivity) getActivity()).hideSoftKeyboard(); + if (getActivity() != null) { + getActivity().startActivity(intent); + } else { + Log.e(TAG, "Activity was null"); + } + } else { + scrollMaybeSmoothToMsgId(original.getId()); + } + } + + @Override + public void onJumpToOriginalClicked(DcMsg messageRecord) { + jumpToOriginal(DcHelper.getContext(getContext()).getMsg(messageRecord.getOriginalMsgId())); + } + + @Override + public void onQuoteClicked(DcMsg messageRecord) { + jumpToOriginal(messageRecord.getQuotedMsg()); + } + + @Override + public void onShowFullClicked(DcMsg messageRecord) { + Intent intent = new Intent(getActivity(), FullMsgActivity.class); + intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId()); + intent.putExtra( + FullMsgActivity.BLOCK_LOADING_REMOTE, getListAdapter().getChat().isContactRequest()); + startActivity(intent); + getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); + } + + @Override + public void onDownloadClicked(DcMsg messageRecord) { + DcHelper.getContext(getContext()).downloadFullMsg(messageRecord.getId()); + } + + @Override + public void onReactionClicked(DcMsg messageRecord) { + ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); + dialog.show(getActivity().getSupportFragmentManager(), null); + } + + @Override + public void onStickerClicked(DcMsg messageRecord) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.add_to_sticker_collection) + .setMessage(R.string.ask_add_sticker_to_collection) + .setPositiveButton( + R.string.ok, + (dialog, which) -> { + handleSaveSticker(messageRecord); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + } + + private class ActionModeCallback implements ActionMode.Callback { + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.conversation_context, menu); + + mode.setTitle("1"); + + Util.redMenuItem(menu, R.id.menu_context_delete_message); + 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(); + + actionMode = null; + hideAddReactionView(); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + hideAddReactionView(); + int itemId = item.getItemId(); + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + + if (itemId == R.id.menu_context_copy) { + handleCopyMessage(getListAdapter().getSelectedItems()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_delete_message) { + handleDeleteMessages( + (int) chatId, + getListAdapter().getSelectedItems(), + playbackViewModel::stopByIds, + playbackViewModel::stopByIds); + return true; + } else if (itemId == R.id.menu_context_share) { + DcHelper.openForViewOrShare( + getContext(), + getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), + Intent.ACTION_SEND); + return true; + } else if (itemId == R.id.menu_context_details) { + handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_forward) { + handleForwardMessage(getListAdapter().getSelectedItems()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_add_to_home_screen) { + WebxdcActivity.addToHomeScreen( + getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_save_attachment) { + handleSaveAttachment(getListAdapter().getSelectedItems()); + return true; + } else if (itemId == R.id.menu_context_reply) { + handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_edit) { + handleEditMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_reply_privately) { + handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + return true; + } else if (itemId == R.id.menu_resend) { + handleResendMessage(getListAdapter().getSelectedItems()); + return true; + } else if (itemId == R.id.menu_toggle_save) { + handleToggleSave(getListAdapter().getSelectedItems()); + actionMode.finish(); + 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_REACTIONS_CHANGED: + 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); + }*/ + } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java index 7efe4c073..4b41c166b 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java @@ -8,14 +8,11 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.DateUtils; - import chat.delta.rpc.types.CallInfo; import chat.delta.rpc.types.CallState; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.DateUtils; public class CallItemView extends FrameLayout { private static final String TAG = CallItemView.class.getSimpleName(); @@ -43,11 +40,12 @@ public class CallItemView extends FrameLayout { this.title = findViewById(R.id.title); this.duration = findViewById(R.id.duration); - setOnClickListener(v -> { - if (viewListener != null && callInfo != null) { - viewListener.onClick(v, callInfo); - } - }); + setOnClickListener( + v -> { + if (viewListener != null && callInfo != null) { + viewListener.onClick(v, callInfo); + } + }); } public void setCallClickListener(CallClickListener listener) { @@ -58,7 +56,9 @@ public class CallItemView extends FrameLayout { this.callInfo = callInfo; if (callInfo.state instanceof CallState.Completed) { - duration.setText(DateUtils.getFormattedCallDuration(getContext(), ((CallState.Completed) callInfo.state).duration)); + duration.setText( + DateUtils.getFormattedCallDuration( + getContext(), ((CallState.Completed) callInfo.state).duration)); duration.setVisibility(VISIBLE); } else { duration.setVisibility(GONE); @@ -76,17 +76,20 @@ public class CallItemView extends FrameLayout { title.setText(R.string.audio_call); } - icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24); + icon.setImageResource( + callInfo.hasVideo ? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24); int[] attrs; if (isOutgoing) { - attrs = new int[]{ - R.attr.conversation_item_outgoing_text_primary_color, - }; + attrs = + new int[] { + R.attr.conversation_item_outgoing_text_primary_color, + }; } else { - attrs = new int[]{ - R.attr.conversation_item_incoming_text_primary_color, - }; + attrs = + new int[] { + R.attr.conversation_item_incoming_text_primary_color, + }; } try (TypedArray ta = getContext().obtainStyledAttributes(attrs)) { icon.setColorFilter(ta.getColor(0, Color.BLACK)); @@ -94,7 +97,8 @@ public class CallItemView extends FrameLayout { } public String getDescription() { - return title.getText() + (duration.getVisibility()==VISIBLE ? ("\n" + duration.getText()) : ""); + return title.getText() + + (duration.getVisibility() == VISIBLE ? ("\n" + duration.getText()) : ""); } public interface CallClickListener { diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java b/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java index 373d7bc09..549e014b9 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java @@ -4,55 +4,55 @@ import android.content.Context; import android.media.AudioAttributes; import android.media.SoundPool; import android.util.Log; - import org.thoughtcrime.securesms.R; public class InChatSounds { - private static final String TAG = InChatSounds.class.getSimpleName(); - private static volatile InChatSounds instance; + private static final String TAG = InChatSounds.class.getSimpleName(); + private static volatile InChatSounds instance; - private SoundPool soundPool = null; - private int soundIn = 0; - private int soundOut = 0; + private SoundPool soundPool = null; + private int soundIn = 0; + private int soundOut = 0; - static public InChatSounds getInstance(Context context) { + public static InChatSounds getInstance(Context context) { + if (instance == null) { + synchronized (InChatSounds.class) { if (instance == null) { - synchronized (InChatSounds.class) { - if (instance == null) { - instance = new InChatSounds(context); - } - } + instance = new InChatSounds(context); } - return instance; + } } + return instance; + } - private InChatSounds(Context context) { - try { - AudioAttributes audioAttrs = new AudioAttributes.Builder() + private InChatSounds(Context context) { + try { + AudioAttributes audioAttrs = + new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); - soundPool = new SoundPool.Builder().setMaxStreams(3).setAudioAttributes(audioAttrs).build(); - soundIn = soundPool.load(context, R.raw.sound_in, 1); - soundOut = soundPool.load(context, R.raw.sound_out, 1); - } catch(Exception e) { - Log.e(TAG, "cannot initialize sounds", e); - } + soundPool = new SoundPool.Builder().setMaxStreams(3).setAudioAttributes(audioAttrs).build(); + soundIn = soundPool.load(context, R.raw.sound_in, 1); + soundOut = soundPool.load(context, R.raw.sound_out, 1); + } catch (Exception e) { + Log.e(TAG, "cannot initialize sounds", e); } + } - public void playSendSound() { - try { - soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); - } catch(Exception e) { - Log.e(TAG, "cannot play send sound", e); - } + public void playSendSound() { + try { + soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); + } catch (Exception e) { + Log.e(TAG, "cannot play send sound", e); } + } - public void playIncomingSound() { - try { - soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); - } catch(Exception e) { - Log.e(TAG, "cannot play incoming sound", e); - } + public void playIncomingSound() { + try { + soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); + } catch (Exception e) { + Log.e(TAG, "cannot play incoming sound", e); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 21c8de8b0..d036dc092 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -24,23 +24,17 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; - import androidx.core.app.RemoteInput; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.Util; - -/** - * Get the response text from the Wearable Device and sends an message as a reply - */ +/** Get the response text from the Wearable Device and sends an message as a reply */ public class RemoteReplyReceiver extends BroadcastReceiver { - public static final String TAG = RemoteReplyReceiver.class.getSimpleName(); - public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY"; + public static final String TAG = RemoteReplyReceiver.class.getSimpleName(); + public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY"; public static final String ACCOUNT_ID_EXTRA = "account_id"; public static final String CHAT_ID_EXTRA = "chat_id"; public static final String MSG_ID_EXTRA = "msg_id"; @@ -60,20 +54,21 @@ public class RemoteReplyReceiver extends BroadcastReceiver { final CharSequence responseText = remoteInput.getCharSequence(EXTRA_REMOTE_REPLY); if (responseText != null) { - Util.runOnAnyBackgroundThread(() -> { - DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); - dcContext.marknoticedChat(chatId); - dcContext.markseenMsgs(new int[]{msgId}); - if (dcContext.getChat(chatId).isContactRequest()) { - dcContext.acceptChat(chatId); - } + Util.runOnAnyBackgroundThread( + () -> { + DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); + dcContext.marknoticedChat(chatId); + dcContext.markseenMsgs(new int[] {msgId}); + if (dcContext.getChat(chatId).isContactRequest()) { + dcContext.acceptChat(chatId); + } - DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - msg.setText(responseText.toString()); - dcContext.sendMsg(chatId, msg); + DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); + msg.setText(responseText.toString()); + dcContext.sendMsg(chatId, msg); - DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); - }); + DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java b/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java index 82b86c07f..263290af1 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java @@ -6,11 +6,9 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.util.Log; - import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.ForegroundDetector; @@ -65,27 +63,31 @@ public final class FetchForegroundService extends Service { Log.i(TAG, "Creating fetch service"); super.onCreate(); - Notification notification = new NotificationCompat.Builder(this, NotificationCenter.CH_GENERIC) - .setContentTitle(getString(R.string.connectivity_updating)) - .setSmallIcon(R.drawable.notification_permanent) - .build(); + Notification notification = + new NotificationCompat.Builder(this, NotificationCenter.CH_GENERIC) + .setContentTitle(getString(R.string.connectivity_updating)) + .setSmallIcon(R.drawable.notification_permanent) + .build(); try { startForeground(NotificationCenter.ID_FETCH, notification); - Util.runOnAnyBackgroundThread(() -> { - Log.i(TAG, "Starting fetch"); - if (!ApplicationContext.getDcAccounts().backgroundFetch(300)) { // as startForeground() was called, there is time - FetchForegroundService.stop(this); - } // else we stop FetchForegroundService on DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE - }); + Util.runOnAnyBackgroundThread( + () -> { + Log.i(TAG, "Starting fetch"); + if (!ApplicationContext.getDcAccounts() + .backgroundFetch(300)) { // as startForeground() was called, there is time + FetchForegroundService.stop(this); + } // else we stop FetchForegroundService on DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE + }); } catch (Exception e) { Log.e(TAG, "Error calling startForeground()", e); } } public static void fetchSynchronously() { - // According to the documentation https://firebase.google.com/docs/cloud-messaging/android/receive, + // According to the documentation + // https://firebase.google.com/docs/cloud-messaging/android/receive, // we need to handle the message within 20s, and the time window may be even shorter than 20s, // so, use 10s to be safe. fetchingSynchronously = true; @@ -97,9 +99,11 @@ public final class FetchForegroundService extends Service { while (fetchingSynchronously) { try { // The `wait()` needs to be enclosed in a while loop because there may be - // "spurious wake-ups", i.e. `wait()` may return even though `notifyAll()` wasn't called. + // "spurious wake-ups", i.e. `wait()` may return even though `notifyAll()` wasn't + // called. STOP_NOTIFIER.wait(); - } catch (InterruptedException ex) {} + } catch (InterruptedException ex) { + } } } } @@ -121,5 +125,4 @@ public final class FetchForegroundService extends Service { ApplicationContext.getDcAccounts().stopBackgroundFetch(); stopSelf(); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 569153394..62db53bb3 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -11,23 +11,20 @@ import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.util.Log; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat.Builder; import androidx.core.content.ContextCompat; - -import org.thoughtcrime.securesms.DummyActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.notifications.NotificationCenter; -import org.thoughtcrime.securesms.util.IntentUtils; - import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.thoughtcrime.securesms.DummyActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.notifications.NotificationCenter; +import org.thoughtcrime.securesms.util.IntentUtils; public final class GenericForegroundService extends Service { @@ -35,17 +32,17 @@ public final class GenericForegroundService extends Service { private final IBinder binder = new LocalBinder(); - private static final String EXTRA_TITLE = "extra_title"; - private static final String EXTRA_CONTENT_TEXT = "extra_content_text"; - private static final String EXTRA_CHANNEL_ID = "extra_channel_id"; - private static final String EXTRA_ICON_RES = "extra_icon_res"; - private static final String EXTRA_ID = "extra_id"; - private static final String EXTRA_PROGRESS = "extra_progress"; - private static final String EXTRA_PROGRESS_MAX = "extra_progress_max"; + private static final String EXTRA_TITLE = "extra_title"; + private static final String EXTRA_CONTENT_TEXT = "extra_content_text"; + private static final String EXTRA_CHANNEL_ID = "extra_channel_id"; + private static final String EXTRA_ICON_RES = "extra_icon_res"; + private static final String EXTRA_ID = "extra_id"; + private static final String EXTRA_PROGRESS = "extra_progress"; + private static final String EXTRA_PROGRESS_MAX = "extra_progress_max"; private static final String EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate"; private static final String ACTION_START = "start"; - private static final String ACTION_STOP = "stop"; + private static final String ACTION_STOP = "stop"; private static final AtomicInteger NEXT_ID = new AtomicInteger(); private static final AtomicBoolean CHANNEL_CREATED = new AtomicBoolean(false); @@ -54,7 +51,9 @@ public final class GenericForegroundService extends Service { private final LinkedHashMap allActiveMessages = new LinkedHashMap<>(); - private static final Entry DEFAULTS = new Entry("", "", NotificationCenter.CH_GENERIC, R.drawable.icon_notification, -1, 0, 0, false); + private static final Entry DEFAULTS = + new Entry( + "", "", NotificationCenter.CH_GENERIC, R.drawable.icon_notification, -1, 0, 0, false); private @Nullable Entry lastPosted; @@ -66,9 +65,11 @@ public final class GenericForegroundService extends Service { synchronized (GenericForegroundService.class) { String action = intent.getAction(); - if (ACTION_START.equals(action)) handleStart(intent); - else if (ACTION_STOP .equals(action)) handleStop(intent); - else throw new IllegalStateException(String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP)); + if (ACTION_START.equals(action)) handleStart(intent); + else if (ACTION_STOP.equals(action)) handleStop(intent); + else + throw new IllegalStateException( + String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP)); updateNotification(); @@ -89,7 +90,6 @@ public final class GenericForegroundService extends Service { } } - private synchronized void handleStart(@NonNull Intent intent) { Entry entry = Entry.fromIntent(intent); @@ -112,14 +112,18 @@ public final class GenericForegroundService extends Service { private void postObligatoryForegroundNotification(@NonNull Entry active) { lastPosted = active; - startForeground(NotificationCenter.ID_GENERIC, new Builder(this, active.channelId) - .setSmallIcon(active.iconRes) - .setContentTitle(active.title) - .setTicker(active.contentText) - .setContentText(active.contentText) - .setProgress(active.progressMax, active.progress, active.indeterminate) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, DummyActivity.class), IntentUtils.FLAG_MUTABLE())) - .build()); + startForeground( + NotificationCenter.ID_GENERIC, + new Builder(this, active.channelId) + .setSmallIcon(active.iconRes) + .setContentTitle(active.title) + .setTicker(active.contentText) + .setContentText(active.contentText) + .setProgress(active.progressMax, active.progress, active.indeterminate) + .setContentIntent( + PendingIntent.getActivity( + this, 0, new Intent(this, DummyActivity.class), IntentUtils.FLAG_MUTABLE())) + .build()); } @Override @@ -127,8 +131,8 @@ public final class GenericForegroundService extends Service { return binder; } - - public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) { + public static NotificationController startForegroundTask( + @NonNull Context context, @NonNull String task) { startedCounter++; final int id = NEXT_ID.getAndIncrement(); @@ -151,14 +155,15 @@ public final class GenericForegroundService extends Service { intent.putExtra(EXTRA_ID, id); ContextCompat.startForegroundService(context, intent); - startedCounter = Math.max(startedCounter-1, 0); + startedCounter = Math.max(startedCounter - 1, 0); } public static boolean isForegroundTaskStarted() { return startedCounter > 0; } - synchronized void replaceProgress(int id, int progressMax, int progress, boolean indeterminate, String message) { + synchronized void replaceProgress( + int id, int progressMax, int progress, boolean indeterminate, String message) { Entry oldEntry = allActiveMessages.get(id); if (oldEntry == null) { @@ -170,7 +175,16 @@ public final class GenericForegroundService extends Service { message = oldEntry.contentText; } - Entry newEntry = new Entry(oldEntry.title, message, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, progressMax, progress, indeterminate); + Entry newEntry = + new Entry( + oldEntry.title, + message, + oldEntry.channelId, + oldEntry.iconRes, + oldEntry.id, + progressMax, + progress, + indeterminate); if (oldEntry.equals(newEntry)) { Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry)); @@ -185,36 +199,47 @@ public final class GenericForegroundService extends Service { } @TargetApi(Build.VERSION_CODES.O) - static public void createFgNotificationChannel(Context context) { - if(!CHANNEL_CREATED.get() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + public static void createFgNotificationChannel(Context context) { + if (!CHANNEL_CREATED.get() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CHANNEL_CREATED.set(true); - NotificationChannel channel = new NotificationChannel(NotificationCenter.CH_GENERIC, - "Generic Background Service", NotificationManager.IMPORTANCE_MIN); - channel.setDescription("Ensure app will not be killed while long ongoing background tasks are running."); + NotificationChannel channel = + new NotificationChannel( + NotificationCenter.CH_GENERIC, + "Generic Background Service", + NotificationManager.IMPORTANCE_MIN); + channel.setDescription( + "Ensure app will not be killed while long ongoing background tasks are running."); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } - private static class Entry { - final @NonNull String title; - final @NonNull String contentText; - final @NonNull String channelId; - final int id; - final @DrawableRes int iconRes; - final int progress; - final int progressMax; - final boolean indeterminate; + final @NonNull String title; + final @NonNull String contentText; + final @NonNull String channelId; + final int id; + final @DrawableRes int iconRes; + final int progress; + final int progressMax; + final boolean indeterminate; - private Entry(@NonNull String title, @NonNull String contentText, @NonNull String channelId, @DrawableRes int iconRes, int id, int progressMax, int progress, boolean indeterminate) { - this.title = title; - this.contentText = contentText; - this.channelId = channelId; - this.iconRes = iconRes; - this.id = id; - this.progress = progress; - this.progressMax = progressMax; + private Entry( + @NonNull String title, + @NonNull String contentText, + @NonNull String channelId, + @DrawableRes int iconRes, + int id, + int progressMax, + int progress, + boolean indeterminate) { + this.title = title; + this.contentText = contentText; + this.channelId = channelId; + this.iconRes = iconRes; + this.id = id; + this.progress = progress; + this.progressMax = progressMax; this.indeterminate = indeterminate; } @@ -230,17 +255,26 @@ public final class GenericForegroundService extends Service { String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID); if (channelId == null) channelId = DEFAULTS.channelId; - int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes); - int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress); - int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax); - boolean indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate); + int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes); + int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress); + int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax); + boolean indeterminate = + intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate); - return new Entry(title, contentText, channelId, iconRes, id, progressMax, progress, indeterminate); + return new Entry( + title, contentText, channelId, iconRes, id, progressMax, progress, indeterminate); } @Override public @NonNull String toString() { - return String.format(Locale.ENGLISH, "ChannelId: %s Id: %d Progress: %d/%d %s", channelId, id, progress, progressMax, indeterminate ? "indeterminate" : "determinate"); + return String.format( + Locale.ENGLISH, + "ChannelId: %s Id: %d Progress: %d/%d %s", + channelId, + id, + progress, + progressMax, + indeterminate ? "indeterminate" : "determinate"); } @Override @@ -249,14 +283,14 @@ public final class GenericForegroundService extends Service { if (o == null || getClass() != o.getClass()) return false; Entry entry = (Entry) o; - return id == entry.id && - iconRes == entry.iconRes && - progress == entry.progress && - progressMax == entry.progressMax && - indeterminate == entry.indeterminate && - title.equals(entry.title) && - contentText.equals(entry.contentText) && - channelId.equals(entry.channelId); + return id == entry.id + && iconRes == entry.iconRes + && progress == entry.progress + && progressMax == entry.progressMax + && indeterminate == entry.indeterminate + && title.equals(entry.title) + && contentText.equals(entry.contentText) + && channelId.equals(entry.channelId); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java b/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java index b45c8fc49..db259d8d7 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java +++ b/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java @@ -5,25 +5,20 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; - import androidx.annotation.NonNull; - import java.util.concurrent.atomic.AtomicReference; -/** - * Class to control notifications triggered by GenericForeGroundService. - * - */ +/** Class to control notifications triggered by GenericForeGroundService. */ public final class NotificationController { private final @NonNull Context context; private final int id; - private int progress; - private int progressMax; + private int progress; + private int progressMax; private boolean indeterminate; - private String message = ""; - private long percent = -1; + private String message = ""; + private long percent = -1; private final ServiceConnection serviceConnection; @@ -31,26 +26,31 @@ public final class NotificationController { NotificationController(@NonNull Context context, int id) { this.context = context; - this.id = id; + this.id = id; - serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service; - GenericForegroundService genericForegroundService = binder.getService(); + serviceConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + GenericForegroundService.LocalBinder binder = + (GenericForegroundService.LocalBinder) service; + GenericForegroundService genericForegroundService = binder.getService(); - NotificationController.this.service.set(genericForegroundService); + NotificationController.this.service.set(genericForegroundService); - updateProgressOnService(); - } + updateProgressOnService(); + } - @Override - public void onServiceDisconnected(ComponentName name) { - service.set(null); - } - }; + @Override + public void onServiceDisconnected(ComponentName name) { + service.set(null); + } + }; - context.bindService(new Intent(context, GenericForegroundService.class), serviceConnection, Context.BIND_AUTO_CREATE); + context.bindService( + new Intent(context, GenericForegroundService.class), + serviceConnection, + Context.BIND_AUTO_CREATE); } public int getId() { @@ -61,7 +61,7 @@ public final class NotificationController { try { GenericForegroundService.stopForegroundTask(context, id); context.unbindService(serviceConnection); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -74,16 +74,18 @@ public final class NotificationController { setProgress((int) newProgressMax, (int) newProgress, false, newMessage); } - private synchronized void setProgress(int newProgressMax, int newProgress, boolean indeterminant, @NonNull String newMessage) { + private synchronized void setProgress( + int newProgressMax, int newProgress, boolean indeterminant, @NonNull String newMessage) { int newPercent = newProgressMax != 0 ? 100 * newProgress / newProgressMax : -1; - boolean same = newPercent == percent && indeterminate == indeterminant && newMessage.equals(message); + boolean same = + newPercent == percent && indeterminate == indeterminant && newMessage.equals(message); - percent = newPercent; - progress = newProgress; - progressMax = newProgressMax; + percent = newPercent; + progress = newProgress; + progressMax = newProgressMax; indeterminate = indeterminant; - message = newMessage; + message = newMessage; if (same) return; @@ -95,6 +97,6 @@ public final class NotificationController { if (genericForegroundService == null) return; - genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate, message); + genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate, message); } } diff --git a/src/main/res/layout/call_item_view.xml b/src/main/res/layout/call_item_view.xml index 84eabaea9..616bac31a 100644 --- a/src/main/res/layout/call_item_view.xml +++ b/src/main/res/layout/call_item_view.xml @@ -9,42 +9,43 @@ android:gravity="center_vertical" android:focusable="true"> - + - - - + android:layout_weight="1" + android:layout_marginStart="6dp" + android:layout_marginEnd="6dp" + android:orientation="vertical"> - + - + + + diff --git a/src/main/res/layout/conversation_item_call.xml b/src/main/res/layout/conversation_item_call.xml index 15a28934f..462433a29 100644 --- a/src/main/res/layout/conversation_item_call.xml +++ b/src/main/res/layout/conversation_item_call.xml @@ -1,9 +1,9 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/call_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + tools:visibility="visible" /> diff --git a/src/main/res/layout/conversation_item_received.xml b/src/main/res/layout/conversation_item_received.xml index 0333daad3..4166bb60a 100644 --- a/src/main/res/layout/conversation_item_received.xml +++ b/src/main/res/layout/conversation_item_received.xml @@ -13,265 +13,263 @@ android:clipToPadding="false" android:clipChildren="false"> - + - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/jumpto_icon"> + + + + + + + + + android:layout_marginRight="@dimen/message_bubble_edge_margin" + android:layout_marginEnd="@dimen/message_bubble_edge_margin" + android:layout_marginLeft="6dp" + android:layout_marginStart="6dp" + android:layout_toRightOf="@id/contact_photo_container" + android:layout_toEndOf="@id/contact_photo_container" + android:orientation="vertical" + android:clipToPadding="false" + android:clipChildren="false" + android:background="@color/white" + tools:backgroundTint="@color/core_light_10"> - + android:layout_marginTop="@dimen/message_bubble_top_padding" + android:layout_marginBottom="2dp" + android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" + android:layout_marginRight="@dimen/message_bubble_horizontal_padding" + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> + + + + + + + + + + + + + + + + + + + + + + + +