From dd30951dfb968afea503872a98bcde946945e418 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Jul 2022 13:00:34 +0200 Subject: [PATCH 01/25] every device is 21+ now --- .../eu/siacs/conversations/persistance/FileBackend.java | 8 ++++---- .../java/eu/siacs/conversations/ui/util/Attachment.java | 2 +- .../java/eu/siacs/conversations/utils/Compatibility.java | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 821899eb7500589ff0a77cf3f296d1f574f52f00..faeca6308938e7ef997676d81c297a0b26220296 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -437,7 +437,7 @@ public class FileBackend { return bitmap; } final String mime = attachment.getMime(); - if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(mime)) { bitmap = cropCenterSquarePdf(attachment.getUri(), size); drawOverlay( bitmap, @@ -961,7 +961,7 @@ public class FileBackend { } DownloadableFile file = getFile(message); final String mime = file.getMimeType(); - if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(mime)) { thumbnail = getPdfDocumentPreview(file, size); } else if (mime.startsWith("video/")) { thumbnail = getVideoPreview(file, size); @@ -1507,12 +1507,12 @@ public class FileBackend { body.append(url); } body.append('|').append(file.getSize()); - if (image || video || (pdf && Compatibility.runsTwentyOne())) { + if (image || video || pdf) { try { final Dimensions dimensions; if (video) { dimensions = getVideoDimensions(file); - } else if (pdf && Compatibility.runsTwentyOne()) { + } else if (pdf) { dimensions = getPdfDocumentDimensions(file); } else { dimensions = getImageDimensions(file); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index b539c70efca6aafc6de8b998f323abbf055b06a1..f994955d0d300c01f61c2f8697c4012b367fec28 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -179,7 +179,7 @@ public class Attachment implements Parcelable { private static boolean renderFileThumbnail(final String mime) { return mime.startsWith("video/") || isImage(mime) - || (Compatibility.runsTwentyOne() && "application/pdf".equals(mime)); + || "application/pdf".equals(mime); } public Uri getUri() { diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index f181853c64a9a2cadc33d2922727090f54d08fdc..aadeaabf57b6d98bd080f9462f3bd2e10d32938b 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -43,10 +43,6 @@ public class Compatibility { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } - public static boolean runsTwentyOne() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - private static boolean runsTwentyFour() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } From 8027b3be248a175c68573c65824319a6747ff89a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 10:39:18 +0200 Subject: [PATCH 02/25] parse pep events only from bare jid --- .../eu/siacs/conversations/parser/MessageParser.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index f45b1e89b871932ffee9e2bcf3838da03ea3f200..86799bd1123ee6ee2e0e0f9ed95b061bd2a98ac4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -196,8 +196,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } private void parseEvent(final Element event, final Jid from, final Account account) { - Element items = event.findChild("items"); - String node = items == null ? null : items.getAttribute("node"); + final Element items = event.findChild("items"); + final String node = items == null ? null : items.getAttribute("node"); if ("urn:xmpp:avatar:metadata".equals(node)) { Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { @@ -1000,7 +1000,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event"); - if (event != null && InvalidJid.hasValidFrom(original)) { + if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) { if (event.hasChild("items")) { parseEvent(event, original.getFrom(), account); } else if (event.hasChild("delete")) { @@ -1012,6 +1012,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final String nick = packet.findChildContent("nick", Namespace.NICK); if (nick != null && InvalidJid.hasValidFrom(original)) { + if (mXmppConnectionService.isMuc(account, from)) { + return; + } final Contact contact = account.getRoster().getContact(from); if (contact.setPresenceName(nick)) { mXmppConnectionService.syncRoster(account); From 78c3b1f527ce56768754b0ab1555498614b71c46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 16:54:18 +0200 Subject: [PATCH 03/25] pulled translations from transifex --- src/main/res/values-ja/strings.xml | 4 +- src/main/res/values-pl/strings.xml | 4 +- src/main/res/values-sv/strings.xml | 172 +++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index a207bb71c53123744b78e3c5507e8be489efa6c9..7fb4f6ae375908ff02e6c593453b91049fc9162e 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -957,4 +957,6 @@ プレーンテキスト文書 アカウント登録はサポートされていません XMPPアドレスがみつかりません - + 一時的な認証失敗 + + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 4b49a6501fd40d97d29ffe31fb4d0201307b99a4..30f28a9adb9829f85d817d7aff147e56d5b562a8 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1003,4 +1003,6 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dokument zwykłego tekstu Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP - + Tymczasowy błąd uwierzytelniania + + diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index d73433c363bfa7391a1adb9be1da07a7b9359d91..14e91099b90c778fbfbc53a95eb70d5f83271a2c 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -201,6 +201,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: External Service Discovery XEP-0163: PEP (Avatarbilder / OMEMO) XEP-0363: Ladda upp via HTTP XEP-0357: Push @@ -464,6 +465,7 @@ Nerladdning gick fel: Filen hittades inte Nerladdningen gick fel: Kunder inte ansluta till server Nerladdning gick fel: Kunde inte skriva fil + Nedladdning misslyckades: Ogiltig fil Tor-nätverk ej tillgängligt Bind-fel Den här servern ansvarar inte för den här domänen @@ -537,11 +539,15 @@ Säkerhetsfel: Ogiltig filåtkomst! Ingen applikation hittades för att dela URI Dela URI med... +
Du registrerar dig med ditt telefonnummer och Quicksy kommer automatiskt – baserat på telefonnumren i din adressbok – att föreslå möjliga kontakter till dig.

Genom att registrera dig godkänner du vår integritetspolicy.]]>
Acceptera och gå vidare + En guide har skapats för kontoskapande på conversations.im.¹\nNär du väljer conversations.im som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. Din fullständiga XMPP-adress kommer att vara: %s Skapa konto Använd min egen leverantör Välj användarnamn + Hantera tillgänglighet manuellt + Ställ in din tillgänglighet när du redigerar ditt statusmeddelande. Statusmeddelande Tillgänglig Online @@ -559,6 +565,8 @@ Kort Medium Lång + Gör användandet offentligt + Låter dina kontakter veta när du använder Conversations Privatliv Tema Välj färgschema @@ -591,6 +599,8 @@ Visa felmeddelande Felmeddelande Databesparing + Ditt operativsystem begränsar åtkomsten till Internet i bakgrunden för %1$s. För att få aviseringar om nya meddelanden bör du tillåta obegränsad åtkomst för %1$s, när databesparing är på.\n %1$s kommer fortfarande att anstränga sig för att spara data när det är möjligt. + Din enhet stöder inte inaktivering av databesparing för %1$s. Det gick inte att skapa en tillfällig fil Denna enhet har verifierats Kopiera fingeravtryck @@ -613,10 +623,13 @@ Rensa privat lagring där filer lagras (De kan om-laddas från servern) Jag följde denna länk från en trovärdig källa Du håller på att verifiera OMEMO-nyckeln för %1$s efter att du följt en länk. Detta är endast säkert om du följde länken från en trovärdig källa där endast %2$s kan ha publiserat denna länk. + Du är på väg att verifiera OMEMO-nycklarna för ditt eget konto. Detta är bara säkert om du följde den här länken från en pålitlig källa där bara du kunde ha publicerat den här länken. + Fortsätt Verifiera OMEMO-nycklar Visa inaktiva Dölj inaktiva Lita ej på enhet + Är du säker på att du vill ta bort verifieringen av den här enheten?\nDen här enheten och meddelanden från den kommer att markeras som \"Ej betrodd\". %d sekund %d sekunder @@ -649,12 +662,15 @@ Korresponderande konversationer är stängda. Kontakt blockerad. Notifieringar från främlingar + Meddela för meddelanden och samtal från främlingar. Mottagna meddelanden från främlingar Blockera främling Blockera hel domän online just nu Försök dekryptera igen Sessionsfel + Nedgraderad SASL-mekanism + Servern kräver registrering via webbplatsen Öppna webbsida Ingen applikation hittades för att kunna öppna webbsidan Se upp-notifikationer @@ -662,46 +678,110 @@ Idag Igår Bekräfta värdnamn med DNSSEC + Servercertifikat som innehåller det validerade värdnamnet anses vara verifierade Certifikatet innehåller ej en XMPP-adress delvis Spela in video Kopiera till urklipp Meddelande kopierat till urklipp Meddelande + Privata meddelanden är inaktiverade + Skyddade applikationer + För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade applikationer. Godkänn okänt certifikat? Servercertifikatet är inte signerat av en känd certifikatutfärdare. + Acceptera servernamn som inte matchar? + Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för: Vill du ansluta ändå? Certifikatdetaljer: + En gång + QR-läsaren behöver åtkomst till kameran + Bläddra till botten + Bläddra ner efter att du har skickat ett meddelande + Redigera Statusmeddelande + Redigera statusmeddelande + Inaktivera kryptering + %1$s kan inte skicka krypterade meddelanden till %2$s. Detta kan bero på att din kontakt använder en föråldrad server eller klient som inte kan hantera OMEMO. + Det gick inte att hämta enhetslistan + Det gick inte att hämta krypteringsnycklar + Tips: I vissa fall kan detta åtgärdas genom att lägga till varandra i era respektive kontaktlistor. + Är du säker på att du vill inaktivera OMEMO-kryptering för den här konversationen?\nDetta gör att din serveradministratör kan läsa dina meddelanden, men det kan också vara det enda sättet att kommunicera med människor som använder äldre klienter. + Inaktivera nu Utkast: + OMEMO-kryptering + OMEMO kommer alltid att användas för privata konversationer och privata gruppchattar. + OMEMO kommer att användas som standard för nya konversationer. + OMEMO måste manuellt aktiveras för varje ny konversation. Skapa genväg + Textstorlek + Den relativa teckenstorleken som används i appen. + På som standard + Av som standard Liten Mellan Stor + Meddelandet är inte krypterat för den här enheten. + Misslyckades med att dekryptera OMEMO-meddelandet. + ångra + Platsdelning är inaktiverat + Lås position + Lås upp position Kopiera plats Dela plats + Hänvisningar Dela plats Visa plats Dela + Det gick inte att starta inspelningen Var god dröj... + Ge %1$s tillgång till mikrofonen Söka i meddelanden GIF + Visa konversation + Dela plats-tillägget + Kopiera webbadress Kopiera XMPP-adress + HTTP-fildelning för S3 + Direktsök + Gruppkonversationens visningsbild + Värden stöder inte visningsbilder för gruppkonversationer + Endast ägaren kan ändra visningsbilden för gruppkonversationen + Kontaktnamn Smeknamn Namn Att ange ett namn är valfritt Gruppchattens namn + Kunde inte att spara inspelningen + Förgrundsservice + Statusinformation + Anslutningsproblem + Meddelanden + Samtal + Meddelanden + Inkommande samtal + Pågående samtal + Tysta meddelanden + Misslyckade leveranser + Videokompression + Visa media Deltagare + Mediautforskare + Videokvalitet Mellan (360p) Hög (720p) + avbruten + Du håller redan på att skriva ett meddelande. Välj ett land telefonnummer Bekräfta ditt telefonnummer + tillbaka Ja Nej Bekräftar... Okänt nätverksfel. För många försök Du använder en föråldrad version av denna app. + Uppdatera Ditt namn Skriv in ditt namn Avslå begäran @@ -709,38 +789,130 @@ Starta Orbot e-bok Öppna med... + Konversationens profilbild Välj konto Återställa säkerhetskopiering Återställa Ange ditt lösenord till kontot %s för att återställa säkerhetskopian. Det gick inte att återställa säkerhetskopian. + Säkerhetskopia & Återställ + Ange XMPP-adress Skapa gruppchatt + Anslut till publik gruppkonversation Skapa sluten gruppchatt + Skapa publik gruppkonversation Kanalnamn XMPP-adress Vänligen ange ett namn på kanalen Ange en XMPP-adress Detta är en XMPP-adress. Ange ett namn. + Skapar publik gruppkonversation... Denna kanal finns redan Du har gått med i en befintlig kanal + Det gick inte att spara kanalkonfigurationen + Tillåt vem som helst att ändra ämnet + Tillåt vem som helst att bjuda in andra + Vem som helst kan ändra ämnet. + Ägaren kan ändra ämnet. + Administratörer kan ändra ämnet. + Ägare kan bjuda in andra. + Vem som helst kan bjuda in andra. XMPP-adresser är synliga för administratörer. XMPP-adresser är synliga för alla. + Den här publika gruppkonversationen har inga deltagare. Bjud in dina kontakter eller använd \'dela-knappen\' för att dela XMPP-adressen. Denna slutna gruppchatt har inga deltagare. Hantera rättigheter + Sök efter deltagare För stor fil Bifoga Upptäck kanaler + Sök efter gruppkonversationer + Möjlig integritetskränkning! Jag har redan ett konto Lägg till befintligt konto Skapa nytt konto Detta verkar vara ett domännamn Lägg till ändå Detta ser ut som en kanaladress + Dela säkerhetskopior + Säkerhetskopior för Conversations + Händelse + Öppna säkerhetskopia Filen du valde är inte en säkerhetskopia till Conversations + Det här kontot har redan konfigurerats + Var god ange lösenordet för det här kontot + Det gick inte att utföra den här åtgärden + Anslut till publik gruppkonversation... + Delnings-appen gav inte behörighet till att komma åt den här filen. + + jabber.network + Lokal server + De flesta användare bör välja \"jabber.network\" för bättre förslag från hela det offentliga XMPP-ekosystemet. + Metod för kanalupptäckt + Säkerhetskopiering Om Aktivera ett konto + Ring + Inkommande samtal + Inkommande videosamtal + Ansluter + Ansluten + Återansluter + Accepterar samtal + Avslutar samtal + Svara + Avvisa + Upptäcker enheter + Ringer Upptagen + Kunde inte koppla samtal + Anslutning bröts + Återkallat samtal + Appmisslyckande + Verifikationsproblem + Lägg på + Pågående samtal + Pågående videosamtal + Återansluter samtalet + Återansluter videosamtalet + Inaktivera Tor för att ringa samtal + Inkommande samtal + Inkommande samtal · %s + Missat samtal · %s + Utgående samtal + Pågående samtal · %s + Missat samtal + Röstsamtal + Videosamtal + Hjälp + Växla till konversation + Din mikrofon är inte tillgänglig + Du kan bara ha ett samtal åt gången. + Återgå till pågående samtal + Kunde inte växla kamera Fäst flik till toppen Ta bort flik från toppen + GPX-spår + Kunde inte korrigera meddelandet + Alla konversationer + Den här konversationen + Din visningsbild + Visningsbild för %s + Krypterad med OMEMO + Krypterad med OpenPGP + Inte krypterad + Avsluta + Spela in ett röstmeddelande + Spela upp ljud + Pausa ljud + Lägg till kontakt, skapa eller gå med i gruppchatt eller upptäck kanaler + + Visa %1$d deltagare + Visa %1$d deltagare + + Misslyckade leveranser Fler alternativ + Ingen applikation hittades + Bjud in till Conversations + Ingen XMPP-adress hittades From b6ce914f6209fc3a2195d75283fee5234c0d1c69 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 20:30:47 +0200 Subject: [PATCH 04/25] version bump to 2.10.8 --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e748a2cf7e46a19e89381c02d58a30d0e081d82..90365d20ef3fdd427a5aebb502510ab4a043dd48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.10.8 + +* Fix wrong avatar being shown for group chats + ### Version 2.10.7 * always ask for battery optimizations opt-out diff --git a/build.gradle b/build.gradle index b48ffb58e053a891801680b472f7c2a92966d45a..900cead2dc499b1aa28fb518254761655083fcba 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42033 - versionName "2.10.7" + versionCode 42034 + versionName "2.10.8" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 62a379862e3ccbcaa4631960da4dedeaa9e0517d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 1 Aug 2022 10:14:49 +0200 Subject: [PATCH 05/25] jingle rtp: improve logging and error reporting --- .../eu/siacs/conversations/parser/AbstractParser.java | 2 +- .../java/eu/siacs/conversations/parser/MessageParser.java | 3 ++- .../xmpp/jingle/JingleConnectionManager.java | 4 ++-- .../conversations/xmpp/jingle/JingleRtpConnection.java | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 000fa036d9675f7ce84f282740f3a005482dcfc2..f4b01b7d3199cf821732226b79cc1c998296ee89 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -126,7 +126,7 @@ public abstract class AbstractParser { return user; } - public static String extractErrorMessage(Element packet) { + public static String extractErrorMessage(final Element packet) { final Element error = packet.findChild("error"); if (error != null && error.getChildren().size() > 0) { final List errorNames = orderedElementNames(error.getChildren()); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 86799bd1123ee6ee2e0e0f9ed95b061bd2a98ac4..5c66451ce5d1af7024457d6d92fe1df9d0c832af 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -327,7 +327,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) { final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length()); - mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId); + final String message = extractErrorMessage(packet); + mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message); return true; } mXmppConnectionService.markMessage(account, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0f9694915fd0cab677ece1614a7e039e87b4f563..416877236b4e7d3cb57a9d8a8fe2d6b0c886feb5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -908,12 +908,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public void failProceed(Account account, final Jid with, String sessionId) { + public void failProceed(Account account, final Jid with, final String sessionId, final String message) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); + ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(message); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index fd918fa9b3030a22663bddb664c81a9812d742bd..353851c37f0cdf63568740b94967c0bf9a67ff64 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -797,7 +797,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } final org.webrtc.SessionDescription sdp = @@ -928,10 +928,10 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - void deliverFailedProceed() { + void deliverFailedProceed(final String message) { Log.d( Config.LOGTAG, - id.account.getJid().asBareJid() + ": receive message error for proceed message"); + id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); Log.d( @@ -1270,7 +1270,7 @@ public class JingleRtpConnection extends AbstractJingleConnection webRTCWrapper.close(); final Reason reason = Reason.ofThrowable(throwable); if (isInState(targetState)) { - sendSessionTerminate(reason); + sendSessionTerminate(reason, throwable.getMessage()); } else { sendRetract(reason); } From 67f021426bc94699c3ce3b066a61c3a3babe40a1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Aug 2022 11:31:58 +0200 Subject: [PATCH 06/25] remove null bytes from strings before creating sql statements in backup --- .../conversations/services/ExportBackupService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index f8943489786a9d2b03857f0244a66c1eec0a88f0..6cbb26ad118eb97addf8cd7dfe47806bbf2163ee 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -15,6 +15,7 @@ import android.util.Log; import androidx.core.app.NotificationCompat; +import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import java.io.DataOutputStream; @@ -114,7 +115,7 @@ public class ExportBackupService extends Service { } builder.append(intValue); } else { - DatabaseUtils.appendEscapedSQLString(builder, value); + appendEscapedSQLString(builder, value); } } builder.append(")"); @@ -127,6 +128,10 @@ public class ExportBackupService extends Service { writer.append(builder.toString()); } + private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) { + DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString)); + } + private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) { final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null); while (cursor != null && cursor.moveToNext()) { @@ -201,7 +206,7 @@ public class ExportBackupService extends Service { } else if (value.matches("[0-9]+")) { builder.append(value); } else { - DatabaseUtils.appendEscapedSQLString(builder, value); + appendEscapedSQLString(builder, value); } } builder.append(")"); From 1f3743122fbd1f1188d6830955d103fc7a78edf2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Aug 2022 11:32:48 +0200 Subject: [PATCH 07/25] upgrade okhttp --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 900cead2dc499b1aa28fb518254761655083fcba..ddd1f46a1761444b6a6ec480b0b874cd659f78c1 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.9.3" + implementation "com.squareup.okhttp3:okhttp:4.10.0" implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' From 353c4f118d4ffbb93309a00b893535549e4607fe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 5 Aug 2022 10:45:44 +0200 Subject: [PATCH 08/25] use threemas webrtc build (trial) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ddd1f46a1761444b6a6ec480b0b874cd659f78c1..5409afedc8435918d6ee5663a74580a321559bc4 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs') + implementation 'ch.threema:webrtc-android:100.0.0' } ext { From d41020ccf3781fbfac45f8bac2249d9ac6ec9aed Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 5 Aug 2022 10:46:12 +0200 Subject: [PATCH 09/25] ignore race condition after reject from notification fixes #4351 fixes #4261 --- .../xmpp/jingle/JingleConnectionManager.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 416877236b4e7d3cb57a9d8a8fe2d6b0c886feb5..b39673fa5030bed6f9251114ed92be68744f0783 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -892,7 +892,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { for (final AbstractJingleConnection connection : this.connections.values()) { if (connection.getId().sessionId.equals(sessionId)) { if (connection instanceof JingleRtpConnection) { - ((JingleRtpConnection) connection).rejectCall(); + try { + ((JingleRtpConnection) connection).rejectCall(); + return; + } catch (final IllegalStateException e) { + Log.w( + Config.LOGTAG, + "race condition on rejecting call from notification", + e); + } } } } From 50ba165746972117e6fcb3a4554ef3456b3fbf4d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Jul 2022 12:34:02 +0200 Subject: [PATCH 10/25] bump targetSdk to 32 --- build.gradle | 4 +- src/conversations/AndroidManifest.xml | 3 +- src/main/AndroidManifest.xml | 16 +++- .../services/NotificationService.java | 96 ++++++++++++++----- .../services/XmppConnectionService.java | 17 +++- .../conversations/utils/Compatibility.java | 68 ++++++++----- 6 files changed, 142 insertions(+), 62 deletions(-) diff --git a/build.gradle b/build.gradle index 5409afedc8435918d6ee5663a74580a321559bc4..9e1366ce3e9b1ac1f96208fabe1bb82e3df02066 100644 --- a/build.gradle +++ b/build.gradle @@ -86,11 +86,11 @@ ext { android { namespace 'eu.siacs.conversations' - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 32 versionCode 42034 versionName "2.10.8" archivesBaseName += "-$versionName" diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index bf2297949f60c8fe268877ed4bca753a0b097169..d573b32d3dae88e55c506eeaf0d51ec065fca73c 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -26,7 +26,8 @@ + android:launchMode="singleTask" + android:exported="true"> diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 458c8a7f4b63897547ced729b89a4005b090631e..2a48da7a010653543fbaa7d85f2d1532fd90e6b2 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -83,7 +83,9 @@ - + @@ -107,6 +109,7 @@ android:label="@string/title_activity_show_location" /> @@ -127,6 +130,7 @@ android:windowSoftInputMode="stateAlwaysHidden" /> @@ -166,6 +170,7 @@ @@ -174,6 +179,7 @@ @@ -192,6 +198,7 @@ @@ -225,6 +232,7 @@ android:label="@string/group_chat_avatar" /> @@ -261,10 +269,6 @@ - - - - @@ -302,6 +307,7 @@ diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index abe6e9e11370833989844dd90bb23953c95c9e46..acac26cc091593340f98062e74600a83607b8adc 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -625,7 +627,9 @@ public class NotificationService { mXmppConnectionService, requestCode, fullScreenIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } public void cancelIncomingCallNotification() { @@ -759,7 +763,7 @@ public class NotificationService { && conversations != null && conversations.size() == 1; // if this check is changed to > 0 catchup messages will - // create one notification per conversation + // create one notification per conversation if (notifications.size() == 0) { cancel(NOTIFICATION_ID); @@ -835,9 +839,7 @@ public class NotificationService { } else { mBuilder.setLocalOnly(true); } - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setCategory(Notification.CATEGORY_MESSAGE); - } + mBuilder.setCategory(Notification.CATEGORY_MESSAGE); mBuilder.setPriority( notify ? (headsup @@ -1280,7 +1282,9 @@ public class NotificationService { mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } } return null; @@ -1299,13 +1303,17 @@ public class NotificationService { mXmppConnectionService, generateRequestCode(conversationUuid, 8), viewConversationIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } else { return PendingIntent.getActivity( mXmppConnectionService, generateRequestCode(conversationUuid, 10), viewConversationIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } } @@ -1332,9 +1340,20 @@ public class NotificationService { if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); return PendingIntent.getService( - mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0); + mXmppConnectionService, + generateRequestCode(conversation, 20), + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } - return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 0, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReplyIntent( @@ -1348,7 +1367,12 @@ public class NotificationService { intent.putExtra("last_message_uuid", lastMessageUuid); final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14); return PendingIntent.getService( - mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mXmppConnectionService, + id, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReadPendingIntent(Conversation conversation) { @@ -1360,7 +1384,9 @@ public class NotificationService { mXmppConnectionService, generateRequestCode(conversation, 16), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { @@ -1369,7 +1395,12 @@ public class NotificationService { intent.setPackage(mXmppConnectionService.getPackageName()); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); return PendingIntent.getService( - mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mXmppConnectionService, + requestCode, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createSnoozeIntent(Conversation conversation) { @@ -1381,19 +1412,33 @@ public class NotificationService { mXmppConnectionService, generateRequestCode(conversation, 22), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createTryAgainIntent() { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); - return PendingIntent.getService(mXmppConnectionService, 45, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 45, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createDismissErrorIntent() { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS); - return PendingIntent.getService(mXmppConnectionService, 69, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 69, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private boolean wasHighlightedOrPrivate(final Message message) { @@ -1538,15 +1583,9 @@ public class NotificationService { } } mBuilder.setDeleteIntent(createDismissErrorIntent()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); - mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); - } else { - mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - mBuilder.setLocalOnly(true); - } + mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); + mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); + mBuilder.setLocalOnly(true); mBuilder.setPriority(Notification.PRIORITY_LOW); final Intent intent; if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) { @@ -1558,7 +1597,12 @@ public class NotificationService { } mBuilder.setContentIntent( PendingIntent.getActivity( - mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + mXmppConnectionService, + 145, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); if (Compatibility.runsTwentySix()) { mBuilder.setChannelId("error"); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 7965a4e31ea2d75ef938668afe3e86394262d4ae..a6823e6702925d38b39642ffb0d1616c6910fac4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -36,6 +36,7 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.security.KeyChain; import android.telephony.PhoneStateListener; +import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -1203,9 +1204,10 @@ public class XmppConnectionService extends Service { private void setupPhoneStateListener() { final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return; } + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } public boolean isPhoneInCall() { @@ -1402,7 +1404,16 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction("ping"); try { - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0); + final PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingIntent = + PendingIntent.getBroadcast( + this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = + PendingIntent.getBroadcast( + this, requestCode, intent, 0); + } alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index aadeaabf57b6d98bd080f9462f3bd2e10d32938b..21004e26a9ca0447ffdd324f5f949b0bb41eda40 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; + import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; @@ -24,23 +26,26 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsFragment; -import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; - public class Compatibility { - private static final List UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList( - "led", - "notification_ringtone", - "notification_headsup", - "vibrate_on_notification" - ); - private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList( - "message_notification_settings" - ); - + private static final List UNUSED_SETTINGS_POST_TWENTYSIX = + Arrays.asList( + "led", + "notification_ringtone", + "notification_headsup", + "vibrate_on_notification"); + private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = + Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || ContextCompat.checkSelfPermission( + context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + public static boolean s() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; } private static boolean runsTwentyFour() { @@ -66,20 +71,22 @@ public class Compatibility { private static boolean targetsTwentySix(Context context) { try { final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); + final ApplicationInfo applicationInfo = + packageManager.getApplicationInfo(context.getPackageName(), 0); return applicationInfo == null || applicationInfo.targetSdkVersion >= 26; } catch (PackageManager.NameNotFoundException | RuntimeException e) { - return true; //when in doubt… + return true; // when in doubt… } } private static boolean targetsTwentyFour(Context context) { try { final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); + final ApplicationInfo applicationInfo = + packageManager.getApplicationInfo(context.getPackageName(), 0); return applicationInfo == null || applicationInfo.targetSdkVersion >= 24; } catch (PackageManager.NameNotFoundException | RuntimeException e) { - return true; //when in doubt… + return true; // when in doubt… } } @@ -92,14 +99,23 @@ public class Compatibility { } public static boolean keepForegroundService(Context context) { - return runsAndTargetsTwentySix(context) || getBooleanPreference(context, SettingsActivity.KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service); + return runsAndTargetsTwentySix(context) + || getBooleanPreference( + context, + SettingsActivity.KEEP_FOREGROUND_SERVICE, + R.bool.enable_foreground_service); } public static void removeUnusedPreferences(SettingsFragment settingsFragment) { - List categories = Arrays.asList( - (PreferenceCategory) settingsFragment.findPreference("notification_category"), - (PreferenceCategory) settingsFragment.findPreference("advanced")); - for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) { + List categories = + Arrays.asList( + (PreferenceCategory) + settingsFragment.findPreference("notification_category"), + (PreferenceCategory) settingsFragment.findPreference("advanced")); + for (String key : + (runsTwentySix() + ? UNUSED_SETTINGS_POST_TWENTYSIX + : UNUESD_SETTINGS_PRE_TWENTYSIX)) { Preference preference = settingsFragment.findPreference(key); if (preference != null) { for (PreferenceCategory category : categories) { @@ -111,7 +127,8 @@ public class Compatibility { } if (Compatibility.runsTwentySix()) { if (targetsTwentySix(settingsFragment.getContext())) { - Preference preference = settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE); + Preference preference = + settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE); if (preference != null) { for (PreferenceCategory category : categories) { if (category != null) { @@ -132,11 +149,12 @@ public class Compatibility { context.startService(intent); } } catch (RuntimeException e) { - Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service"); + Log.d( + Config.LOGTAG, + context.getClass().getSimpleName() + " was unable to start service"); } } - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") public static boolean hasFeatureCamera(final Context context) { final PackageManager packageManager = context.getPackageManager(); From 52ff6f446ce2e7f304b3489df1cc4fbea0cf2f10 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 15 Jul 2022 09:31:43 +0200 Subject: [PATCH 11/25] add permission checks to appRTCBluetoothManager --- src/main/AndroidManifest.xml | 1 + .../services/AppRTCBluetoothManager.java | 317 ++++++++++-------- 2 files changed, 172 insertions(+), 146 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 2a48da7a010653543fbaa7d85f2d1532fd90e6b2..8cb3758703c9064054fc4ccfc0bd7f9fac70ec5c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java index 862cdf0c7f007aec351550ddd26149dacd75a1b1..484072605174ab852628cf501086145d9a14dddf 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java @@ -9,6 +9,7 @@ */ package eu.siacs.conversations.services; +import android.Manifest; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -20,25 +21,25 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioManager; +import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.os.Process; import android.util.Log; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +import com.google.common.collect.ImmutableList; import org.webrtc.ThreadUtils; +import java.util.Collections; import java.util.List; -import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; -/** - * AppRTCProximitySensor manages functions related to Bluetoth devices in the - * AppRTC demo. - */ +/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */ public class AppRTCBluetoothManager { // Timeout interval for starting or stopping audio to a Bluetooth SCO device. private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; @@ -46,28 +47,26 @@ public class AppRTCBluetoothManager { private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; private final Context apprtcContext; private final AppRTCAudioManager apprtcAudioManager; - @Nullable - private final AudioManager audioManager; + @Nullable private final AudioManager audioManager; private final Handler handler; private final BluetoothProfile.ServiceListener bluetoothServiceListener; private final BroadcastReceiver bluetoothHeadsetReceiver; int scoConnectionAttempts; private State bluetoothState; - @Nullable - private BluetoothAdapter bluetoothAdapter; - @Nullable - private BluetoothHeadset bluetoothHeadset; - @Nullable - private BluetoothDevice bluetoothDevice; + @Nullable private BluetoothAdapter bluetoothAdapter; + @Nullable private BluetoothHeadset bluetoothHeadset; + @Nullable private BluetoothDevice bluetoothDevice; // Runs when the Bluetooth timeout expires. We use that timeout after calling // startScoAudio() or stopScoAudio() because we're not guaranteed to get a // callback after those calls. - private final Runnable bluetoothTimeoutRunnable = new Runnable() { - @Override - public void run() { - bluetoothTimeout(); - } - }; + private final Runnable bluetoothTimeoutRunnable = + new Runnable() { + @Override + public void run() { + bluetoothTimeout(); + } + }; + protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { Log.d(Config.LOGTAG, "ctor"); ThreadUtils.checkIsOnMainThread(); @@ -80,42 +79,29 @@ public class AppRTCBluetoothManager { handler = new Handler(Looper.getMainLooper()); } - /** - * Construction. - */ + /** Construction. */ static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo()); return new AppRTCBluetoothManager(context, audioManager); } - /** - * Returns the internal state. - */ + /** Returns the internal state. */ public State getState() { ThreadUtils.checkIsOnMainThread(); return bluetoothState; } /** - * Activates components required to detect Bluetooth devices and to enable - * BT SCO (audio is routed via BT SCO) for the headset profile. The end - * state will be HEADSET_UNAVAILABLE but a state machine has started which - * will start a state change sequence where the final outcome depends on - * if/when the BT headset is enabled. - * Example of state change sequence when start() is called while BT device - * is connected and enabled: - * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> - * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. - * Note that the AppRTCAudioManager is also involved in driving this state - * change. + * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is + * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a + * state machine has started which will start a state change sequence where the final outcome + * depends on if/when the BT headset is enabled. Example of state change sequence when start() + * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE --> + * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. + * Note that the AppRTCAudioManager is also involved in driving this state change. */ public void start() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "start"); - if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) { - Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission"); - return; - } if (bluetoothState != State.UNINITIALIZED) { Log.w(Config.LOGTAG, "Invalid BT state"); return; @@ -130,11 +116,10 @@ public class AppRTCBluetoothManager { return; } // Ensure that the device supports use of BT SCO audio for off call use cases. - if (!audioManager.isBluetoothScoAvailableOffCall()) { + if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) { Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call"); return; } - logBluetoothAdapterInfo(bluetoothAdapter); // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and // Hands-Free) proxy object and install a listener. if (!getBluetoothProfileProxy( @@ -149,16 +134,20 @@ public class AppRTCBluetoothManager { // Register receiver for change in audio connection state of the Headset profile. bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); - Log.d(Config.LOGTAG, "HEADSET profile state: " - + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); + if (hasBluetoothConnectPermission()) { + Log.d( + Config.LOGTAG, + "HEADSET profile state: " + + stateToString( + bluetoothAdapter.getProfileConnectionState( + BluetoothProfile.HEADSET))); + } Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started"); bluetoothState = State.HEADSET_UNAVAILABLE; Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState); } - /** - * Stops and closes all components related to Bluetooth audio. - */ + /** Stops and closes all components related to Bluetooth audio. */ public void stop() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState); @@ -184,23 +173,29 @@ public class AppRTCBluetoothManager { } /** - * Starts Bluetooth SCO connection with remote device. - * Note that the phone application always has the priority on the usage of the SCO connection - * for telephony. If this method is called while the phone is in call it will be ignored. - * Similarly, if a call is received or sent while an application is using the SCO connection, - * the connection will be lost for the application and NOT returned automatically when the call - * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a - * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO - * audio connection is established. + * Starts Bluetooth SCO connection with remote device. Note that the phone application always + * has the priority on the usage of the SCO connection for telephony. If this method is called + * while the phone is in call it will be ignored. Similarly, if a call is received or sent while + * an application is using the SCO connection, the connection will be lost for the application + * and NOT returned automatically when the call ends. Also note that: up to and including API + * version JELLY_BEAN_MR1, this method initiates a virtual voice call to the Bluetooth headset. + * After API version JELLY_BEAN_MR2 only a raw SCO audio connection is established. * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and * higher. It might be required to initiates a virtual voice call since many devices do not * accept SCO audio without a "call". */ public boolean startScoAudio() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", " - + "attempts: " + scoConnectionAttempts + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "startSco: BT state=" + + bluetoothState + + ", " + + "attempts: " + + scoConnectionAttempts + + ", " + + "SCO is on: " + + isScoOn()); if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts"); return false; @@ -213,24 +208,29 @@ public class AppRTCBluetoothManager { Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); // The SCO connection establishment can take several seconds, hence we cannot rely on the // connection to be available when the method returns but instead register to receive the - // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. + // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be + // SCO_AUDIO_STATE_CONNECTED. bluetoothState = State.SCO_CONNECTING; audioManager.startBluetoothSco(); audioManager.setBluetoothScoOn(true); scoConnectionAttempts++; startTimer(); - Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "startScoAudio done: BT state=" + + bluetoothState + + ", " + + "SCO is on: " + + isScoOn()); return true; } - /** - * Stops Bluetooth SCO connection with remote device. - */ + /** Stops Bluetooth SCO connection with remote device. */ public void stopScoAudio() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { return; } @@ -238,17 +238,18 @@ public class AppRTCBluetoothManager { audioManager.stopBluetoothSco(); audioManager.setBluetoothScoOn(false); bluetoothState = State.SCO_DISCONNECTING; - Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); } /** - * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset - * Service via IPC) to update the list of connected devices for the HEADSET - * profile. The internal state will change to HEADSET_UNAVAILABLE or to - * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected - * device if available. + * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to + * update the list of connected devices for the HEADSET profile. The internal state will change + * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the + * connected device if available. */ + @SuppressLint("MissingPermission") public void updateDevice() { if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { return; @@ -257,7 +258,12 @@ public class AppRTCBluetoothManager { // Get connected devices for the headset profile. Returns the set of // devices which are in state STATE_CONNECTED. The BluetoothDevice class // is just a thin wrapper for a Bluetooth hardware address. - List devices = bluetoothHeadset.getConnectedDevices(); + final List devices; + if (hasBluetoothConnectPermission()) { + devices = bluetoothHeadset.getConnectedDevices(); + } else { + devices = ImmutableList.of(); + } if (devices.isEmpty()) { bluetoothDevice = null; bluetoothState = State.HEADSET_UNAVAILABLE; @@ -266,17 +272,21 @@ public class AppRTCBluetoothManager { // Always use first device in list. Android only supports one device. bluetoothDevice = devices.get(0); bluetoothState = State.HEADSET_AVAILABLE; - Log.d(Config.LOGTAG, "Connected bluetooth headset: " - + "name=" + bluetoothDevice.getName() + ", " - + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) - + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + Log.d( + Config.LOGTAG, + "Connected bluetooth headset: " + + "name=" + + bluetoothDevice.getName() + + ", " + + "state=" + + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + + ", SCO audio=" + + bluetoothHeadset.isAudioConnected(bluetoothDevice)); } Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState); } - /** - * Stubs for test mocks. - */ + /** Stubs for test mocks. */ @Nullable protected AudioManager getAudioManager(Context context) { return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -295,52 +305,31 @@ public class AppRTCBluetoothManager { return bluetoothAdapter.getProfileProxy(context, listener, profile); } - protected boolean hasPermission(Context context, String permission) { - return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) - == PackageManager.PERMISSION_GRANTED; - } - - /** - * Logs the state of the local Bluetooth adapter. - */ - @SuppressLint("HardwareIds") - protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { - Log.d(Config.LOGTAG, "BluetoothAdapter: " - + "enabled=" + localAdapter.isEnabled() + ", " - + "state=" + stateToString(localAdapter.getState()) + ", " - + "name=" + localAdapter.getName() + ", " - + "address=" + localAdapter.getAddress()); - // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. - Set pairedDevices = localAdapter.getBondedDevices(); - if (!pairedDevices.isEmpty()) { - Log.d(Config.LOGTAG, "paired devices:"); - for (BluetoothDevice device : pairedDevices) { - Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress()); - } + protected boolean hasBluetoothConnectPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return ActivityCompat.checkSelfPermission( + apprtcContext, Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED; + } else { + return true; } } - /** - * Ensures that the audio manager updates its list of available audio devices. - */ + /** Ensures that the audio manager updates its list of available audio devices. */ private void updateAudioDeviceState() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "updateAudioDeviceState"); apprtcAudioManager.updateAudioDeviceState(); } - /** - * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. - */ + /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */ private void startTimer() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "startTimer"); handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); } - /** - * Cancels any outstanding timer tasks. - */ + /** Cancels any outstanding timer tasks. */ private void cancelTimer() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "cancelTimer"); @@ -348,23 +337,36 @@ public class AppRTCBluetoothManager { } /** - * Called when start of the BT SCO channel takes too long time. Usually - * happens when the BT device has been turned on during an ongoing call. + * Called when start of the BT SCO channel takes too long time. Usually happens when the BT + * device has been turned on during an ongoing call. */ + @SuppressLint("MissingPermission") private void bluetoothTimeout() { ThreadUtils.checkIsOnMainThread(); if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { return; } - Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " - + "attempts: " + scoConnectionAttempts + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "bluetoothTimeout: BT state=" + + bluetoothState + + ", " + + "attempts: " + + scoConnectionAttempts + + ", " + + "SCO is on: " + + isScoOn()); if (bluetoothState != State.SCO_CONNECTING) { return; } // Bluetooth SCO should be connecting; check the latest result. boolean scoConnected = false; - List devices = bluetoothHeadset.getConnectedDevices(); + final List devices; + if (hasBluetoothConnectPermission()) { + devices = bluetoothHeadset.getConnectedDevices(); + } else { + devices = Collections.emptyList(); + } if (devices.size() > 0) { bluetoothDevice = devices.get(0); if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { @@ -387,16 +389,12 @@ public class AppRTCBluetoothManager { Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState); } - /** - * Checks whether audio uses Bluetooth SCO. - */ + /** Checks whether audio uses Bluetooth SCO. */ private boolean isScoOn() { return audioManager.isBluetoothScoOn(); } - /** - * Converts BluetoothAdapter states into local string representations. - */ + /** Converts BluetoothAdapter states into local string representations. */ private String stateToString(int state) { switch (state) { case BluetoothAdapter.STATE_DISCONNECTED: @@ -412,11 +410,13 @@ public class AppRTCBluetoothManager { case BluetoothAdapter.STATE_ON: return "ON"; case BluetoothAdapter.STATE_TURNING_OFF: - // Indicates the local Bluetooth adapter is turning off. Local clients should immediately + // Indicates the local Bluetooth adapter is turning off. Local clients should + // immediately // attempt graceful disconnection of any remote links. return "TURNING_OFF"; case BluetoothAdapter.STATE_TURNING_ON: - // Indicates the local Bluetooth adapter is turning on. However local clients should wait + // Indicates the local Bluetooth adapter is turning on. However local clients should + // wait // for STATE_ON before attempting to use the adapter. return "TURNING_ON"; default: @@ -457,7 +457,9 @@ public class AppRTCBluetoothManager { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { return; } - Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); + Log.d( + Config.LOGTAG, + "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); // Android only supports one connected Bluetooth Headset at a time. bluetoothHeadset = (BluetoothHeadset) proxy; updateAudioDeviceState(); @@ -470,7 +472,9 @@ public class AppRTCBluetoothManager { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { return; } - Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + Log.d( + Config.LOGTAG, + "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); stopScoAudio(); bluetoothHeadset = null; bluetoothDevice = null; @@ -495,12 +499,20 @@ public class AppRTCBluetoothManager { // headset while audio is active using another audio device. if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { final int state = - intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); - Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_CONNECTION_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); + Log.d( + Config.LOGTAG, + "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_CONNECTION_STATE_CHANGED, " + + "s=" + + stateToString(state) + + ", " + + "sb=" + + isInitialStickyBroadcast() + + ", " + + "BT state: " + + bluetoothState); if (state == BluetoothHeadset.STATE_CONNECTED) { scoConnectionAttempts = 0; updateAudioDeviceState(); @@ -516,13 +528,22 @@ public class AppRTCBluetoothManager { // Change in the audio (SCO) connection state of the Headset profile. // Typically received after call to startScoAudio() has finalized. } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { - final int state = intent.getIntExtra( - BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); - Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_AUDIO_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + final int state = + intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, + BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + Log.d( + Config.LOGTAG, + "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_AUDIO_STATE_CHANGED, " + + "s=" + + stateToString(state) + + ", " + + "sb=" + + isInitialStickyBroadcast() + + ", " + + "BT state: " + + bluetoothState); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { cancelTimer(); if (bluetoothState == State.SCO_CONNECTING) { @@ -531,14 +552,18 @@ public class AppRTCBluetoothManager { scoConnectionAttempts = 0; updateAudioDeviceState(); } else { - Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); + Log.w( + Config.LOGTAG, + "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); } } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting..."); } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected"); if (isInitialStickyBroadcast()) { - Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); + Log.d( + Config.LOGTAG, + "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); return; } updateAudioDeviceState(); @@ -547,4 +572,4 @@ public class AppRTCBluetoothManager { Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState); } } -} \ No newline at end of file +} From 5aeed638444dad5d710350900e159e0dca9f5503 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 8 Aug 2022 21:08:28 +0200 Subject: [PATCH 12/25] request bluetooth connect permission fixes #4338 --- .../ui/ConversationFragment.java | 1962 +++++++++++------ .../conversations/ui/RtpSessionActivity.java | 19 +- .../conversations/utils/PermissionUtils.java | 39 +- 3 files changed, 1295 insertions(+), 725 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 2eb602257c4e99c6dad0ae4bf01109a6ec4a9e61..0471c014f3b570fd8ff6f1bea2bb077d13d4b25e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,12 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; +import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; +import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; @@ -55,6 +62,9 @@ import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.databinding.DataBindingUtil; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; + +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; @@ -114,6 +124,7 @@ import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.TimeFrameUtils; @@ -129,18 +140,10 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; -import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; - -import org.jetbrains.annotations.NotNull; - - -public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked { - +public class ConversationFragment extends XmppFragment + implements EditMessage.KeyboardListener, + MessageAdapter.OnContactPictureLongClicked, + MessageAdapter.OnContactPictureClicked { public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; @@ -161,10 +164,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307; public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; - public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid"; - public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; - public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews"; - public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri"; + public static final String STATE_CONVERSATION_UUID = + ConversationFragment.class.getName() + ".uuid"; + public static final String STATE_SCROLL_POSITION = + ConversationFragment.class.getName() + ".scroll_position"; + public static final String STATE_PHOTO_URI = + ConversationFragment.class.getName() + ".media_previews"; + public static final String STATE_MEDIA_PREVIEWS = + ConversationFragment.class.getName() + ".take_photo_uri"; private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid"; private final List messageList = new ArrayList<>(); @@ -185,282 +192,376 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private Toast messageLoaderToast; private ConversationsActivity activity; private boolean reInitRequiredOnStart = true; - private final OnClickListener clickToMuc = new OnClickListener() { + private final OnClickListener clickToMuc = + new OnClickListener() { - @Override - public void onClick(View v) { - ConferenceDetailsActivity.open(getActivity(), conversation); - } - }; - private final OnClickListener leaveMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.archiveConversation(conversation); - } - }; - private final OnClickListener joinMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.joinMuc(conversation); - } - }; - - private final OnClickListener acceptJoin = new OnClickListener() { - @Override - public void onClick(View v) { - conversation.setAttribute("accept_non_anonymous", true); - activity.xmppConnectionService.updateConversation(conversation); - activity.xmppConnectionService.joinMuc(conversation); - } - }; + @Override + public void onClick(View v) { + ConferenceDetailsActivity.open(getActivity(), conversation); + } + }; + private final OnClickListener leaveMuc = + new OnClickListener() { - private final OnClickListener enterPassword = new OnClickListener() { + @Override + public void onClick(View v) { + activity.xmppConnectionService.archiveConversation(conversation); + } + }; + private final OnClickListener joinMuc = + new OnClickListener() { - @Override - public void onClick(View v) { - MucOptions muc = conversation.getMucOptions(); - String password = muc.getPassword(); - if (password == null) { - password = ""; - } - activity.quickPasswordEdit(password, value -> { - activity.xmppConnectionService.providePasswordForMuc(conversation, value); - return null; - }); - } - }; - private final OnScrollListener mOnScrollListener = new OnScrollListener() { + @Override + public void onClick(View v) { + activity.xmppConnectionService.joinMuc(conversation); + } + }; + + private final OnClickListener acceptJoin = + new OnClickListener() { + @Override + public void onClick(View v) { + conversation.setAttribute("accept_non_anonymous", true); + activity.xmppConnectionService.updateConversation(conversation); + activity.xmppConnectionService.joinMuc(conversation); + } + }; - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { - fireReadEvent(); - } - } + private final OnClickListener enterPassword = + new OnClickListener() { - @Override - public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - toggleScrollDownButton(view); - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) { - long timestamp; - if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { - timestamp = messageList.get(1).getTimeSent(); - } else { - timestamp = messageList.get(0).getTimeSent(); + @Override + public void onClick(View v) { + MucOptions muc = conversation.getMucOptions(); + String password = muc.getPassword(); + if (password == null) { + password = ""; } - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int c, final Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - conversation.messagesLoaded.set(true); - return; - } - runOnUiThread(() -> { - synchronized (messageList) { - final int oldPosition = binding.messagesView.getFirstVisiblePosition(); - Message message = null; - int childPos; - for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) { - message = messageList.get(oldPosition + childPos); - if (message.getType() != Message.TYPE_STATUS) { - break; - } - } - final String uuid = message != null ? message.getUuid() : null; - View v = binding.messagesView.getChildAt(childPos); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - try { - updateStatusMessages(); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages"); - } - messageListAdapter.notifyDataSetChanged(); - int pos = Math.max(getIndexOf(uuid, messageList), 0); - binding.messagesView.setSelectionFromTop(pos, pxOffset); - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - conversation.messagesLoaded.set(true); - } + activity.quickPasswordEdit( + password, + value -> { + activity.xmppConnectionService.providePasswordForMuc( + conversation, value); + return null; }); - } - - @Override - public void informUser(final int resId) { + } + }; + private final OnScrollListener mOnScrollListener = + new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { + fireReadEvent(); + } + } - runOnUiThread(() -> { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG); - messageLoaderToast.show(); - }); + @Override + public void onScroll( + final AbsListView view, + int firstVisibleItem, + int visibleItemCount, + int totalItemCount) { + toggleScrollDownButton(view); + synchronized (ConversationFragment.this.messageList) { + if (firstVisibleItem < 5 + && conversation != null + && conversation.messagesLoaded.compareAndSet(true, false) + && messageList.size() > 0) { + long timestamp; + if (messageList.get(0).getType() == Message.TYPE_STATUS + && messageList.size() >= 2) { + timestamp = messageList.get(1).getTimeSent(); + } else { + timestamp = messageList.get(0).getTimeSent(); + } + activity.xmppConnectionService.loadMoreMessages( + conversation, + timestamp, + new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded( + final int c, final Conversation conversation) { + if (ConversationFragment.this.conversation + != conversation) { + conversation.messagesLoaded.set(true); + return; + } + runOnUiThread( + () -> { + synchronized (messageList) { + final int oldPosition = + binding.messagesView + .getFirstVisiblePosition(); + Message message = null; + int childPos; + for (childPos = 0; + childPos + oldPosition + < messageList.size(); + ++childPos) { + message = + messageList.get( + oldPosition + + childPos); + if (message.getType() + != Message.TYPE_STATUS) { + break; + } + } + final String uuid = + message != null + ? message.getUuid() + : null; + View v = + binding.messagesView.getChildAt( + childPos); + final int pxOffset = + (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation + .populateWithMessages( + ConversationFragment + .this + .messageList); + try { + updateStatusMessages(); + } catch (IllegalStateException e) { + Log.d( + Config.LOGTAG, + "caught illegal state exception while updating status messages"); + } + messageListAdapter + .notifyDataSetChanged(); + int pos = + Math.max( + getIndexOf( + uuid, + messageList), + 0); + binding.messagesView + .setSelectionFromTop( + pos, pxOffset); + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + conversation.messagesLoaded.set(true); + } + }); + } + @Override + public void informUser(final int resId) { + + runOnUiThread( + () -> { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + if (ConversationFragment.this.conversation + != conversation) { + return; + } + messageLoaderToast = + Toast.makeText( + view.getContext(), + resId, + Toast.LENGTH_LONG); + messageLoaderToast.show(); + }); + } + }); } - }); - + } } - } - } - }; - private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { - @Override - public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) { - // try to get permission to read the image, if applicable - if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { - try { - inputContentInfo.requestPermission(); - } catch (Exception e) { - Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e); - Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG - ).show(); - return false; + }; + private final EditMessage.OnCommitContentListener mEditorContentListener = + new EditMessage.OnCommitContentListener() { + @Override + public boolean onCommitContent( + InputContentInfoCompat inputContentInfo, + int flags, + Bundle opts, + String[] contentMimeTypes) { + // try to get permission to read the image, if applicable + if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) + != 0) { + try { + inputContentInfo.requestPermission(); + } catch (Exception e) { + Log.e( + Config.LOGTAG, + "InputContentInfoCompat#requestPermission() failed.", + e); + Toast.makeText( + getActivity(), + activity.getString( + R.string.no_permission_to_access_x, + inputContentInfo.getDescription()), + Toast.LENGTH_LONG) + .show(); + return false; + } + } + if (hasPermissions( + REQUEST_ADD_EDITOR_CONTENT, + Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + attachEditorContentToConversation(inputContentInfo.getContentUri()); + } else { + mPendingEditorContent = inputContentInfo.getContentUri(); + } + return true; } - } - if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - attachEditorContentToConversation(inputContentInfo.getContentUri()); - } else { - mPendingEditorContent = inputContentInfo.getContentUri(); - } - return true; - } - }; + }; private Message selectedMessage; - private final OnClickListener mEnableAccountListener = new OnClickListener() { - @Override - public void onClick(View v) { - final Account account = conversation == null ? null : conversation.getAccount(); - if (account != null) { - account.setOption(Account.OPTION_DISABLED, false); - activity.xmppConnectionService.updateAccount(account); - } - } - }; - private final OnClickListener mUnblockClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - v.post(() -> v.setVisibility(View.INVISIBLE)); - if (conversation.isDomainBlocked()) { - BlockContactDialog.show(activity, conversation); - } else { - unblockConversation(conversation); - } - } - }; + private final OnClickListener mEnableAccountListener = + new OnClickListener() { + @Override + public void onClick(View v) { + final Account account = conversation == null ? null : conversation.getAccount(); + if (account != null) { + account.setOption(Account.OPTION_DISABLED, false); + activity.xmppConnectionService.updateAccount(account); + } + } + }; + private final OnClickListener mUnblockClickListener = + new OnClickListener() { + @Override + public void onClick(final View v) { + v.post(() -> v.setVisibility(View.INVISIBLE)); + if (conversation.isDomainBlocked()) { + BlockContactDialog.show(activity, conversation); + } else { + unblockConversation(conversation); + } + } + }; private final OnClickListener mBlockClickListener = this::showBlockSubmenu; - private final OnClickListener mAddBackClickListener = new OnClickListener() { - - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.createContact(contact, true); - activity.switchToContactDetails(contact); - } - } - }; + private final OnClickListener mAddBackClickListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null : conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.createContact(contact, true); + activity.switchToContactDetails(contact); + } + } + }; private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; - private final OnClickListener mAllowPresenceSubscription = new OnClickListener() { - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.sendPresencePacket(contact.getAccount(), - activity.xmppConnectionService.getPresenceGenerator() - .sendPresenceUpdatesTo(contact)); - hideSnackbar(); - } - } - }; - protected OnClickListener clickToDecryptListener = new OnClickListener() { - - @Override - public void onClick(View v) { - PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent(); - if (pendingIntent != null) { - try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), - REQUEST_DECRYPT_PGP, - null, - 0, - 0, - 0); - } catch (SendIntentException e) { - Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show(); - conversation.getAccount().getPgpDecryptionService().continueDecryption(true); + private final OnClickListener mAllowPresenceSubscription = + new OnClickListener() { + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null : conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.sendPresencePacket( + contact.getAccount(), + activity.xmppConnectionService + .getPresenceGenerator() + .sendPresenceUpdatesTo(contact)); + hideSnackbar(); + } } - } - updateSnackBar(conversation); - } - }; + }; + protected OnClickListener clickToDecryptListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + PendingIntent pendingIntent = + conversation.getAccount().getPgpDecryptionService().getPendingIntent(); + if (pendingIntent != null) { + try { + getActivity() + .startIntentSenderForResult( + pendingIntent.getIntentSender(), + REQUEST_DECRYPT_PGP, + null, + 0, + 0, + 0); + } catch (SendIntentException e) { + Toast.makeText( + getActivity(), + R.string.unable_to_connect_to_keychain, + Toast.LENGTH_SHORT) + .show(); + conversation + .getAccount() + .getPgpDecryptionService() + .continueDecryption(true); + } + } + updateSnackBar(conversation); + } + }; private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); - private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEND) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null && imm.isFullscreenMode()) { - imm.hideSoftInputFromWindow(v.getWindowToken(), 0); - } - sendMessage(); - return true; - } else { - return false; - } - }; - private final OnClickListener mScrollButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - stopScrolling(); - setSelection(binding.messagesView.getCount() - 1, true); - } - }; - private final OnClickListener mSendButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - Object tag = v.getTag(); - if (tag instanceof SendButtonAction) { - SendButtonAction action = (SendButtonAction) tag; - switch (action) { - case TAKE_PHOTO: - case RECORD_VIDEO: - case SEND_LOCATION: - case RECORD_VOICE: - case CHOOSE_PICTURE: - attachFile(action.toChoice()); - break; - case CANCEL: - if (conversation != null) { - if (conversation.setCorrectingMessage(null)) { - binding.textinput.setText(""); - binding.textinput.append(conversation.getDraftMessage()); - conversation.setDraftMessage(null); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - binding.textinput.setText(""); - } else { - binding.textinput.setText(""); - } - updateChatMsgHint(); - updateSendButton(); - updateEditablity(); + private final OnEditorActionListener mEditorActionListener = + (v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND) { + InputMethodManager imm = + (InputMethodManager) + activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && imm.isFullscreenMode()) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + sendMessage(); + return true; + } else { + return false; + } + }; + private final OnClickListener mScrollButtonListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + stopScrolling(); + setSelection(binding.messagesView.getCount() - 1, true); + } + }; + private final OnClickListener mSendButtonListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + Object tag = v.getTag(); + if (tag instanceof SendButtonAction) { + SendButtonAction action = (SendButtonAction) tag; + switch (action) { + case TAKE_PHOTO: + case RECORD_VIDEO: + case SEND_LOCATION: + case RECORD_VOICE: + case CHOOSE_PICTURE: + attachFile(action.toChoice()); + break; + case CANCEL: + if (conversation != null) { + if (conversation.setCorrectingMessage(null)) { + binding.textinput.setText(""); + binding.textinput.append(conversation.getDraftMessage()); + conversation.setDraftMessage(null); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); + binding.textinput.setText(""); + } else { + binding.textinput.setText(""); + } + updateChatMsgHint(); + updateSendButton(); + updateEditablity(); + } + break; + default: + sendMessage(); } - break; - default: + } else { sendMessage(); + } } - } else { - sendMessage(); - } - } - }; + }; private int completionIndex = 0; private int lastCompletionLength = 0; private String incomplete; @@ -531,7 +632,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return (ConversationFragment) fragment; } else { fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); - return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; + return fragment instanceof ConversationFragment + ? (ConversationFragment) fragment + : null; } } @@ -593,7 +696,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } next = next.next(); } - } } return -1; @@ -601,7 +703,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private ScrollState getScrollPosition() { final ListView listView = this.binding == null ? null : this.binding.messagesView; - if (listView == null || listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) { + if (listView == null + || listView.getCount() == 0 + || listView.getLastVisiblePosition() == listView.getCount() - 1) { return null; } else { final int pos = listView.getFirstVisiblePosition(); @@ -619,10 +723,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.lastMessageUuid = lastMessageUuid; if (lastMessageUuid != null) { - binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); + binding.unreadCountCustomView.setUnreadCount( + conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); } - //TODO maybe this needs a 'post' - this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset); + // TODO maybe this needs a 'post' + this.binding.messagesView.setSelectionFromTop( + scrollPosition.position, scrollPosition.offset); toggleScrollDownButton(); } } @@ -631,61 +737,65 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (conversation == null) { return; } - activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback() { - - @Override - public void success(Message message) { - - } + activity.xmppConnectionService.attachLocationToConversation( + conversation, + uri, + new UiCallback() { - @Override - public void error(int errorCode, Message object) { - //TODO show possible pgp error - } + @Override + public void success(Message message) {} - @Override - public void userInputRequired(PendingIntent pi, Message object) { + @Override + public void error(int errorCode, Message object) { + // TODO show possible pgp error + } - } - }); + @Override + public void userInputRequired(PendingIntent pi, Message object) {} + }); } private void attachFileToConversation(Conversation conversation, Uri uri, String type) { if (conversation == null) { return; } - final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG); + final Toast prepareFileToast = + Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachFileToConversation(conversation, uri, type, new UiInformableCallback() { - @Override - public void inform(final String text) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(text)); - } - - @Override - public void success(Message message) { - runOnUiThread(() -> activity.hideToast()); - hidePrepareFileToast(prepareFileToast); - } + activity.xmppConnectionService.attachFileToConversation( + conversation, + uri, + type, + new UiInformableCallback() { + @Override + public void inform(final String text) { + hidePrepareFileToast(prepareFileToast); + runOnUiThread(() -> activity.replaceToast(text)); + } - @Override - public void error(final int errorCode, Message message) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(getString(errorCode))); + @Override + public void success(Message message) { + runOnUiThread(() -> activity.hideToast()); + hidePrepareFileToast(prepareFileToast); + } - } + @Override + public void error(final int errorCode, Message message) { + hidePrepareFileToast(prepareFileToast); + runOnUiThread(() -> activity.replaceToast(getString(errorCode))); + } - @Override - public void userInputRequired(PendingIntent pi, Message message) { - hidePrepareFileToast(prepareFileToast); - } - }); + @Override + public void userInputRequired(PendingIntent pi, Message message) { + hidePrepareFileToast(prepareFileToast); + } + }); } public void attachEditorContentToConversation(Uri uri) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), uri, Attachment.Type.FILE)); toggleInputMethod(); } @@ -693,10 +803,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (conversation == null) { return; } - final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); + final Toast prepareFileToast = + Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, + activity.xmppConnectionService.attachImageToConversation( + conversation, + uri, + type, new UiCallback() { @Override @@ -762,19 +876,31 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) { - return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode); + return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL + && trustKeysIfNeeded(requestCode); } protected boolean trustKeysIfNeeded(int requestCode) { AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); final List targets = axolotlService.getCryptoTargets(conversation); boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets); - boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty(); - boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty(); + boolean hasUndecidedOwn = + !axolotlService + .getKeysWithTrust(FingerprintStatus.createActiveUndecided()) + .isEmpty(); + boolean hasUndecidedContacts = + !axolotlService + .getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets) + .isEmpty(); boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty(); boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets); boolean downloadInProgress = axolotlService.hasPendingKeyFetches(targets); - if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted || downloadInProgress) { + if (hasUndecidedOwn + || hasUndecidedContacts + || hasPendingKeys + || hasNoTrustedKeys + || hasUnaccepted + || downloadInProgress) { axolotlService.createSessionsIfNeeded(conversation); Intent intent = new Intent(getActivity(), TrustKeysActivity.class); String[] contacts = new String[targets.size()]; @@ -782,7 +908,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke contacts[i] = targets.get(i).toString(); } intent.putExtra("contacts", contacts); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra( + EXTRA_ACCOUNT, + conversation.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra("conversation", conversation.getUuid()); startActivityForResult(intent, requestCode); return true; @@ -799,9 +927,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else if (multi && conversation.getNextCounterpart() != null) { this.binding.textinput.setHint(R.string.send_unencrypted_message); this.binding.textInputHint.setVisibility(View.VISIBLE); - this.binding.textInputHint.setText(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResource())); + this.binding.textInputHint.setText( + getString( + R.string.send_private_message_to, + conversation.getNextCounterpart().getResource())); } else if (multi && !conversation.getMucOptions().participating()) { this.binding.textInputHint.setVisibility(View.GONE); this.binding.textinput.setHint(R.string.you_are_not_participating); @@ -839,14 +968,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - final List imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); + final List imageUris = + Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); mediaPreviewAdapter.addMediaPreviews(imageUris); toggleInputMethod(); break; case ATTACHMENT_CHOICE_TAKE_PHOTO: final Uri takePhotoUri = pendingTakePhotoUri.pop(); if (takePhotoUri != null) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); toggleInputMethod(); } else { Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); @@ -855,8 +986,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case ATTACHMENT_CHOICE_CHOOSE_FILE: case ATTACHMENT_CHOICE_RECORD_VIDEO: case ATTACHMENT_CHOICE_RECORD_VOICE: - final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE; - final List fileUris = Attachment.extractAttachments(getActivity(), data, type); + final Attachment.Type type = + requestCode == ATTACHMENT_CHOICE_RECORD_VOICE + ? Attachment.Type.RECORDING + : Attachment.Type.FILE; + final List fileUris = + Attachment.extractAttachments(getActivity(), data, type); mediaPreviewAdapter.addMediaPreviews(fileUris); toggleInputMethod(); break; @@ -870,14 +1005,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude)); } - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); toggleInputMethod(); break; case REQUEST_INVITE_TO_CONVERSATION: XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data); if (invite != null) { if (invite.execute(activity)) { - activity.mToast = Toast.makeText(activity, R.string.creating_conference, Toast.LENGTH_LONG); + activity.mToast = + Toast.makeText( + activity, R.string.creating_conference, Toast.LENGTH_LONG); activity.mToast.show(); } } @@ -887,40 +1025,51 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void commitAttachments() { final List attachments = mediaPreviewAdapter.getAttachments(); - if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (anyNeedsExternalStoragePermission(attachments) + && !hasPermissions( + REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { return; } if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) { return; } - final PresenceSelector.OnPresenceSelected callback = () -> { - for (Iterator i = attachments.iterator(); i.hasNext(); i.remove()) { - final Attachment attachment = i.next(); - if (attachment.getType() == Attachment.Type.LOCATION) { - attachLocationToConversation(conversation, attachment.getUri()); - } else if (attachment.getType() == Attachment.Type.IMAGE) { - Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, attachment.getUri(), attachment.getMime()); - } else { - Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); - attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); - } - } - mediaPreviewAdapter.notifyDataSetChanged(); - toggleInputMethod(); - }; + final PresenceSelector.OnPresenceSelected callback = + () -> { + for (Iterator i = attachments.iterator(); i.hasNext(); i.remove()) { + final Attachment attachment = i.next(); + if (attachment.getType() == Attachment.Type.LOCATION) { + attachLocationToConversation(conversation, attachment.getUri()); + } else if (attachment.getType() == Attachment.Type.IMAGE) { + Log.d( + Config.LOGTAG, + "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); + attachImageToConversation( + conversation, attachment.getUri(), attachment.getMime()); + } else { + Log.d( + Config.LOGTAG, + "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); + attachFileToConversation( + conversation, attachment.getUri(), attachment.getMime()); + } + } + mediaPreviewAdapter.notifyDataSetChanged(); + toggleInputMethod(); + }; if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || Attachment.canBeSendInband(attachments) - || (conversation.getAccount().httpUploadAvailable() && FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation)))) { + || (conversation.getAccount().httpUploadAvailable() + && FileBackend.allFilesUnderSize( + getActivity(), attachments, getMaxHttpUploadSize(conversation)))) { callback.onPresenceSelected(); } else { activity.selectPresence(conversation, callback); } } - - private static boolean anyNeedsExternalStoragePermission(final Collection attachments) { + private static boolean anyNeedsExternalStoragePermission( + final Collection attachments) { for (final Attachment attachment : attachments) { if (attachment.getType() != Attachment.Type.LOCATION) { return true; @@ -940,7 +1089,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke switch (requestCode) { case ATTACHMENT_CHOICE_TAKE_PHOTO: if (pendingTakePhotoUri.clear()) { - Log.d(Config.LOGTAG, "cleared pending photo uri after negative activity result"); + Log.d( + Config.LOGTAG, + "cleared pending photo uri after negative activity result"); } break; } @@ -968,14 +1119,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (activity instanceof ConversationsActivity) { this.activity = (ConversationsActivity) activity; } else { - throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationsActivity"); + throw new IllegalStateException( + "Trying to attach fragment to activity that is not the ConversationsActivity"); } } @Override public void onDetach() { super.onDetach(); - this.activity = null; //TODO maybe not a good idea since some callbacks really need it + this.activity = null; // TODO maybe not a good idea since some callbacks really need it } @Override @@ -997,30 +1149,42 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); final MenuItem menuTogglePinned = menu.findItem(R.id.action_toggle_pinned); - if (conversation != null) { if (conversation.getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); - menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); + menuMucDetails.setTitle( + conversation.getMucOptions().isPrivateAndNonAnonymous() + ? R.string.action_muc_details + : R.string.channel_details); menuCall.setVisible(false); menuOngoingCall.setVisible(false); } else { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final Optional ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; + final Optional ongoingRtpSession = + service == null + ? Optional.absent() + : service.getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); if (ongoingRtpSession.isPresent()) { menuOngoingCall.setVisible(true); menuCall.setVisible(false); } else { menuOngoingCall.setVisible(false); - final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); - final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable(); + final RtpCapability.Capability rtpCapability = + RtpCapability.check(conversation.getContact()); + final boolean cameraAvailable = + activity != null && activity.isCameraFeatureAvailable(); menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); + menuVideoCall.setVisible( + rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); - menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null); + menuInviteContact.setVisible( + service != null + && service.findConferenceServer(conversation.getAccount()) != null); } if (conversation.isMuted()) { menuMute.setVisible(false); @@ -1039,14 +1203,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); - binding.getRoot().setOnClickListener(null); //TODO why the fuck did we do this? + public View onCreateView( + final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + this.binding = + DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); + binding.getRoot().setOnClickListener(null); // TODO why the fuck did we do this? - binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput)); + binding.textinput.addTextChangedListener( + new StylingHelper.MessageEditorStyler(binding.textinput)); binding.textinput.setOnEditorActionListener(mEditorActionListener); - binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener); + binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener); binding.textSendButton.setOnClickListener(this.mSendButtonListener); @@ -1063,7 +1230,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke registerForContextMenu(binding.messagesView); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - this.binding.textinput.setCustomInsertionActionModeCallback(new EditMessageActionModeCallback(this.binding.textinput)); + this.binding.textinput.setCustomInsertionActionModeCallback( + new EditMessageActionModeCallback(this.binding.textinput)); } return binding.getRoot(); @@ -1081,9 +1249,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (binding.textinput.isEnabled()) { binding.textinput.insertAsQuote(text); binding.textinput.requestFocus(); - InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager inputMethodManager = + (InputMethodManager) + getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { - inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT); + inputMethodManager.showSoftInput( + binding.textinput, InputMethodManager.SHOW_IMPLICIT); } } } @@ -1094,7 +1265,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - //This should cancel any remaining click events that would otherwise trigger links + // This should cancel any remaining click events that would otherwise trigger links v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); synchronized (this.messageList) { super.onCreateContextMenu(menu, v, menuInfo); @@ -1113,18 +1284,26 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { - if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { return; } - if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) { + if (m.getStatus() == Message.STATUS_RECEIVED + && t != null + && (t.getStatus() == Transferable.STATUS_CANCELLED + || t.getStatus() == Transferable.STATUS_FAILED)) { return; } final boolean deleted = m.isDeleted(); - final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED - || m.getEncryption() == Message.ENCRYPTION_PGP; - final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleFileTransferConnection || t instanceof HttpDownloadConnection); + final boolean encrypted = + m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED + || m.getEncryption() == Message.ENCRYPTION_PGP; + final boolean receiving = + m.getStatus() == Message.STATUS_RECEIVED + && (t instanceof JingleFileTransferConnection + || t instanceof HttpDownloadConnection); activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); MenuItem openWith = menu.findItem(R.id.open_with); @@ -1141,8 +1320,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke MenuItem deleteFile = menu.findItem(R.id.delete_file); MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m); - final boolean showError = m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); - if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable() && !unInitiatedButKnownSize && t == null) { + final boolean showError = + m.getStatus() == Message.STATUS_SEND_FAILED + && m.getErrorMessage() != null + && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); + if (!m.isFileOrImage() + && !encrypted + && !m.isGeoUri() + && !m.treatAsDownloadable() + && !unInitiatedButKnownSize + && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && MessageUtils.prepareQuote(m).length() > 0); String body = m.getMergedBody().toString(); @@ -1163,7 +1350,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke && m.getConversation() instanceof Conversation) { correctMessage.setVisible(true); } - if ((m.isFileOrImage() && !deleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) && !unInitiatedButKnownSize && t == null) { + if ((m.isFileOrImage() && !deleted && !receiving) + || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) + && !unInitiatedButKnownSize + && t == null) { shareWith.setVisible(true); } if (m.getStatus() == Message.STATUS_SEND_FAILED) { @@ -1178,27 +1368,38 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (m.isFileOrImage() && deleted && m.hasFileOnRemoteHost()) { downloadFile.setVisible(true); - downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m))); - } - final boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_UNSEND - || m.getStatus() == Message.STATUS_OFFERED; - final boolean cancelable = (t != null && !deleted) || waitingOfferedSending && m.needsUploading(); + downloadFile.setTitle( + activity.getString( + R.string.download_x_file, + UIHelper.getFileDescriptionString(activity, m))); + } + final boolean waitingOfferedSending = + m.getStatus() == Message.STATUS_WAITING + || m.getStatus() == Message.STATUS_UNSEND + || m.getStatus() == Message.STATUS_OFFERED; + final boolean cancelable = + (t != null && !deleted) || waitingOfferedSending && m.needsUploading(); if (cancelable) { cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { final String path = m.getRelativeFilePath(); - if (path == null || !path.startsWith("/") || FileBackend.inConversationsDirectory(requireActivity(), path)) { + if (path == null + || !path.startsWith("/") + || FileBackend.inConversationsDirectory(requireActivity(), path)) { deleteFile.setVisible(true); - deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); + deleteFile.setTitle( + activity.getString( + R.string.delete_x_file, + UIHelper.getFileDescriptionString(activity, m))); } } if (showError) { showErrorMessage.setVisible(true); } final String mime = m.isFileOrImage() ? m.getMimeType() : null; - if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) || (mime != null && mime.startsWith("audio/"))) { + if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) + || (mime != null && mime.startsWith("audio/"))) { openWith.setVisible(true); } } @@ -1285,7 +1486,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke ConferenceDetailsActivity.open(getActivity(), conversation); break; case R.id.action_invite: - startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION); + startActivityForResult( + ChooseContactActivity.create(activity, conversation), + REQUEST_INVITE_TO_CONVERSATION); break; case R.id.action_clear_history: clearHistoryDialog(conversation); @@ -1294,7 +1497,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke muteConversationDialog(conversation); break; case R.id.action_unmute: - unmuteConversation(conversation); + unMuteConversation(conversation); break; case R.id.action_block: case R.id.action_unblock: @@ -1328,11 +1531,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void returnToOngoingCall() { - final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + final Optional ongoingRtpSession = + activity.xmppConnectionService + .getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); if (ongoingRtpSession.isPresent()) { final OngoingRtpSession id = ongoingRtpSession.get(); final Intent intent = new Intent(getActivity(), RtpSessionActivity.class); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra( + RtpSessionActivity.EXTRA_ACCOUNT, + id.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); if (id instanceof AbstractJingleConnection.Id) { intent.setAction(Intent.ACTION_VIEW); @@ -1346,11 +1554,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } startActivity(intent); } - } private void togglePinned() { - final boolean pinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); + final boolean pinned = + conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); conversation.setAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, !pinned); activity.xmppConnectionService.updateConversation(conversation); activity.invalidateOptionsMenu(); @@ -1361,7 +1569,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } - if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { + final List permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions = + Arrays.asList( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.BLUETOOTH_CONNECT); + } else { + permissions = Collections.singletonList(Manifest.permission.RECORD_AUDIO); + } + if (hasPermissions(REQUEST_START_AUDIO_CALL, permissions)) { triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); } } @@ -1371,15 +1588,26 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } - if (hasPermissions(REQUEST_START_VIDEO_CALL, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) { + final List permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions = + Arrays.asList( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA, + Manifest.permission.BLUETOOTH_CONNECT); + } else { + permissions = + Arrays.asList(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA); + } + if (hasPermissions(REQUEST_START_VIDEO_CALL, permissions)) { triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } } - private void triggerRtpSession(final String action) { if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { - Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG) + .show(); return; } final Contact contact = conversation.getContact(); @@ -1392,9 +1620,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { capability = RtpCapability.Capability.AUDIO; } - PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> { - triggerRtpSession(contact.getAccount(), fullJid, action); - }); + PresenceSelector.selectFullJidForDirectRtpConnection( + activity, + contact, + capability, + fullJid -> { + triggerRtpSession(contact.getAccount(), fullJid, action); + }); } } @@ -1448,7 +1680,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke item.setChecked(true); } else { updated = false; - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); + activity.announcePgp( + conversation.getAccount(), + conversation, + null, + activity.onOpenPGPKeyPublished); } } else { activity.showInstallPgpDialog(); @@ -1456,8 +1692,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) - + "Enabled axolotl for Contact " + conversation.getContact().getJid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(conversation.getAccount()) + + "Enabled axolotl for Contact " + + conversation.getContact().getJid()); updated = conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); item.setChecked(true); break; @@ -1479,11 +1718,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) { if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) { + if (!hasPermissions( + attachmentChoice, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO)) { return; } - } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)) { + } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO + || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { + if (!hasPermissions( + attachmentChoice, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA)) { return; } } else if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) { @@ -1498,39 +1744,50 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final int mode = conversation.getMode(); if (encryption == Message.ENCRYPTION_PGP) { if (activity.hasPgp()) { - if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) { - activity.xmppConnectionService.getPgpEngine().hasKey( - conversation.getContact(), - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, attachmentChoice); - } + if (mode == Conversation.MODE_SINGLE + && conversation.getContact().getPgpKeyId() != 0) { + activity.xmppConnectionService + .getPgpEngine() + .hasKey( + conversation.getContact(), + new UiCallback() { + + @Override + public void userInputRequired( + PendingIntent pi, Contact contact) { + startPendingIntent(pi, attachmentChoice); + } - @Override - public void success(Contact contact) { - invokeAttachFileIntent(attachmentChoice); - } + @Override + public void success(Contact contact) { + invokeAttachFileIntent(attachmentChoice); + } - @Override - public void error(int error, Contact contact) { - activity.replaceToast(getString(error)); - } - }); - } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) { + @Override + public void error(int error, Contact contact) { + activity.replaceToast(getString(error)); + } + }); + } else if (mode == Conversation.MODE_MULTI + && conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { - Toast warning = Toast.makeText(getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG); + Toast warning = + Toast.makeText( + getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } invokeAttachFileIntent(attachmentChoice); } else { - showNoPGPKeyDialog(false, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - activity.xmppConnectionService.updateConversation(conversation); - invokeAttachFileIntent(attachmentChoice); - }); + showNoPGPKeyDialog( + false, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + activity.xmppConnectionService.updateConversation(conversation); + invokeAttachFileIntent(attachmentChoice); + }); } } else { activity.showInstallPgpDialog(); @@ -1542,18 +1799,24 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void storeRecentlyUsedQuickAction(final int attachmentChoice) { try { - activity.getPreferences().edit() - .putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString()) + activity.getPreferences() + .edit() + .putString( + RECENTLY_USED_QUICK_ACTION, + SendButtonAction.of(attachmentChoice).toString()) .apply(); } catch (IllegalArgumentException e) { - //just do not save + // just do not save } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + final PermissionUtils.PermissionResult permissionResult = + PermissionUtils.removeBluetoothConnect(permissions, grantResults); if (grantResults.length > 0) { - if (allGranted(grantResults)) { + if (allGranted(permissionResult.grantResults)) { switch (requestCode) { case REQUEST_START_DOWNLOAD: if (this.mPendingDownloadableMessage != null) { @@ -1580,7 +1843,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } else { @StringRes int res; - String firstDenied = getFirstDenied(grantResults, permissions); + String firstDenied = + getFirstDenied(permissionResult.grantResults, permissionResult.permissions); if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { @@ -1588,7 +1852,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { res = R.string.no_storage_permission; } - Toast.makeText(getActivity(), getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + getString(res, getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); } } if (writeGranted(grantResults, permissions)) { @@ -1613,41 +1881,53 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (!transferable.start()) { Log.d(Config.LOGTAG, "type: " + transferable.getClass().getName()); - Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT) + .show(); } - } else if (message.treatAsDownloadable() || message.hasFileOnRemoteHost() || MessageUtils.unInitiatedButKnownSize(message)) { + } else if (message.treatAsDownloadable() + || message.hasFileOnRemoteHost() + || MessageUtils.unInitiatedButKnownSize(message)) { createNewConnection(message); } else { - Log.d(Config.LOGTAG, message.getConversation().getAccount() + ": unable to start downloadable"); + Log.d( + Config.LOGTAG, + message.getConversation().getAccount() + ": unable to start downloadable"); } } private void createNewConnection(final Message message) { if (!activity.xmppConnectionService.hasInternetConnection()) { - Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT) + .show(); return; } - activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); + activity.xmppConnectionService + .getHttpConnectionManager() + .createNewDownloadConnection(message, true); } @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(getString(R.string.clear_conversation_history)); - final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); - final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox); + final View dialogView = + requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = + dialogView.findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.confirm), (dialog, which) -> { - this.activity.xmppConnectionService.clearConversationHistory(conversation); - if (endConversationCheckBox.isChecked()) { - this.activity.xmppConnectionService.archiveConversation(conversation); - this.activity.onConversationArchived(conversation); - } else { - activity.onConversationsListItemUpdated(); - refresh(); - } - }); + builder.setPositiveButton( + getString(R.string.confirm), + (dialog, which) -> { + this.activity.xmppConnectionService.clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + this.activity.xmppConnectionService.archiveConversation(conversation); + this.activity.onConversationArchived(conversation); + } else { + activity.onConversationsListItemUpdated(); + refresh(); + } + }); builder.create().show(); } @@ -1663,27 +1943,30 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke labels[i] = TimeFrameUtils.resolve(activity, 1000L * durations[i]); } } - builder.setItems(labels, (dialog, which) -> { - final long till; - if (durations[which] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which] * 1000L); - } - conversation.setMutedTill(till); - activity.xmppConnectionService.updateConversation(conversation); - activity.onConversationsListItemUpdated(); - refresh(); - requireActivity().invalidateOptionsMenu(); - }); + builder.setItems( + labels, + (dialog, which) -> { + final long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = System.currentTimeMillis() + (durations[which] * 1000L); + } + conversation.setMutedTill(till); + activity.xmppConnectionService.updateConversation(conversation); + activity.onConversationsListItemUpdated(); + refresh(); + requireActivity().invalidateOptionsMenu(); + }); builder.create().show(); } - private boolean hasPermissions(int requestCode, String... permissions) { + private boolean hasPermissions(int requestCode, List permissions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final List missingPermissions = new ArrayList<>(); for (String permission : permissions) { - if (Config.ONLY_INTERNAL_STORAGE && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (Config.ONLY_INTERNAL_STORAGE + && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { continue; } if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { @@ -1693,7 +1976,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (missingPermissions.size() == 0) { return true; } else { - requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]), requestCode); + requestPermissions( + missingPermissions.toArray(new String[0]), + requestCode); return false; } } else { @@ -1701,7 +1986,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - public void unmuteConversation(final Conversation conversation) { + private boolean hasPermissions(int requestCode, String... permissions) { + return hasPermissions(requestCode, ImmutableList.copyOf(permissions)); + } + + public void unMuteConversation(final Conversation conversation) { conversation.setMutedTill(0); this.activity.xmppConnectionService.updateConversation(conversation); this.activity.onConversationsListItemUpdated(); @@ -1709,7 +1998,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke requireActivity().invalidateOptionsMenu(); } - protected void invokeAttachFileIntent(final int attachmentChoice) { Intent intent = new Intent(); boolean chooser = false; @@ -1789,7 +2077,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke try { message = (Message) binding.messagesView.getItemAtPosition(i); } catch (IndexOutOfBoundsException e) { - //should not happen if we synchronize properly. however if that fails we just gonna try item -1 + // should not happen if we synchronize properly. however if that fails we + // just gonna try item -1 continue; } if (message.getType() != Message.TYPE_STATUS) { @@ -1811,7 +2100,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (message.isGeoUri()) { GeoHelper.view(getActivity(), message); } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); ViewUtil.view(activity, file); } } @@ -1820,7 +2110,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); - final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); + final String[] errorMessageParts = + errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); final String displayError; if (errorMessageParts.length == 2) { displayError = errorMessageParts[1]; @@ -1828,31 +2119,37 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke displayError = errorMessage; } builder.setMessage(displayError); - builder.setNegativeButton(R.string.copy_to_clipboard, (dialog, which) -> { - activity.copyTextToClipboard(displayError, R.string.error_message); - Toast.makeText(activity, R.string.error_message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - }); + builder.setNegativeButton( + R.string.copy_to_clipboard, + (dialog, which) -> { + activity.copyTextToClipboard(displayError, R.string.error_message); + Toast.makeText( + activity, + R.string.error_message_copied_to_clipboard, + Toast.LENGTH_SHORT) + .show(); + }); builder.setPositiveButton(R.string.confirm, null); builder.create().show(); } - private void deleteFile(final Message message) { AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { - message.setDeleted(true); - activity.xmppConnectionService.evictPreview(message.getUuid()); - activity.xmppConnectionService.updateMessage(message, false); - activity.onConversationsListItemUpdated(); - refresh(); - } - }); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { + message.setDeleted(true); + activity.xmppConnectionService.evictPreview(message.getUuid()); + activity.xmppConnectionService.updateMessage(message, false); + activity.onConversationsListItemUpdated(); + refresh(); + } + }); builder.create().show(); - } private void resendMessage(final Message message) { @@ -1861,21 +2158,29 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } final Conversation conversation = (Conversation) message.getConversation(); - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); if ((file.exists() && file.canRead()) || message.hasFileOnRemoteHost()) { final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection(); if (!message.hasFileOnRemoteHost() && xmppConnection != null && conversation.getMode() == Conversational.MODE_SINGLE - && !xmppConnection.getFeatures().httpUpload(message.getFileParams().getSize())) { - activity.selectPresence(conversation, () -> { - message.setCounterpart(conversation.getNextCounterpart()); - activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); - }); + && !xmppConnection + .getFeatures() + .httpUpload(message.getFileParams().getSize())) { + activity.selectPresence( + conversation, + () -> { + message.setCounterpart(conversation.getNextCounterpart()); + activity.xmppConnectionService.resendFailedMessages(message); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection( + size - 1); + }); + }); return; } } else if (!Compatibility.hasStoragePermission(getActivity())) { @@ -1891,10 +2196,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection(size - 1); + }); } private void cancelTransmission(Message message) { @@ -1902,7 +2209,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (transferable != null) { transferable.cancel(); } else if (message.getStatus() != Message.STATUS_RECEIVED) { - activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); + activity.xmppConnectionService.markMessage( + message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); } } @@ -1933,7 +2241,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.conversation.setDraftMessage(editable.toString()); this.binding.textinput.setText(""); this.binding.textinput.append(message.getBody()); - } private void highlightInConference(String nick) { @@ -1949,14 +2256,22 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke editable.insert(pos, nick + ": "); } else { if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) { - if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) { + if (NickValidityChecker.check( + conversation, + Arrays.asList( + editable.subSequence(0, pos - 2).toString().split(", ")))) { editable.insert(pos - 2, ", " + nick); return; } } - editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " ")); + editable.insert( + pos, + (Character.isWhitespace(before) ? "" : " ") + + nick + + (Character.isWhitespace(after) ? "" : " ")); if (Character.isWhitespace(after)) { - this.binding.textinput.setSelection(this.binding.textinput.getSelectionStart() + 1); + this.binding.textinput.setSelection( + this.binding.textinput.getSelectionStart() + 1); } } } @@ -1985,7 +2300,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (scrollState != null) { outState.putParcelable(STATE_SCROLL_POSITION, scrollState); } - final ArrayList attachments = mediaPreviewAdapter == null ? new ArrayList<>() : mediaPreviewAdapter.getAttachments(); + final ArrayList attachments = + mediaPreviewAdapter == null + ? new ArrayList<>() + : mediaPreviewAdapter.getAttachments(); if (attachments.size() > 0) { outState.putParcelableArrayList(STATE_MEDIA_PREVIEWS, attachments); } @@ -1999,7 +2317,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID); - ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); + ArrayList attachments = + savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); pendingLastMessageUuid.push(savedInstanceState.getString(STATE_LAST_MESSAGE_UUID, null)); if (uuid != null) { QuickLoader.set(uuid); @@ -2024,9 +2343,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (extras != null) { processExtras(extras); } - } else if (conversation == null && activity != null && activity.xmppConnectionService != null) { + } else if (conversation == null + && activity != null + && activity.xmppConnectionService != null) { final String uuid = pendingConversationsUuid.pop(); - Log.d(Config.LOGTAG, "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + uuid); + Log.d( + Config.LOGTAG, + "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + + uuid); if (uuid != null) { findAndReInitByUuidOrArchive(uuid); } @@ -2101,7 +2425,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return false; } this.conversation = conversation; - //once we set the conversation all is good and it will automatically do the right thing in onStart() + // once we set the conversation all is good and it will automatically do the right thing in + // onStart() if (this.activity == null || this.binding == null) { return false; } @@ -2121,12 +2446,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke setupIme(); - final boolean scrolledToBottomAndNoPending = this.scrolledToBottom() && pendingScrollState.peek() == null; + final boolean scrolledToBottomAndNoPending = + this.scrolledToBottom() && pendingScrollState.peek() == null; - this.binding.textSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName())); + this.binding.textSendButton.setContentDescription( + activity.getString(R.string.send_message_to_x, conversation.getName())); this.binding.textinput.setKeyboardListener(null); this.binding.textinput.setText(""); - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); + final boolean participating = + conversation.getMode() == Conversational.MODE_SINGLE + || conversation.getMucOptions().participating(); if (participating) { this.binding.textinput.append(this.conversation.getNextMessage()); } @@ -2157,10 +2486,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - this.binding.messagesView.post(this::fireReadEvent); - //TODO if we only do this when this fragment is running on main it won't *bing* in tablet layout which might be unnecessary since we can *see* it - activity.xmppConnectionService.getNotificationService().setOpenConversation(this.conversation); + // TODO if we only do this when this fragment is running on main it won't *bing* in tablet + // layout which might be unnecessary since we can *see* it + activity.xmppConnectionService + .getNotificationService() + .setOpenConversation(this.conversation); return true; } @@ -2180,11 +2511,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void setSelection(int pos, boolean jumpToBottom) { ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom); - this.binding.messagesView.post(() -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); + this.binding.messagesView.post( + () -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); this.binding.messagesView.post(this::fireReadEvent); } - private boolean scrolledToBottom() { return this.binding != null && scrolledToBottom(this.binding.messagesView); } @@ -2193,18 +2524,22 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID); final String text = extras.getString(Intent.EXTRA_TEXT); final String nick = extras.getString(ConversationsActivity.EXTRA_NICK); - final String postInitAction = extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION); + final String postInitAction = + extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION); final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); - final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); + final boolean doNotAppend = + extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); final String type = extras.getString(ConversationsActivity.EXTRA_TYPE); final List uris = extractUris(extras); if (uris != null && uris.size() > 0) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); } else { final List cleanedUris = cleanUris(new ArrayList<>(uris)); - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), cleanedUris, type)); } toggleInputMethod(); return; @@ -2216,7 +2551,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Jid next = Jid.of(jid.getLocal(), jid.getDomain(), nick); privateMessageWith(next); } catch (final IllegalArgumentException ignored) { - //do nothing + // do nothing } } else { final MucOptions mucOptions = conversation.getMucOptions(); @@ -2226,7 +2561,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } else { if (text != null && GeoHelper.GEO_URI.matcher(text).matches()) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); toggleInputMethod(); return; } else if (text != null && asQuote) { @@ -2239,7 +2575,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false); return; } - final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid); + final Message message = + downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid); if (message != null) { startDownloadable(message); } @@ -2264,7 +2601,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final Uri uri = iterator.next(); if (FileBackend.weOwnFile(uri)) { iterator.remove(); - Toast.makeText(getActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + R.string.security_violation_not_attaching_file, + Toast.LENGTH_SHORT) + .show(); } } return uris; @@ -2272,27 +2613,37 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private boolean showBlockSubmenu(View view) { final Jid jid = conversation.getJid(); - final boolean showReject = !conversation.isWithStranger() && conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + final boolean showReject = + !conversation.isWithStranger() + && conversation + .getContact() + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); PopupMenu popupMenu = new PopupMenu(getActivity(), view); popupMenu.inflate(R.menu.block); popupMenu.getMenu().findItem(R.id.block_contact).setVisible(jid.getLocal() != null); popupMenu.getMenu().findItem(R.id.reject).setVisible(showReject); - popupMenu.setOnMenuItemClickListener(menuItem -> { - Blockable blockable; - switch (menuItem.getItemId()) { - case R.id.reject: - activity.xmppConnectionService.stopPresenceUpdatesTo(conversation.getContact()); - updateSnackBar(conversation); + popupMenu.setOnMenuItemClickListener( + menuItem -> { + Blockable blockable; + switch (menuItem.getItemId()) { + case R.id.reject: + activity.xmppConnectionService.stopPresenceUpdatesTo( + conversation.getContact()); + updateSnackBar(conversation); + return true; + case R.id.block_domain: + blockable = + conversation + .getAccount() + .getRoster() + .getContact(jid.getDomain()); + break; + default: + blockable = conversation; + } + BlockContactDialog.show(activity, blockable); return true; - case R.id.block_domain: - blockable = conversation.getAccount().getRoster().getContact(jid.getDomain()); - break; - default: - blockable = conversation; - } - BlockContactDialog.show(activity, blockable); - return true; - }); + }); popupMenu.show(); return true; } @@ -2306,13 +2657,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } if (account.getStatus() == Account.State.DISABLED) { - showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener); + showSnackbar( + R.string.this_account_is_disabled, + R.string.enable, + this.mEnableAccountListener); } else if (conversation.isBlocked()) { showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); - } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener); - } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener); + } else if (contact != null + && !contact.showInRoster() + && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar( + R.string.contact_added_you, + R.string.add_back, + this.mAddBackClickListener, + this.mLongPressBlockListener); + } else if (contact != null + && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar( + R.string.contact_asks_for_presence_subscription, + R.string.allow, + this.mAllowPresenceSubscription, + this.mLongPressBlockListener); } else if (mode == Conversation.MODE_MULTI && !conversation.getMucOptions().online() && account.getStatus() == Account.State.ONLINE) { @@ -2338,7 +2703,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } break; case PASSWORD_REQUIRED: - showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword); + showSnackbar( + R.string.conference_requires_password, + R.string.enter_password, + enterPassword); break; case BANNED: showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc); @@ -2347,7 +2715,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc); break; case RESOURCE_CONSTRAINT: - showSnackbar(R.string.conference_resource_constraint, R.string.try_again, joinMuc); + showSnackbar( + R.string.conference_resource_constraint, R.string.try_again, joinMuc); break; case KICKED: showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); @@ -2364,7 +2733,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke showSnackbar(R.string.conference_destroyed, R.string.leave, leaveMuc); break; case NON_ANONYMOUS: - showSnackbar(R.string.group_chat_will_make_your_jabber_id_public, R.string.join, acceptJoin); + showSnackbar( + R.string.group_chat_will_make_your_jabber_id_public, + R.string.join, + acceptJoin); break; default: hideSnackbar(); @@ -2377,7 +2749,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke && conversation.countMessages() != 0 && !conversation.isBlocked() && conversation.isWithStranger()) { - showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener); + showSnackbar( + R.string.received_message_from_stranger, R.string.block, mBlockClickListener); } else { hideSnackbar(); } @@ -2386,10 +2759,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void refresh() { if (this.binding == null) { - Log.d(Config.LOGTAG, "ConversationFragment.refresh() skipped updated because view binding was null"); + Log.d( + Config.LOGTAG, + "ConversationFragment.refresh() skipped updated because view binding was null"); return; } - if (this.conversation != null && this.activity != null && this.activity.xmppConnectionService != null) { + if (this.conversation != null + && this.activity != null + && this.activity.xmppConnectionService != null) { if (!activity.xmppConnectionService.isConversationStillOpen(this.conversation)) { activity.onConversationArchived(this.conversation); return; @@ -2406,7 +2783,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke updateStatusMessages(); if (conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid) != 0) { binding.unreadCountCustomView.setVisibility(View.VISIBLE); - binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); + binding.unreadCountCustomView.setUnreadCount( + conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); } this.messageListAdapter.notifyDataSetChanged(); updateChatMsgHint(); @@ -2429,12 +2807,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke storeNextMessage(); updateChatMsgHint(); SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean prefScrollToBottom = p.getBoolean("scroll_to_bottom", activity.getResources().getBoolean(R.bool.scroll_to_bottom)); + final boolean prefScrollToBottom = + p.getBoolean( + "scroll_to_bottom", + activity.getResources().getBoolean(R.bool.scroll_to_bottom)); if (prefScrollToBottom || scrolledToBottom()) { - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection(size - 1); + }); } } @@ -2443,8 +2826,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private boolean storeNextMessage(String msg) { - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED && participating && this.conversation.setNextMessage(msg)) { + final boolean participating = + conversation.getMode() == Conversational.MODE_SINGLE + || conversation.getMucOptions().participating(); + if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED + && participating + && this.conversation.setNextMessage(msg)) { this.activity.xmppConnectionService.updateConversation(this.conversation); return true; } @@ -2461,7 +2848,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void updateEditablity() { - boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null; + boolean canWrite = + this.conversation.getMode() == Conversation.MODE_SINGLE + || this.conversation.getMucOptions().participating() + || this.conversation.getNextCounterpart() != null; this.binding.textinput.setFocusable(canWrite); this.binding.textinput.setFocusableInTouchMode(canWrite); this.binding.textSendButton.setEnabled(canWrite); @@ -2470,10 +2860,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public void updateSendButton() { - boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); + boolean hasAttachments = + mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); final Conversation c = this.conversation; final Presence.Status status; - final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); + final String text = + this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); final SendButtonAction action; if (hasAttachments) { action = SendButtonAction.TEXT; @@ -2481,12 +2873,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke action = SendButtonTool.getAction(getActivity(), c, text); } if (c.getAccount().getStatus() == Account.State.ONLINE) { - if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { + if (activity != null + && activity.xmppConnectionService != null + && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { status = Presence.Status.OFFLINE; } else if (c.getMode() == Conversation.MODE_SINGLE) { status = c.getContact().getShownStatus(); } else { - status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE; + status = + c.getMucOptions().online() + ? Presence.Status.ONLINE + : Presence.Status.OFFLINE; } } else { status = Presence.Status.OFFLINE; @@ -2494,7 +2891,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.binding.textSendButton.setTag(action); final Activity activity = getActivity(); if (activity != null) { - this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(activity, action, status)); + this.binding.textSendButton.setImageResource( + SendButtonTool.getSendButtonImageResource(activity, action, status)); } } @@ -2506,9 +2904,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); if (state == ChatState.COMPOSING) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); + this.messageList.add( + Message.createStatusMessage( + conversation, + getString(R.string.contact_is_typing, conversation.getName()))); } else if (state == ChatState.PAUSED) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); + this.messageList.add( + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_stopped_typing, + conversation.getName()))); } else { for (int i = this.messageList.size() - 1; i >= 0; --i) { final Message message = this.messageList.get(i); @@ -2517,8 +2923,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } else { if (message.getStatus() == Message.STATUS_SEND_DISPLAYED) { - this.messageList.add(i + 1, - Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + this.messageList.add( + i + 1, + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_read_up_to_this_point, + conversation.getName()))); return; } } @@ -2530,18 +2941,22 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final List allUsers = mucOptions.getUsers(); final Set addedMarkers = new HashSet<>(); ChatState state = ChatState.COMPOSING; - List users = conversation.getMucOptions().getUsersWithChatState(state, 5); + List users = + conversation.getMucOptions().getUsersWithChatState(state, 5); if (users.size() == 0) { state = ChatState.PAUSED; users = conversation.getMucOptions().getUsersWithChatState(state, 5); } if (mucOptions.isPrivateAndNonAnonymous()) { for (int i = this.messageList.size() - 1; i >= 0; --i) { - final Set markersForMessage = messageList.get(i).getReadByMarkers(); + final Set markersForMessage = + messageList.get(i).getReadByMarkers(); final List shownMarkers = new ArrayList<>(); for (ReadByMarker marker : markersForMessage) { if (!ReadByMarker.contains(marker, addedMarkers)) { - addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway + addedMarkers.add( + marker); // may be put outside this condition. set should do + // dedup anyway MucOptions.User user = mucOptions.findUser(marker); if (user != null && !users.contains(user)) { shownMarkers.add(user); @@ -2554,16 +2969,29 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (size > 1) { final String body; if (size <= 4) { - body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers)); - } else if (ReadByMarker.allUsersRepresented(allUsers, markersForMessage, markerForSender)) { + body = + getString( + R.string.contacts_have_read_up_to_this_point, + UIHelper.concatNames(shownMarkers)); + } else if (ReadByMarker.allUsersRepresented( + allUsers, markersForMessage, markerForSender)) { body = getString(R.string.everyone_has_read_up_to_this_point); } else { - body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3); + body = + getString( + R.string.contacts_and_n_more_have_read_up_to_this_point, + UIHelper.concatNames(shownMarkers, 3), + size - 3); } statusMessage = Message.createStatusMessage(conversation, body); statusMessage.setCounterparts(shownMarkers); } else if (size == 1) { - statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0)))); + statusMessage = + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_read_up_to_this_point, + UIHelper.getDisplayName(shownMarkers.get(0)))); statusMessage.setCounterpart(shownMarkers.get(0).getFullJid()); statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid()); } else { @@ -2582,18 +3010,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Message statusMessage; if (users.size() == 1) { MucOptions.User user = users.get(0); - int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing; - statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user))); + int id = + state == ChatState.COMPOSING + ? R.string.contact_is_typing + : R.string.contact_has_stopped_typing; + statusMessage = + Message.createStatusMessage( + conversation, getString(id, UIHelper.getDisplayName(user))); statusMessage.setTrueCounterpart(user.getRealJid()); statusMessage.setCounterpart(user.getFullJid()); } else { - int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing; - statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users))); + int id = + state == ChatState.COMPOSING + ? R.string.contacts_are_typing + : R.string.contacts_have_stopped_typing; + statusMessage = + Message.createStatusMessage( + conversation, getString(id, UIHelper.concatNames(users))); statusMessage.setCounterparts(users); } this.messageList.add(statusMessage); } - } } @@ -2608,8 +3045,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return false; } final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked(); - final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService(); - return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c))); + final MessageArchiveService service = + activity.xmppConnectionService.getMessageArchiveService(); + return mam + && (c.getLastClearHistory().getTimestamp() != 0 + || (c.countMessages() == 0 + && c.messagesLoaded.get() + && c.hasMessagesLeftOnServer() + && !service.queryInProgress(c))); } private boolean hasMamSupport(final Conversation c) { @@ -2621,11 +3064,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) { + protected void showSnackbar( + final int message, final int action, final OnClickListener clickListener) { showSnackbar(message, action, clickListener, null); } - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) { + protected void showSnackbar( + final int message, + final int action, + final OnClickListener clickListener, + final View.OnLongClickListener longClickListener) { this.binding.snackbar.setVisibility(View.VISIBLE); this.binding.snackbar.setOnClickListener(null); this.binding.snackbarMessage.setText(message); @@ -2655,7 +3103,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } if (conversation.getAccount().getPgpSignature() == null) { - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); + activity.announcePgp( + conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); return; } if (!mSendingPgpMessage.compareAndSet(false, true)) { @@ -2663,85 +3112,107 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (conversation.getMode() == Conversation.MODE_SINGLE) { if (contact.getPgpKeyId() != 0) { - xmppService.getPgpEngine().hasKey(contact, - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); - } + xmppService + .getPgpEngine() + .hasKey( + contact, + new UiCallback() { + + @Override + public void userInputRequired( + PendingIntent pi, Contact contact) { + startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); + } - @Override - public void success(Contact contact) { - encryptTextMessage(message); - } + @Override + public void success(Contact contact) { + encryptTextMessage(message); + } - @Override - public void error(int error, Contact contact) { - activity.runOnUiThread(() -> Toast.makeText(activity, - R.string.unable_to_connect_to_keychain, - Toast.LENGTH_SHORT - ).show()); - mSendingPgpMessage.set(false); - } - }); + @Override + public void error(int error, Contact contact) { + activity.runOnUiThread( + () -> + Toast.makeText( + activity, + R.string + .unable_to_connect_to_keychain, + Toast.LENGTH_SHORT) + .show()); + mSendingPgpMessage.set(false); + } + }); } else { - showNoPGPKeyDialog(false, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message); - messageSent(); - }); + showNoPGPKeyDialog( + false, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + xmppService.updateConversation(conversation); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + }); } } else { if (conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { - Toast warning = Toast - .makeText(getActivity(), - R.string.missing_public_keys, - Toast.LENGTH_LONG); + Toast warning = + Toast.makeText( + getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } encryptTextMessage(message); } else { - showNoPGPKeyDialog(true, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - xmppService.sendMessage(message); - messageSent(); - }); + showNoPGPKeyDialog( + true, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.updateConversation(conversation); + xmppService.sendMessage(message); + messageSent(); + }); } } } public void encryptTextMessage(Message message) { - activity.xmppConnectionService.getPgpEngine().encrypt(message, - new UiCallback() { + activity.xmppConnectionService + .getPgpEngine() + .encrypt( + message, + new UiCallback() { - @Override - public void userInputRequired(PendingIntent pi, Message message) { - startPendingIntent(pi, REQUEST_SEND_MESSAGE); - } + @Override + public void userInputRequired(PendingIntent pi, Message message) { + startPendingIntent(pi, REQUEST_SEND_MESSAGE); + } - @Override - public void success(Message message) { - //TODO the following two call can be made before the callback - getActivity().runOnUiThread(() -> messageSent()); - } + @Override + public void success(Message message) { + // TODO the following two call can be made before the callback + getActivity().runOnUiThread(() -> messageSent()); + } - @Override - public void error(final int error, Message message) { - getActivity().runOnUiThread(() -> { - doneSendingPgpMessage(); - Toast.makeText(getActivity(), error == 0 ? R.string.unable_to_connect_to_keychain : error, Toast.LENGTH_SHORT).show(); + @Override + public void error(final int error, Message message) { + getActivity() + .runOnUiThread( + () -> { + doneSendingPgpMessage(); + Toast.makeText( + getActivity(), + error == 0 + ? R.string + .unable_to_connect_to_keychain + : error, + Toast.LENGTH_SHORT) + .show(); + }); + } }); - - } - }); } public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) { @@ -2766,12 +3237,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final Editable editable = this.binding.textinput.getText(); String previous = editable == null ? "" : editable.toString(); if (doNotAppend && !TextUtils.isEmpty(previous)) { - Toast.makeText(getActivity(), R.string.already_drafting_message, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.already_drafting_message, Toast.LENGTH_LONG) + .show(); return; } if (UIHelper.isLastLineQuote(previous)) { text = '\n' + text; - } else if (previous.length() != 0 && !Character.isWhitespace(previous.charAt(previous.length() - 1))) { + } else if (previous.length() != 0 + && !Character.isWhitespace(previous.charAt(previous.length() - 1))) { text = " " + text; } this.binding.textinput.append(text); @@ -2792,24 +3265,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public boolean onArrowUpCtrlPressed() { - final Message lastEditableMessage = conversation == null ? null : conversation.getLastEditableMessage(); + final Message lastEditableMessage = + conversation == null ? null : conversation.getLastEditableMessage(); if (lastEditableMessage != null) { correctMessage(lastEditableMessage); return true; } else { - Toast.makeText(getActivity(), R.string.could_not_correct_message, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.could_not_correct_message, Toast.LENGTH_LONG) + .show(); return false; } } @Override public void onTypingStarted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { + if (status == Account.State.ONLINE + && conversation.setOutgoingChatState(ChatState.COMPOSING)) { service.sendChatState(conversation); } runOnUiThread(this::updateSendButton); @@ -2817,7 +3294,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onTypingStopped() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } @@ -2829,21 +3307,24 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onTextDeleted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { + if (status == Account.State.ONLINE + && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { service.sendChatState(conversation); } if (storeNextMessage()) { - runOnUiThread(() -> { - if (activity == null) { - return; - } - activity.onConversationsListItemUpdated(); - }); + runOnUiThread( + () -> { + if (activity == null) { + return; + } + activity.onConversationsListItemUpdated(); + }); } runOnUiThread(this::updateSendButton); } @@ -2867,7 +3348,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke completionIndex = 0; final String content = this.binding.textinput.getText().toString(); lastCompletionCursor = this.binding.textinput.getSelectionEnd(); - int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0; + int start = + lastCompletionCursor > 0 + ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 + : 0; firstWord = start == 0; incomplete = content.substring(start, lastCompletionCursor); } @@ -2881,12 +3365,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Collections.sort(completions); if (completions.size() > completionIndex) { String completion = completions.get(completionIndex).substring(incomplete.length()); - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); + this.binding + .textinput + .getEditableText() + .delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); this.binding.textinput.getEditableText().insert(lastCompletionCursor, completion); lastCompletionLength = completion.length(); } else { completionIndex = -1; - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); + this.binding + .textinput + .getEditableText() + .delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); lastCompletionLength = 0; } return true; @@ -2894,7 +3384,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void startPendingIntent(PendingIntent pendingIntent, int requestCode) { try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); + getActivity() + .startIntentSenderForResult( + pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); } catch (final SendIntentException ignored) { } } @@ -2968,63 +3460,85 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onContactPictureLongClicked(View v, final Message message) { final String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { fingerprint = "pgp"; } else { fingerprint = message.getFingerprint(); } final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final Contact contact = message.getContact(); - if (message.getStatus() <= Message.STATUS_RECEIVED && (contact == null || !contact.isSelf())) { + if (message.getStatus() <= Message.STATUS_RECEIVED + && (contact == null || !contact.isSelf())) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { final Jid cp = message.getCounterpart(); if (cp == null || cp.isBareJid()) { return; } final Jid tcp = message.getTrueCounterpart(); - final User userByRealJid = tcp != null ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp) : null; - final User user = userByRealJid != null ? userByRealJid : conversation.getMucOptions().findUserByFullJid(cp); + final User userByRealJid = + tcp != null + ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp) + : null; + final User user = + userByRealJid != null + ? userByRealJid + : conversation.getMucOptions().findUserByFullJid(cp); popupMenu.inflate(R.menu.muc_details_context); final Menu menu = popupMenu.getMenu(); - MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, conversation, user); - popupMenu.setOnMenuItemClickListener(menuItem -> MucDetailsContextMenuHelper.onContextItemSelected(menuItem, user, activity, fingerprint)); + MucDetailsContextMenuHelper.configureMucDetailsContextMenu( + activity, menu, conversation, user); + popupMenu.setOnMenuItemClickListener( + menuItem -> + MucDetailsContextMenuHelper.onContextItemSelected( + menuItem, user, activity, fingerprint)); } else { popupMenu.inflate(R.menu.one_on_one_context); - popupMenu.setOnMenuItemClickListener(item -> { - switch (item.getItemId()) { - case R.id.action_contact_details: - activity.switchToContactDetails(message.getContact(), fingerprint); - break; - case R.id.action_show_qr_code: - activity.showQrCode("xmpp:" + message.getContact().getJid().asBareJid().toEscapedString()); - break; - } - return true; - }); + popupMenu.setOnMenuItemClickListener( + item -> { + switch (item.getItemId()) { + case R.id.action_contact_details: + activity.switchToContactDetails( + message.getContact(), fingerprint); + break; + case R.id.action_show_qr_code: + activity.showQrCode( + "xmpp:" + + message.getContact() + .getJid() + .asBareJid() + .toEscapedString()); + break; + } + return true; + }); } } else { popupMenu.inflate(R.menu.account_context); final Menu menu = popupMenu.getMenu(); - menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations()); - popupMenu.setOnMenuItemClickListener(item -> { - final XmppActivity activity = this.activity; - if (activity == null) { - Log.e(Config.LOGTAG,"Unable to perform action. no context provided"); - return true; - } - switch (item.getItemId()) { - case R.id.action_show_qr_code: - activity.showQrCode(conversation.getAccount().getShareableUri()); - break; - case R.id.action_account_details: - activity.switchToAccount(message.getConversation().getAccount(), fingerprint); - break; - case R.id.action_manage_accounts: - AccountUtils.launchManageAccounts(activity); - break; - } - return true; - }); + menu.findItem(R.id.action_manage_accounts) + .setVisible(QuickConversationsService.isConversations()); + popupMenu.setOnMenuItemClickListener( + item -> { + final XmppActivity activity = this.activity; + if (activity == null) { + Log.e(Config.LOGTAG, "Unable to perform action. no context provided"); + return true; + } + switch (item.getItemId()) { + case R.id.action_show_qr_code: + activity.showQrCode(conversation.getAccount().getShareableUri()); + break; + case R.id.action_account_details: + activity.switchToAccount( + message.getConversation().getAccount(), fingerprint); + break; + case R.id.action_manage_accounts: + AccountUtils.launchManageAccounts(activity); + break; + } + return true; + }); } popupMenu.show(); } @@ -3032,25 +3546,43 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onContactPictureClicked(Message message) { String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { fingerprint = "pgp"; } else { fingerprint = message.getFingerprint(); } final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; if (received) { - if (message.getConversation() instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getConversation() instanceof Conversation + && message.getConversation().getMode() == Conversation.MODE_MULTI) { Jid tcp = message.getTrueCounterpart(); Jid user = message.getCounterpart(); if (user != null && !user.isBareJid()) { - final MucOptions mucOptions = ((Conversation) message.getConversation()).getMucOptions(); - if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { - if (!mucOptions.isUserInRoom(user) && mucOptions.findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { - Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), Toast.LENGTH_SHORT).show(); + final MucOptions mucOptions = + ((Conversation) message.getConversation()).getMucOptions(); + if (mucOptions.participating() + || ((Conversation) message.getConversation()).getNextCounterpart() + != null) { + if (!mucOptions.isUserInRoom(user) + && mucOptions.findUserByRealJid( + tcp == null ? null : tcp.asBareJid()) + == null) { + Toast.makeText( + getActivity(), + activity.getString( + R.string.user_has_left_conference, + user.getResource()), + Toast.LENGTH_SHORT) + .show(); } highlightInConference(user.getResource()); } else { - Toast.makeText(getActivity(), R.string.you_are_not_participating, Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + R.string.you_are_not_participating, + Toast.LENGTH_SHORT) + .show(); } } return; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 0e286c8dc5f7d6a7552d125afc2ac8bdbe8411e0..fc7b504494ff6edbce00e23ae7ca40a3ba2d9a41 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -272,14 +272,16 @@ public class RtpSessionActivity extends XmppActivity } private void requestPermissionsAndAcceptCall() { - final List permissions; + final ImmutableList.Builder permissions = ImmutableList.builder(); if (getMedia().contains(Media.VIDEO)) { - permissions = - ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); + permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO); } else { - permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); + permissions.add(Manifest.permission.RECORD_AUDIO); } - if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT); + } + if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) { putScreenInCallMode(); checkRecorderAndAcceptCall(); } @@ -491,13 +493,16 @@ public class RtpSessionActivity extends XmppActivity public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (PermissionUtils.allGranted(grantResults)) { + final PermissionUtils.PermissionResult permissionResult = + PermissionUtils.removeBluetoothConnect(permissions, grantResults); + if (PermissionUtils.allGranted(permissionResult.grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { checkRecorderAndAcceptCall(); } } else { @StringRes int res; - final String firstDenied = getFirstDenied(grantResults, permissions); + final String firstDenied = + getFirstDenied(permissionResult.grantResults, permissionResult.permissions); if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { diff --git a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java index 80b58d8cbef886a28e04a3489ded4cccdf4e2479..004676156f2ffb8d0c98a2256a015f32287a6cbc 100644 --- a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java @@ -8,7 +8,9 @@ import android.os.Build; import androidx.core.app.ActivityCompat; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import java.util.ArrayList; import java.util.List; public class PermissionUtils { @@ -40,11 +42,41 @@ public class PermissionUtils { return null; } - public static boolean hasPermission(final Activity activity, final List permissions, final int requestCode) { + public static class PermissionResult { + public final String[] permissions; + public final int[] grantResults; + + public PermissionResult(String[] permissions, int[] grantResults) { + this.permissions = permissions; + this.grantResults = grantResults; + } + } + + public static PermissionResult removeBluetoothConnect( + final String[] inPermissions, final int[] inGrantResults) { + final List outPermissions = new ArrayList<>(); + final List outGrantResults = new ArrayList<>(); + for (int i = 0; i < Math.min(inPermissions.length, inGrantResults.length); ++i) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (inPermissions[i].equals(Manifest.permission.BLUETOOTH_CONNECT)) { + continue; + } + } + outPermissions.add(inPermissions[i]); + outGrantResults.add(inGrantResults[i]); + } + + return new PermissionResult( + outPermissions.toArray(new String[0]), Ints.toArray(outGrantResults)); + } + + public static boolean hasPermission( + final Activity activity, final List permissions, final int requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final ImmutableList.Builder missingPermissions = new ImmutableList.Builder<>(); for (final String permission : permissions) { - if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.checkSelfPermission(activity, permission) + != PackageManager.PERMISSION_GRANTED) { missingPermissions.add(permission); } } @@ -52,7 +84,8 @@ public class PermissionUtils { if (missing.size() == 0) { return true; } - ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), requestCode); + ActivityCompat.requestPermissions( + activity, missing.toArray(new String[0]), requestCode); return false; } else { return true; From b3a3f2b9308deadd5ab1af55e1e66b0e7b704ba5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 9 Aug 2022 09:40:01 +0200 Subject: [PATCH 13/25] try to detect if a container contains video or audio fixes #4321 --- .../persistance/FileBackend.java | 31 +++- .../services/XmppConnectionService.java | 6 +- .../ui/adapter/ConversationAdapter.java | 153 +++++++++++++----- .../siacs/conversations/utils/MimeUtils.java | 18 ++- .../siacs/conversations/utils/UIHelper.java | 4 +- src/main/res/values/strings.xml | 1 + 6 files changed, 159 insertions(+), 54 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index faeca6308938e7ef997676d81c297a0b26220296..c3093f2cf9a054a3c73d0668d285526cc694c755 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1496,6 +1496,7 @@ public class FileBackend { DownloadableFile file = getFile(message); final String mime = file.getMimeType(); final boolean privateMessage = message.isPrivateMessage(); + final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); @@ -1507,7 +1508,21 @@ public class FileBackend { body.append(url); } body.append('|').append(file.getSize()); - if (image || video || pdf) { + if (ambiguous) { + try { + final Dimensions dimensions = getVideoDimensions(file); + if (dimensions.valid()) { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is video"); + body.append('|').append(dimensions.width).append('|').append(dimensions.height); + } else { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } catch (final NotAVideoFile e) { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } else if (image || video || pdf) { try { final Dimensions dimensions; if (video) { @@ -1537,14 +1552,16 @@ public class FileBackend { : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } - private int getMediaRuntime(File file) { + private int getMediaRuntime(final File file) { try { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - return Integer.parseInt( - mediaMetadataRetriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_DURATION)); - } catch (RuntimeException e) { + final String value = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (Strings.isNullOrEmpty(value)) { + return 0; + } + return Integer.parseInt(value); + } catch (NumberFormatException e) { return 0; } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a6823e6702925d38b39642ffb0d1616c6910fac4..b15d9bccbcc9bfab9b10d3e10ed4a6c6ebb83902 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1384,6 +1384,7 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { + //TODO add immutable flag final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); @@ -1430,7 +1431,8 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + //TODO add immutable flag + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); @@ -1443,7 +1445,7 @@ public class XmppConnectionService extends Service { connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.mPresenceParser); connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); - connection.setOnJinglePacketReceivedListener(((a, jp) -> mJingleConnectionManager.deliverPacket(a, jp))); + connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 662120d8436b28206ad0d6f31abac5a26be3d5e2..9822ac004bac4fdf1e1f9e7ef321d97b73c78f6f 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -11,6 +11,7 @@ import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; import com.google.common.base.Optional; +import com.google.common.base.Strings; import java.util.List; @@ -24,11 +25,13 @@ import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.IrregularUnicodeDetector; +import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -public class ConversationAdapter extends RecyclerView.Adapter { +public class ConversationAdapter + extends RecyclerView.Adapter { private final XmppActivity activity; private final List conversations; @@ -39,11 +42,15 @@ public class ConversationAdapter extends RecyclerView.Adapter 0 && fileParams.height > 0) { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_videocam, + R.drawable.ic_attach_videocam); showPreviewText = false; - break; - case "audio": - imageResource = activity.getThemeResource(R.attr.ic_attach_record, R.drawable.ic_attach_record); + } else if (fileParams.runtime > 0) { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_record, R.drawable.ic_attach_record); showPreviewText = false; - break; - default: - imageResource = activity.getThemeResource(R.attr.ic_attach_document, R.drawable.ic_attach_document); + } else { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_document, + R.drawable.ic_attach_document); showPreviewText = true; - break; + } + } else { + switch (Strings.nullToEmpty(mime).split("/")[0]) { + case "image": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_photo, R.drawable.ic_attach_photo); + showPreviewText = false; + break; + case "video": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_videocam, + R.drawable.ic_attach_videocam); + showPreviewText = false; + break; + case "audio": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_record, + R.drawable.ic_attach_record); + showPreviewText = false; + break; + default: + imageResource = + activity.getThemeResource( + R.attr.ic_attach_document, + R.drawable.ic_attach_document); + showPreviewText = true; + break; + } } } viewHolder.binding.conversationLastmsgImg.setImageResource(imageResource); @@ -125,13 +174,18 @@ public class ConversationAdapter extends RecyclerView.Adapter preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor()); + final Pair preview = + UIHelper.getMessagePreview( + activity, + message, + viewHolder.binding.conversationLastmsg.getCurrentTextColor()); if (showPreviewText) { viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(preview.first)); } else { viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); } - viewHolder.binding.conversationLastmsg.setVisibility(showPreviewText ? View.VISIBLE : View.GONE); + viewHolder.binding.conversationLastmsg.setVisibility( + showPreviewText ? View.VISIBLE : View.GONE); if (preview.second) { if (isRead) { viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.ITALIC); @@ -152,7 +206,8 @@ public class ConversationAdapter extends RecyclerView.Adapter ongoingCall; if (conversation.getMode() == Conversational.MODE_MULTI) { ongoingCall = Optional.absent(); } else { - ongoingCall = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + ongoingCall = + activity.xmppConnectionService + .getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); } if (ongoingCall.isPresent()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); - viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); + final int ic_ongoing_call = + activity.getThemeResource( + R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); + viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); } else { - final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); + final long muted_till = + conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); if (muted_till == Long.MAX_VALUE) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); + int ic_notifications_off = + activity.getThemeResource( + R.attr.icon_notifications_off, + R.drawable.ic_notifications_off_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off); } else if (muted_till >= System.currentTimeMillis()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); + int ic_notifications_paused = + activity.getThemeResource( + R.attr.icon_notifications_paused, + R.drawable.ic_notifications_paused_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); } else if (conversation.alwaysNotify()) { viewHolder.binding.notificationStatus.setVisibility(View.GONE); } else { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); + int ic_notifications_none = + activity.getThemeResource( + R.attr.icon_notifications_none, + R.drawable.ic_notifications_none_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); } } @@ -201,9 +270,16 @@ public class ConversationAdapter extends RecyclerView.Adapter listener.onConversationClick(v, conversation)); } @@ -216,7 +292,6 @@ public class ConversationAdapter extends RecyclerView.Adapter AMBIGUOUS_CONTAINER_FORMATS = ImmutableList.of( + "application/ogg", + "video/3gpp", // .3gp files can contain audio, video or both + "video/3gpp2" + ); + private static final Map mimeTypeToExtensionMap = new HashMap<>(); private static final Map extensionToMimeTypeMap = new HashMap<>(); @@ -225,7 +234,12 @@ public final class MimeUtils { add("application/x-xcf", "xcf"); add("application/x-xfig", "fig"); add("application/xhtml+xml", "xhtml"); + add("video/3gpp", "3gpp"); + add("video/3gpp", "3gp"); + add("video/3gpp2", "3gpp2"); + add("video/3gpp2", "3g2"); add("audio/3gpp", "3gpp"); + add("audio/3gpp", "3gp"); add("audio/aac", "aac"); add("audio/aac-adts", "aac"); add("audio/amr", "amr"); @@ -365,10 +379,6 @@ public final class MimeUtils { add("text/x-tex", "cls"); add("text/x-vcalendar", "vcs"); add("text/x-vcard", "vcf"); - add("video/3gpp", "3gpp"); - add("video/3gpp", "3gp"); - add("video/3gpp2", "3gpp2"); - add("video/3gpp2", "3g2"); add("video/avi", "avi"); add("video/dl", "dl"); add("video/dv", "dif"); diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 26732b501e45733b3738f2fa20edb72ab3a64fb0..b70bfc5589ed80ce84eaf23258423a278a8856ab 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -477,8 +477,10 @@ public class UIHelper { public static String getFileDescriptionString(final Context context, final Message message) { final String mime = message.getMimeType(); - if (mime == null) { + if (Strings.isNullOrEmpty(mime)) { return context.getString(R.string.file); + } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { + return context.getString(R.string.multimedia_file); } else if (mime.startsWith("audio/")) { return context.getString(R.string.audio); } else if (mime.startsWith("video/")) { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ee5cbce8138454aa05280581348134ed904091f7..8b1fd1de38f1d4c4030ae3985b9a28bf09fdfc7e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -415,6 +415,7 @@ video image vector graphic + multimedia file PDF document Android App Contact From cc80a2a758be3953a149b5bcc55064bd8d31d5fd Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Tue, 9 Aug 2022 12:34:39 +0000 Subject: [PATCH 14/25] Fix typo --- src/main/java/eu/siacs/conversations/utils/Compatibility.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 21004e26a9ca0447ffdd324f5f949b0bb41eda40..c28b8fe29681302eabaf5efa59d8c6dbf6cc412b 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -34,7 +34,7 @@ public class Compatibility { "notification_ringtone", "notification_headsup", "vibrate_on_notification"); - private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = + private static final List UNUSED_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(Context context) { @@ -115,7 +115,7 @@ public class Compatibility { for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX - : UNUESD_SETTINGS_PRE_TWENTYSIX)) { + : UNUSED_SETTINGS_PRE_TWENTYSIX)) { Preference preference = settingsFragment.findPreference(key); if (preference != null) { for (PreferenceCategory category : categories) { From 2c5601ccf1c85f6de72cd98591bb470ce9fc847e Mon Sep 17 00:00:00 2001 From: Millesimus <32270710+Millesimus@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:29:01 +0200 Subject: [PATCH 15/25] add migration tutorial * Create migrating_to_new_device.md * Add link to Readme for further information. * Added some info * Make separate backup guide and integrate Readme information with new guides. Co-authored-by: Licaon_Kter --- README.md | 12 ++------ docs/user/backup.md | 19 +++++++++++++ docs/user/migrating_to_new_device.md | 42 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 docs/user/backup.md create mode 100644 docs/user/migrating_to_new_device.md diff --git a/README.md b/README.md index 689af33d51f6d1907445be1f433c824a233dda8c..6e78332827810e602b29cf7971aed9180c909345 100644 --- a/README.md +++ b/README.md @@ -273,16 +273,10 @@ the translation team and then step by our group chat on and introduce yourself to `iNPUTmice` so he can approve your join request. #### How do I backup / move Conversations to a new device? -On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.) -As of version 2.4.0 an integrated Backup & Restore function will help with this, go to Settings and you’ll find a setting called Create backup. A notification will pop-up during the creation process that will announce you when it's ready. After the files, one for each account, are created, you can move the **Conversations** folder *(if you want your old media files too)* or only the **Conversations/Backup** folder *(for OMEMO keys and history only)* to your new device (or to a storage place) where a freshly installed Conversations can restore each account. Don't forget to enable the accounts after a successfull restore. - -This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. - -In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. - -**WARNING**: Be sure to know your accounts passwords or find ways to reset them **before** doing the backup as the files are encrypted using those passwords and the Restore process will ask for them. -**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. +See the dedicated guides for +- [backups](docs/user/backup.md) +- [migrations](docs/user/migrating_to_new_device.md) #### Conversations is missing a certain feature diff --git a/docs/user/backup.md b/docs/user/backup.md new file mode 100644 index 0000000000000000000000000000000000000000..4d81d8ddda914502d4cbd9901a896b8684a8f7f0 --- /dev/null +++ b/docs/user/backup.md @@ -0,0 +1,19 @@ +# Making a backup of Conversations + +This tutorial explains how you can backup your Conversations data. + +**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. + +1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup. +2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts". +3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option. +4. Wait, until the notification tells you that the backup is finished. +5. Move the backup to whatever location you feel save with. + +Done! + +## Further information / troubleshooting +### Unable to decrypt +This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. + +In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. diff --git a/docs/user/migrating_to_new_device.md b/docs/user/migrating_to_new_device.md new file mode 100644 index 0000000000000000000000000000000000000000..401a153867a4bd4286b6091e23d90a2a9267d373 --- /dev/null +++ b/docs/user/migrating_to_new_device.md @@ -0,0 +1,42 @@ +# Migrating to a new device + +This tutorial explains how you can transfer your Conversations data from an old to a new device. It assumes that you do not have Conversations installed on your new device, yet. It basically consists of three steps: + +1. Make a backup (old device) +2. Move that backup to your new device +3. Import the backup (new device) + +**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. + +## 1. Make a backup (old device) +1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup. +2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts". +3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option. +4. Wait, until the notification tells you that the backup is finished. + +## 2. Move that backup to your new device +1. Locate the backup. You should find it in your Files, either in *Conversations/Backup* or in *Download/Conversations/Backup*. The file is named after your account (*e.g. kim@example.org*). If you have multiple accounts, you find one file for each. +2. Use your USB cable or bluetooth, your Nextcloud or other cloud storage or pretty much anything you want to copy the backup from the old device to the new device. +3. Remember the location you saved your backup to. For instance, you might want to save them to the *Download* folder. + +## 3. Import the backup (new device) +1. Install Conversations on your new device. +2. Open Conversations for the first time. +3. Tap on "Use other server" +4. Tap on the three dot menu in the upper right corner and tap on "Import backup" +5. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from the where you saved them. +6. Enter your account password to decrypt the backup. +7. Remember to activate your account (head back to "manage accounts", see step 1.2). +8. Check if chats work. + +Once confirmed that the new device is running fine you can just uninstall the app from the old device. + +Note: The backup only contains your text chats and required encryption keys, all the files need to be transferred separately and put on the new device in the same locations. + +Done! + +## Further information / troubleshooting +### Unable to decrypt +This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. + +In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. From 508e1ac1bd22d16503a1d05150317d783ac83c0b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 9 Aug 2022 19:42:57 +0200 Subject: [PATCH 16/25] add immutable flag to pending alarm intents --- .../conversations/http/HttpUploadConnection.java | 1 + .../services/XmppConnectionService.java | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index f1bae9956a33ddc3994879fb059f69e044fe0f57..20db7bfbd31bb1f94cde0af1ac69fdbbb24fff82 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -144,6 +144,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan @Override public void onFailure(@NotNull final Throwable throwable) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); + // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence fail(throwable.getMessage()); } }, MoreExecutors.directExecutor()); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b15d9bccbcc9bfab9b10d3e10ed4a6c6ebb83902..43d0e769f233daf07b8456b04a622294321dae9b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -1384,8 +1386,9 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { - //TODO add immutable flag - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); } else { @@ -1397,7 +1400,7 @@ public class XmppConnectionService extends Service { } public void scheduleWakeUpCall(int seconds, int requestCode) { - final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000; + final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager == null) { return; @@ -1431,8 +1434,9 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { - //TODO add immutable flag - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); From fe3433e427d7d271fb1baf0e0a071550550ff9e9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 10 Aug 2022 09:11:09 +0200 Subject: [PATCH 17/25] do not accept empty credentials as ice-restart --- .../java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index e95a7e36d11a6987a8bd51f50a34ba4021ffbc81..e7693d6a8345bcb01bd6354caa25a5a529a1c9e5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -182,6 +182,9 @@ public class RtpContentMap { final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { + if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { + throw new IllegalStateException("Credentials are missing password or ufrag"); + } return credentials; } throw new IllegalStateException("Content map does not have distinct credentials"); From e559b1472910453a40c7cb0dc7ba5291867c39ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 10 Aug 2022 19:11:58 +0200 Subject: [PATCH 18/25] pulled translations from transifex --- src/main/res/values-de/strings.xml | 1 + src/main/res/values-pt-rBR/strings.xml | 1 + src/main/res/values-ro-rRO/strings.xml | 1 + src/main/res/values-zh-rCN/strings.xml | 1 + src/main/res/values-zh-rTW/strings.xml | 331 +++++++++++++++++++------ 5 files changed, 253 insertions(+), 82 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 8372ff2dc4f36c3839865cc77f651add94eaadf6..6d85f67d971a12045533fe3b3d664b8bd525abbb 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -417,6 +417,7 @@ Video Bild Vektorgrafik + Multimediadatei PDF-Dokument Android App Kontakt diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 57fe94c67e043eacd0d598d222486b7e6715cf16..0847137b3edd8f7274e0f0f9e9d5c8d26b425d6c 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -420,6 +420,7 @@ vídeo imagem gráfico vetorial + arquivo multimídia Documento PDF Aplicativo Android Contato diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index c0c15cb4abbe4b575fbb88499d9fd52a15d68d47..17459ba0d20703985ed59576ed9cd78a25c0173b 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -420,6 +420,7 @@ video imagine grafic vectorial + fișier multimedia document PDF Aplicație Android Contact diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index be4aa4face764a0e86f050991d20d9eeffa6b333..17d1851058d8628138ad476663a768d3b410b058 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -414,6 +414,7 @@ 视频 图片 矢量图 + 多媒体文件 PDF文档 Android App 联系人 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index c2ca60d1a248ddb8ab0952eea1c90eb24cb199be..0179f100896697f02578bb02c8fb0938b8946fa1 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -1,97 +1,132 @@ 設定 - 新對話 + 新會話 管理帳戶 - 聯絡人詳情 + 管理帳戶 + 關閉會話 + 聯絡人詳細資料 + 群組聊天詳細資料 + 頻道詳細資料 新增帳戶 - 編輯姓名 - 添加到地址薄 - 從列表中刪除 + 編輯名稱 + 新增至通訊錄 + 從名冊中刪除 封鎖連絡人 解除封鎖連絡人 封鎖網域 解除封鎖網域 + 封鎖成員 + 解除封鎖成員 管理帳戶 - 設置 - 分享到 Conversation + 設定 + 分享至 Conversation 開始會話 + 選擇聯絡人 + 選擇聯絡人 + 透過帳戶分享 封鎖清單 剛剛 1 分鐘前 - %d分鐘前 - 正在發送… - 訊息解密中,請稍候… - OpenPGP 加密的信息 - 該名稱已存在 + %d 分鐘前 + + %d 則未讀會話 + + + 正在傳送… + 正在解密訊息,請稍候… + OpenPGP 已加密的訊息 + 暱稱已有人使用 + 無效的暱稱 管理員 - 所有者 + 擁有者 版主 - 參與者 + 成員 訪客 - 要封鎖 %s 讓它不能送訊息給你嗎? - 要解除封鎖 %s 讓它可以送訊息給你嗎? - 要封鎖來自 %s 的所有連絡人嗎? - 要解除封鎖來自 %s 的所有連絡人嗎? + 要將 %s 從你的聯絡人清單中移除嗎?與此聯絡人的會話將不會被移除。 + 要封鎖 %s 向您傳送訊息嗎? + 要解除封鎖 %s 並允許他們向您傳送訊息嗎? + 要封鎖來自 %s 的所有聯絡人嗎? + 要解除封鎖來自 %s 的所有聯絡人嗎? 連絡人已封鎖 + 已封鎖 + 要從書籤中移除 %s 嗎?與此書籤相關的會話將不會被移除。 在伺服器上註冊新帳戶 - 在伺服器上改變密碼 - 分享… - 連絡人 - 連絡人 + 在伺服器上變更密碼 + 分享至… + 開始會話 + 邀請聯絡人 + 邀請 + 聯絡人 + 聯絡人 取消 - 設置 - 添加 + 設定 + 新增 編輯 刪除 封鎖 解除封鎖 - 保存 + 儲存 完成 - 現在發送 + %1$s 已當機 + 立即傳送 不再詢問 + 無法連線至帳戶 + 無法連線至多個帳戶 + 輕觸以管理你的帳戶 附加檔案 - 添加連絡人 + 要將這位遺失的聯絡人新增至你的聯絡人清單嗎? + 新增聯絡人 傳遞失敗 - 正在分享檔案中,請稍候… + 正在準備傳送圖片 + 正在準備傳送圖片 + 正在分享檔案,請稍候… 清除歷史記錄 清除會話記錄 - 選擇設備 - 發送未加密的訊息 - 送訊息 - 送訊息給 %s - 送 OMEMO 加密訊息 - 送 v\\OMEMO 加密訊息 - 送 OpenPGP 加密訊息 - 不加密發送 + 刪除檔案 + 之後關閉此會話 + 選擇裝置 + 傳送未加密的訊息 + 傳送訊息 + 傳送訊息至 %s + 傳送 OMEMO 加密訊息 + 傳送 v\\OMEMO 加密訊息 + 傳送 OpenPGP 加密訊息 + 新暱稱已被使用 + 不加密傳送 解密失敗,可能是私密金鑰不正確。 OpenKeychain - 重啟 + 重新啟動 安裝 請安裝 OpenKeychain 以解密 - 輸入… - 等待… - 未發現 OpenPGP 金鑰 + 正在提供… + 正在等候… + 找不到 OpenPGP 金鑰 未找到 OpenPGP 金鑰 - 常規 - 接收檔案 - 自動接收小於 … 的檔案 + 一般 + 接受檔案 + 自動接受小於此大小的檔案 附件 通知 震動 收到新訊息時震動 - LED 燈通知 + LED 通知 收到新訊息時閃爍通知燈 鈴聲 + 通知音效 + 收到新訊息時發出通知音效 + 來電時響鈴 靜默期限 - 高級 - 總不發送崩潰報告 + 進階 + 永不傳送當機報告 確認訊息 - 讓聯絡人知道它們的訊息已經收到以及讀取 + 讓你的聯絡人知道你已經收到並閱讀了他們的訊息 + 防止截圖 + 在多工畫面隱藏應用程式聯絡人並且封鎖螢幕截圖 UI 接受 產生了一個錯誤 - 你的帳號 + 你的帳戶 發送線上連絡人列表更新 接收線上連絡人列表更新 請求線上連絡人列表更新 @@ -114,12 +149,12 @@ 註冊完成 違反政策 伺服器不相容 - 流錯誤 + 串流錯誤 未加密 OTR OpenPGP OMEMO - 刪除帳號 + 刪除帳戶 暫時不可用 發佈頭像 發佈 OpenPGP 公開金鑰 @@ -128,16 +163,21 @@ 啟用帳戶 確定? 錄音 + XMPP 位址 + 封鎖 XMPP 位址 username@example.com 密碼 - 是否添加 %s 到地址薄? + 這不是有效的 XMPP 位址 + 記憶體不足,圖片過大 + 要將 %s 新增至通訊錄嗎? 伺服器資訊 XEP-0313: MAM XEP-0280: 訊息複本 XEP-0352: 用戶端狀態指示 - XEP-0191: 封鎖指令 - XEP-0237: 花名冊版本控制 - XEP-0198: 流管理 + XEP-0191: 封鎖命令 + XEP-0237: 名冊版本設定 + XEP-0198: 串流管理 + XEP-0215: 外部服務探索 XEP-0163: PEP (替身 / OMEMO) XEP-0363: HTTP 檔案上傳 XEP-0357: Push @@ -151,25 +191,31 @@ OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 - 其他設備 + 其他裝置 信任的 OMEMO 指紋 - 獲取金鑰中 + 正在擷取金鑰… 完成 解密 - 查找 - 輸入連絡人 - 查看連絡人詳細資訊 - 封鎖連絡人 - 解除封鎖連絡人 - 創建 - 選擇 - 連絡人已存在 + 書籤 + 尋找 + 輸入聯絡人 + 刪除聯絡人 + 檢視聯絡人詳細資料 + 封鎖聯絡人 + 解除封鎖聯絡人 + 建立 + 選取 + 聯絡人已存在 加入 - 保存為書簽 - 刪除書簽 + channel@conference.example.com/nick + channel@conference.example.com + 儲存為書籤 + 刪除書籤 + 主旨 + 正在加入群組聊天… 離開 - 連絡人已添加你到連絡人列表 - 反向添加 + 聯絡人已新增至你的聯絡人清單 + 新增回 %s 已讀此句 發佈 正在發佈… @@ -180,25 +226,29 @@ 至 %s 送私密訊息給 %s 連接 - 該帳號已存在 + 此帳戶已存在 下一步 - 忽略 + 工作階段已建立 + 跳過 關閉通知 打開通知 + 群組聊天需要密碼 輸入密碼 - 現在發送請求 + 立即要求 忽略 - 安全 + 安全性 允許更正訊息 允許您的連絡人追回編輯他們的訊息 - 高級設置 + 專家設定 請謹慎使用 + 關於 %s 靜默時間段 開始時間 結束時間 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 + 同步處理書籤 用帳戶 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 @@ -208,7 +258,11 @@ 引用 拷貝原始URL 再次發送 - 檔案位址(URL) + 檔案 URL + 已複製 URL 到剪貼簿 + 已複製 XMPP 位址到剪貼簿 + 已複製錯誤訊息到剪貼簿 + 網頁地址 掃描二維條碼 顯示二維條碼 顯示封鎖清單 @@ -216,15 +270,26 @@ 確認 再試一遍 防止作業系統中斷你的連接 - 選檔案 - 接收中 %1$s (已完成 %2$d%%) + 建立備份 + 備份檔案將被儲存至 %s + 正在建立備份檔案 + 你的備份已建立 + 此備份檔案已被儲存至 %s + 正在還原備份 + 你的備份已還原 + 不要忘記啟用帳戶。 + 選擇檔案 + 正在接收 %1$s (已完成 %2$d%%) 下載 %s 刪除 %s 檔案 - 打開 %s - 發送中 (已完成 %1$d%%) + 開啟 %s + 正在傳送 (已完成 %1$d%%) + 正在準備分享檔案 可以下載 %s 取消傳送 + 無法分享檔案 + 檔案已刪除 在連絡人下方顯示唯讀標籤 啟用通知 帳戶頭像 @@ -418,20 +483,122 @@ 再試解密ㄧ次 通訊對話錯誤 頭條通知 - 消息已經拷貝到剪貼板 + 今天 + 昨天 + 錄製影片 + 複製到剪貼簿 + 訊息已複製到剪貼簿 + 訊息 + 私密訊息已停用 + 受保護的應用程式 + 接受未知憑證? + 憑證詳細資料: + 僅一次 + 捲動至底部 + 傳送訊息後向下捲動 + 編輯狀態訊息 + 編輯訊息 + 停用加密 + 無法擷取裝置清單 + 無法擷取加密金鑰 + 立即停用 OMEMO 加密 一對一以及私人群組的聊天一定會用 OMEMO 新的對話預設會用 OMEMO 加密 - 新的對話必須要手動開啟 OMEMO 加密 + 新的會話必須要手動開啟 OMEMO 加密 + 建立捷徑 字型大小 - App 中所使用的相對字型大小 + 應用程式中使用的相對字型大小 預設開啟 預設關閉 - 適中 + + 復原 + 位置分享已停用 + 固定位置 + 取消固定位置 + 複製位置 + 分享位置 + 方向 + 分享位置 顯示位置 + 分享 + 無法開始錄製 + 請稍候… + 授予 %1$s 以存取麥克風 搜尋訊息 + GIF + 檢視會話 + 分享位置外掛程式 + 使用分享位置外掛程式而非內建地圖 + 複製網站位址 + 複製 XMPP 位址 + 直接搜尋 + 暱稱 + 名稱 聊天群組名稱 + 無法儲存錄製 + 狀態資訊 + 訊息 + 通話 + 訊息 + 來電 + 正在進行的通話 + 無聲訊息 + 訊息通知設定 + 來電通知設定 + 影片壓縮 + 檢視媒體 + 成員 + 媒體瀏覽器 + 影片質量 + 低質量意味這更小的檔案 + 中 (360P) + 高 (720P) + 已取消 + 無效的國家碼 + 選擇國家 + 電話號碼 + 驗證電話號碼 + 請輸入您的電話號碼。 + 搜尋國家 + 驗證 %s + 重新傳送簡訊 + 重新傳送簡訊 (%s) + 請等候 (%s) + 返回 + + + 正在驗證… + 正在要求簡訊… + 未知網路錯誤。 + 沒有網路連線。 + 更新 + 你的名稱 + 輸入你的名稱 + 拒絕要求 + 電子書 + 開啟為… + 選擇帳戶 + 還原備份 + 還原 + 備份與還原 + 建立群組聊天 + 加入公用頻道 + 建立私人群組聊天 + 建立公用頻道 + 頻道名稱 + XMPP 位址 + 活動 + 開啟備份 + 本機伺服器 + 關於 忙碌 + 說明 + 釘選 + 取消釘選 + 離開 + 播放音訊 + 更多選項 From 5a8d70a1f0bbad1d7b0ea89be4fc7573bfbbb27c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Aug 2022 13:27:25 +0200 Subject: [PATCH 19/25] pulled translations from transifex --- src/main/res/values-gl/strings.xml | 1 + src/main/res/values-pl/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 338ff69b8d8e516a312992aefb19d19982c158a3..3c7a0606c76ecc8eff70f7d99709fb85757b1bdd 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -417,6 +417,7 @@ video imaxe gráfico de vector + ficheiro multimedia documento PDF App Android Contacto diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 30f28a9adb9829f85d817d7aff147e56d5b562a8..d5d494124728448aa234cd0020eed725de9702f7 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -423,6 +423,7 @@ plik wideo obraz grafika wektorowa + plik multimediów Dokument PDF Aplikacja Androida Kontakt From 150f8313a0d97cf71980a380c1d8e8070a23ec81 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Aug 2022 14:31:27 +0200 Subject: [PATCH 20/25] make launch conversation and launch tor pending intents immutable --- .../services/NotificationService.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index acac26cc091593340f98062e74600a83607b8adc..c9b932415586fd37a8dd9744eeefc15cd90f6c1e 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1524,7 +1524,9 @@ public class NotificationService { mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), - 0); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } catch (RuntimeException e) { return null; } @@ -1573,13 +1575,25 @@ public class NotificationService { R.drawable.ic_play_circle_filled_white_48dp, mXmppConnectionService.getString(R.string.start_orbot), PendingIntent.getActivity( - mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0)); + mXmppConnectionService, + 147, + TorServiceUtils.LAUNCH_INTENT, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); } else { mBuilder.addAction( R.drawable.ic_file_download_white_24dp, mXmppConnectionService.getString(R.string.install_orbot), PendingIntent.getActivity( - mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0)); + mXmppConnectionService, + 146, + TorServiceUtils.INSTALL_INTENT, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); } } mBuilder.setDeleteIntent(createDismissErrorIntent()); From 7cc96e704e5d35637b66d49537d40e7e3c5d03ca Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 09:58:33 +0200 Subject: [PATCH 21/25] do not retrieve media attributes from encrypted files fixes #4353 --- .../crypto/PgpDecryptionService.java | 2 +- .../persistance/FileBackend.java | 137 ++++++++++-------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index a676e5d5d32613f846522a937f0fe914e48eae55..db84e0cf4da5e3ca8336dcb6842f594acdc45cf1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -207,8 +207,8 @@ public class PgpDecryptionService { } } final String url = message.getFileParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); + mXmppConnectionService.getFileBackend().updateFileParams(message, url); mXmppConnectionService.updateMessage(message); if (!inputFile.delete()) { Log.w(Config.LOGTAG,"unable to delete pgp encrypted source file "+inputFile.getAbsolutePath()); diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index c3093f2cf9a054a3c73d0668d285526cc694c755..0d1c03fcb2462df754fab7f6c4c556832d31518d 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -66,7 +66,6 @@ import eu.siacs.conversations.services.AttachFileToConversationRunnable; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.MediaAdapter; import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.FileWriterException; @@ -400,25 +399,23 @@ public class FileBackend { public static Uri getMediaUri(Context context, File file) { final String filePath = file.getAbsolutePath(); - final Cursor cursor; - try { - cursor = - context.getContentResolver() - .query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[] {MediaStore.Images.Media._ID}, - MediaStore.Images.Media.DATA + "=? ", - new String[] {filePath}, - null); - } catch (SecurityException e) { - return null; - } - if (cursor != null && cursor.moveToFirst()) { - final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); - cursor.close(); - return Uri.withAppendedPath( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); - } else { + try (final Cursor cursor = + context.getContentResolver() + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Images.Media._ID}, + MediaStore.Images.Media.DATA + "=? ", + new String[] {filePath}, + null)) { + if (cursor != null && cursor.moveToFirst()) { + final int id = + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); + return Uri.withAppendedPath( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); + } else { + return null; + } + } catch (final Exception e) { return null; } } @@ -1492,57 +1489,73 @@ public class FileBackend { updateFileParams(message, null); } - public void updateFileParams(Message message, String url) { - DownloadableFile file = getFile(message); + public void updateFileParams(final Message message, final String url) { + final boolean encrypted = + message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED; + final DownloadableFile file = getFile(message); final String mime = file.getMimeType(); - final boolean privateMessage = message.isPrivateMessage(); - final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); - final boolean video = mime != null && mime.startsWith("video/"); - final boolean audio = mime != null && mime.startsWith("audio/"); - final boolean pdf = "application/pdf".equals(mime); + final boolean privateMessage = message.isPrivateMessage(); final StringBuilder body = new StringBuilder(); if (url != null) { body.append(url); } - body.append('|').append(file.getSize()); - if (ambiguous) { - try { - final Dimensions dimensions = getVideoDimensions(file); - if (dimensions.valid()) { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is video"); - body.append('|').append(dimensions.width).append('|').append(dimensions.height); - } else { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + if (encrypted && !file.exists()) { + Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted"); + final DownloadableFile encryptedFile = getFile(message, false); + body.append('|').append(encryptedFile.getSize()); + } else { + Log.d(Config.LOGTAG, "running updateFileParams"); + final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); + final boolean video = mime != null && mime.startsWith("video/"); + final boolean audio = mime != null && mime.startsWith("audio/"); + final boolean pdf = "application/pdf".equals(mime); + body.append('|').append(file.getSize()); + if (ambiguous) { + try { + final Dimensions dimensions = getVideoDimensions(file); + if (dimensions.valid()) { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is video"); + body.append('|') + .append(dimensions.width) + .append('|') + .append(dimensions.height); + } else { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } catch (final NotAVideoFile e) { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); body.append("|0|0|").append(getMediaRuntime(file)); } - } catch (final NotAVideoFile e) { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); - body.append("|0|0|").append(getMediaRuntime(file)); - } - } else if (image || video || pdf) { - try { - final Dimensions dimensions; - if (video) { - dimensions = getVideoDimensions(file); - } else if (pdf) { - dimensions = getPdfDocumentDimensions(file); - } else { - dimensions = getImageDimensions(file); - } - if (dimensions.valid()) { - body.append('|').append(dimensions.width).append('|').append(dimensions.height); + } else if (image || video || pdf) { + try { + final Dimensions dimensions; + if (video) { + dimensions = getVideoDimensions(file); + } else if (pdf) { + dimensions = getPdfDocumentDimensions(file); + } else { + dimensions = getImageDimensions(file); + } + if (dimensions.valid()) { + body.append('|') + .append(dimensions.width) + .append('|') + .append(dimensions.height); + } + } catch (NotAVideoFile notAVideoFile) { + Log.d( + Config.LOGTAG, + "file with mime type " + file.getMimeType() + " was not a video file"); + // fall threw } - } catch (NotAVideoFile notAVideoFile) { - Log.d( - Config.LOGTAG, - "file with mime type " + file.getMimeType() + " was not a video file"); - // fall threw + } else if (audio) { + body.append("|0|0|").append(getMediaRuntime(file)); } - } else if (audio) { - body.append("|0|0|").append(getMediaRuntime(file)); } message.setBody(body.toString()); message.setDeleted(false); @@ -1556,12 +1569,14 @@ public class FileBackend { try { final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - final String value = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + final String value = + mediaMetadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION); if (Strings.isNullOrEmpty(value)) { return 0; } return Integer.parseInt(value); - } catch (NumberFormatException e) { + } catch (final IllegalArgumentException e) { return 0; } } From e9816a7f9092c29c4e7b37615dc939b43f0dbe1a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 10:02:07 +0200 Subject: [PATCH 22/25] pulled translations from transifex --- src/conversations/res/values-zh-rTW/strings.xml | 8 ++++++++ src/quicksy/res/values-zh-rTW/strings.xml | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/conversations/res/values-zh-rTW/strings.xml create mode 100644 src/quicksy/res/values-zh-rTW/strings.xml diff --git a/src/conversations/res/values-zh-rTW/strings.xml b/src/conversations/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a5ba73d42d4d6ae97ce46021774d9f63b559e125 --- /dev/null +++ b/src/conversations/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + + 挑選您的 XMPP 提供者 + 使用 conversations.im + 建立新帳戶 + 您的伺服器邀請 + 分享邀請至… + \ No newline at end of file diff --git a/src/quicksy/res/values-zh-rTW/strings.xml b/src/quicksy/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..d4b37b79204765bbf648c13f3883f5b3b42a6040 --- /dev/null +++ b/src/quicksy/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + + Quicksy 設定檔圖片 + Quicksy 在您的國家無法使用。 + 無法驗證伺服器身分。 + 未知安全性錯誤。 + 連線伺服器逾時。 + From 4fbe2deffc94d087d123dd2b74cd54e1f3558216 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 10:22:45 +0200 Subject: [PATCH 23/25] skip empty uris on attach --- src/main/java/eu/siacs/conversations/ui/util/Attachment.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index f994955d0d300c01f61c2f8697c4012b367fec28..9c6849ce602debecb01b2a36943536bd834fdf80 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -139,6 +139,9 @@ public class Attachment implements Parcelable { public static List of(final Context context, List uris, final String type) { final List attachments = new ArrayList<>(); for (final Uri uri : uris) { + if (uri == null) { + continue; + } final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } From 41d98da17d584289767ab8186ad16d4cf8846db3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 11:02:18 +0200 Subject: [PATCH 24/25] set immutable flags for backup notifications --- .../conversations/services/ImportBackupService.java | 6 +++++- src/main/AndroidManifest.xml | 4 ++++ .../conversations/services/ExportBackupService.java | 12 +++++++++--- .../eu/siacs/conversations/ui/util/Attachment.java | 3 +++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index a1b5f9e771cbbd1ce4e8706ea7ef36b091c9ae8c..c118d7375365febc8206b36bf4f2ec1ceb8fa98c 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -304,7 +306,9 @@ public class ImportBackupService extends Service { mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) .setContentText(getString(R.string.notification_restored_backup_subtitle)) .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp); notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8cb3758703c9064054fc4ccfc0bd7f9fac70ec5c..e37b5ab36d073400739972f10ca98695959944ff 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -60,6 +60,10 @@ + + + + diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 6cbb26ad118eb97addf8cd7dfe47806bbf2163ee..4e144f223cfde44518bba71333b3f57aba3b8a96 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -344,9 +346,11 @@ public class ExportBackupService extends Service { PendingIntent openFolderIntent = null; - for (Intent intent : getPossibleFileOpenIntents(this, path)) { + for (final Intent intent : getPossibleFileOpenIntents(this, path)) { if (intent.resolveActivityInfo(getPackageManager(), 0) != null) { - openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT); + openFolderIntent = PendingIntent.getActivity(this, 189, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); break; } } @@ -362,7 +366,9 @@ public class ExportBackupService extends Service { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setType(MIME_TYPE); final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files)); - shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, PendingIntent.FLAG_UPDATE_CURRENT); + shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index 9c6849ce602debecb01b2a36943536bd834fdf80..e68bcc53430b655ed162c4d99a0b5326c28b6a28 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -38,6 +38,8 @@ import android.os.Parcelable; import com.google.common.base.MoreObjects; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -89,6 +91,7 @@ public class Attachment implements Parcelable { return type; } + @NotNull @Override public String toString() { return MoreObjects.toStringHelper(this) From 83d258f90ff16b21af238d7ca96d04bd96aa795a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Aug 2022 11:16:27 +0200 Subject: [PATCH 25/25] version bump to 2.10.9 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90365d20ef3fdd427a5aebb502510ab4a043dd48..ac29b5f883dd4a74a1b369567e61eaf9489975fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.9 + +* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets) +* Fix bug when calling Movim + ### Version 2.10.8 * Fix wrong avatar being shown for group chats diff --git a/build.gradle b/build.gradle index 9e1366ce3e9b1ac1f96208fabe1bb82e3df02066..7678d888baddf4f92132def8e3cfb97c4ed03a10 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42034 - versionName "2.10.8" + versionCode 42037 + versionName "2.10.9" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId