mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ef372c22 | |||
| 19e6d4c303 | |||
| 64752d3bae | |||
| 6145e0d2df | |||
| b365284743 | |||
| af62041c14 | |||
| 910dbf56fd | |||
| 4eca0dea4a | |||
| a76c17fd45 | |||
| b00aeec03a | |||
| ffe147fde8 | |||
| 02a5bb06a9 | |||
| a8997738bf | |||
| 202690d02e | |||
| 6d14bbdbc7 | |||
| 03a0b53eee | |||
| a044181a75 | |||
| e9ae9dc5bf | |||
| cbfa3b7e58 | |||
| bf57d3bd73 | |||
| 6200f354ca | |||
| c8dfb08dcb | |||
| 543e9d91e4 | |||
| dec5879919 | |||
| a1d21d8562 | |||
| 253ed877e5 | |||
| b809d24ab1 | |||
| 78aee0a487 | |||
| d16254c146 | |||
| 7ab3ef8453 | |||
| 4b695a3293 | |||
| da4c382c9a | |||
| 80973960f5 | |||
| 7be75f7008 | |||
| 6fa114c6c0 | |||
| 767b5f2bae | |||
| ed3a21b992 | |||
| 899c2b5647 | |||
| 61e616a53b | |||
| 3bf4504de9 | |||
| cd0740d895 | |||
| 9f99edc159 | |||
| 60568f23f5 | |||
| 6dc8bd7ba8 | |||
| ff4ecb3bda | |||
| c71e71359a | |||
| d5a09fa25e | |||
| f52c2be2c2 | |||
| 2233b93108 | |||
| d76eb3239a | |||
| a982bd2bbb | |||
| 9ef3edcc0e | |||
| de7c54b886 | |||
| 9c9f966597 | |||
| 12e77789d3 | |||
| bd640072ab | |||
| 0a9c46fbfc | |||
| 1cc4f11484 | |||
| 9386f2f9cb | |||
| 45d73d4604 | |||
| a5892330dd | |||
| 8a0e2d72a4 | |||
| 2629c65564 | |||
| 10694a6809 | |||
| aa3d177243 | |||
| 4d35d4edeb | |||
| bbaba3cd33 | |||
| d1f002a132 | |||
| 9ce9a91c95 | |||
| 7844e146b1 | |||
| eb997eca00 | |||
| 30124de2a8 | |||
| 1dcf7e4860 | |||
| 9ffc904ae5 | |||
| 2704749b44 | |||
| 1c174b5b70 | |||
| 8b5dd70d75 | |||
| f022316ad7 | |||
| 3386f5c5f7 | |||
| 57eead3a34 | |||
| 2868b51835 | |||
| 82118db71b | |||
| 14f55ca6b1 | |||
| cebfa12142 | |||
| 96c8c21b78 | |||
| 9ab1b1f3a7 | |||
| 94a0e426f8 | |||
| 006f8ae826 | |||
| 2889266522 | |||
| 5e6fccf143 | |||
| 3847e20d18 | |||
| fc69212a51 | |||
| 8999f54ba2 | |||
| f470e92300 | |||
| 3ac49e3e58 | |||
| 8dd9cfec5b | |||
| 0588214ee7 | |||
| 4ec49a031c | |||
| a31d7d6d3e | |||
| 64bbe9866d | |||
| 5d7ab84efc | |||
| 11f73a88e8 | |||
| dd0e847976 | |||
| 71ed333468 | |||
| 41d94ae3ee | |||
| b66bc1f863 | |||
| df7d80319c | |||
| 134145d166 | |||
| 688a103c10 | |||
| e2efa1f913 | |||
| aebd5c66f7 | |||
| 15c60c6b12 | |||
| f319ba2b83 | |||
| 93f12e7367 | |||
| 10acb07f82 | |||
| ee9a8dd53a | |||
| 0a55023bdb | |||
| fec9f8b4d3 | |||
| 13374df709 | |||
| 7fa04dc3c0 | |||
| 9d4e0e4e21 | |||
| 473c28ab07 | |||
| de47feac40 | |||
| 84894ff538 | |||
| f254c35749 | |||
| 1ff4e069ea | |||
| d574d33596 | |||
| f6756fc34b | |||
| 7c27eb47fc | |||
| 8d5a55c24c | |||
| 2e9aa79b02 | |||
| 1f9264225b | |||
| 18e145faaf | |||
| 47cf70120a | |||
| 34be7aab17 | |||
| 59139ed242 | |||
| 11f3964bdc | |||
| 7683408d18 | |||
| b74e793654 | |||
| 4bd74324d2 | |||
| 9a121b3039 | |||
| a9832c9c53 | |||
| 1e4c8bc291 |
+14
-2
@@ -1,6 +1,7 @@
|
||||
# Delta Chat Android Changelog
|
||||
|
||||
## Unreleased
|
||||
## v2.43.0
|
||||
2026-02
|
||||
|
||||
* Improve switch speed when changing profiles
|
||||
* Allow to switch profile when sharing or forwarding
|
||||
@@ -12,13 +13,24 @@
|
||||
* Mark external links with " ↗" to make them clear
|
||||
* Make QR code larger on "Add Second Device" screen
|
||||
* Add indication for blocked contacts in user profile
|
||||
* Allow to start calls with video disabled
|
||||
* Show hint for empty contact search results
|
||||
* Add background playing for voice messages and other audio files
|
||||
* Allow scanning Invitation Code when creating a new profile
|
||||
* Add context menu in long-pressing relays items instead of showing buttons
|
||||
* Enhanced video player UI
|
||||
* Fix: Show dialog if pasted QR codes are invalid
|
||||
* Fix: Refresh chat list when returning from conversation if selected profile changed
|
||||
* Fix: Update menu when using "select all" in contact selection
|
||||
* Fix: Avoid empty profiles after using "add as second device" from welcome screen
|
||||
* Fix: Remove from group deselected members in the contact selection list
|
||||
* Fix multi-device seen messages synchronization when using multiple relays
|
||||
* Update to core 2.39.0
|
||||
* Fix mailto handling
|
||||
* Fix layout problems inside in-chat apps
|
||||
* Fix real-time for in-chat apps that need it
|
||||
* Avoid crash when the app is minimized with profile switcher or reactions dialogs open
|
||||
* Remove "trash icon" option from contact selection list when adding members to group
|
||||
* Update to core 2.43.0
|
||||
|
||||
## v2.35.0
|
||||
2026-01
|
||||
|
||||
+8
-3
@@ -33,8 +33,8 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000736
|
||||
versionName "2.36.0"
|
||||
versionCode 30000737
|
||||
versionName "2.43.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
@@ -211,6 +211,8 @@ dependencies {
|
||||
implementation "io.noties.markwon:inline-parser:$markwon_version"
|
||||
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
|
||||
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
|
||||
implementation 'androidx.sharetarget:sharetarget:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
@@ -231,8 +233,11 @@ dependencies {
|
||||
implementation 'androidx.work:work-runtime:2.9.1'
|
||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
|
||||
implementation 'com.google.guava:guava:31.1-android'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$media3_version"
|
||||
implementation "androidx.media3:media3-session:$media3_version"
|
||||
implementation "androidx.media3:media3-ui:$media3_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
|
||||
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
|
||||
|
||||
+38
-2
@@ -153,6 +153,42 @@ static uint32_t* jintArray2uint32Pointer(JNIEnv* env, jintArray ja, int* ret_icn
|
||||
}
|
||||
|
||||
|
||||
/************************************************************
|
||||
* DcEventChannel
|
||||
************************************************************/
|
||||
|
||||
static dc_event_channel_t* get_dc_event_channel(JNIEnv *env, jobject obj)
|
||||
{
|
||||
static jfieldID fid = 0;
|
||||
if (fid==0) {
|
||||
jclass cls = (*env)->GetObjectClass(env, obj);
|
||||
fid = (*env)->GetFieldID(env, cls, "eventChannelCPtr", "J" /*Signature, J=long*/);
|
||||
}
|
||||
if (fid) {
|
||||
return (dc_event_channel_t*)(*env)->GetLongField(env, obj, fid);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_createEventChannelCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return (jlong)dc_event_channel_new();
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcEventChannel_unrefEventChannelCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
dc_event_channel_unref(get_dc_event_channel(env, obj));
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_getEventEmitterCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return (jlong)dc_event_channel_get_event_emitter(get_dc_event_channel(env, obj));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* DcAccounts
|
||||
******************************************************************************/
|
||||
@@ -172,11 +208,11 @@ static dc_accounts_t* get_dc_accounts(JNIEnv *env, jobject obj)
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir)
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir, jobject chanObj)
|
||||
{
|
||||
CHAR_REF(dir);
|
||||
int writable = 1;
|
||||
jlong accountsCPtr = (jlong)dc_accounts_new(dirPtr, writable);
|
||||
jlong accountsCPtr = (jlong)dc_accounts_new_with_event_channel(dirPtr, writable, get_dc_event_channel(env, chanObj));
|
||||
CHAR_UNREF(dir);
|
||||
return accountsCPtr;
|
||||
}
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: 8b2e88f45a...7ad67cfa56
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="228"
|
||||
android:viewportHeight="280">
|
||||
<group android:scaleX="0.35014287"
|
||||
android:scaleY="0.43"
|
||||
android:translateX="74.08372"
|
||||
android:translateY="79.8">
|
||||
<path
|
||||
android:pathData="m10.03,234.14c0.3,-0.01 0.6,-0.02 0.9,-0.04 -0.07,-0.49 -0.14,-0.97 -0.2,-1.46 -0.15,-1.34 -0.26,-2.69 -0.25,-4.04 -0.02,-0.86 -0.05,-1.71 -0.07,-2.57 -0.09,-0.06 -0.18,-0.13 -0.27,-0.19 -0.02,-0.02 -0.04,-0.03 -0.07,-0.05zM44.87,232.95c11.71,-8.35 26.86,-14.79 46.21,-15.9 0,0 39.93,-0.27 47.91,-3.53 7.98,-3.26 68.68,-14.69 82.94,-98.43 14.26,-83.74 -1.06,-115.09 -1.06,-115.09 0,0 -21.14,55.68 -81.02,59.81 0,0 -14.5,1.03 -38.82,1.42 -24.32,0.39 -75.77,20.65 -90.55,85.62l-0.22,43.44c2.5,4.22 5.49,8.12 8.91,11.66 3.99,4.11 8.11,8.12 12.79,11.45 2.26,1.65 4.65,3.2 6.51,5.33 1.94,2.34 3.33,5 4.2,7.93 0.71,2.1 1.45,4.2 2.2,6.28z"
|
||||
android:fillColor="@color/ic_launcher_background"/>
|
||||
<path
|
||||
android:pathData="m217.97,45.86c-0.3,0.01 -0.6,0.02 -0.9,0.04 0.07,0.49 0.14,0.97 0.2,1.46 0.15,1.34 0.26,2.69 0.25,4.04 0.02,0.86 0.05,1.71 0.07,2.57 0.09,0.06 0.18,0.13 0.27,0.19 0.02,0.02 0.04,0.03 0.07,0.05zM183.13,47.05c-11.71,8.35 -26.86,14.79 -46.21,15.9 0,0 -39.93,0.27 -47.91,3.53 -7.98,3.26 -68.68,14.69 -82.94,98.43 -14.26,83.74 1.06,115.09 1.06,115.09 0,0 21.14,-55.68 81.02,-59.81 0,0 14.5,-1.03 38.82,-1.42 24.32,-0.39 75.77,-20.65 90.55,-85.62l0.22,-43.44c-2.5,-4.22 -5.49,-8.12 -8.91,-11.66 -3.99,-4.11 -8.11,-8.12 -12.79,-11.45 -2.26,-1.65 -4.65,-3.2 -6.51,-5.33 -1.94,-2.34 -3.33,-5 -4.2,-7.93 -0.71,-2.1 -1.45,-4.2 -2.2,-6.28z"
|
||||
android:fillColor="@color/ic_launcher_background"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -38,6 +38,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
|
||||
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
|
||||
@@ -92,6 +93,7 @@
|
||||
<intent-filter>
|
||||
<data android:scheme="mailto"/>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
@@ -390,6 +392,15 @@
|
||||
android:name=".service.FetchForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.AudioPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1045,31 +1045,31 @@ für die <a href="https://chatmail.at/clients">Chatmail-Clients</a> umfasst, von
|
||||
|
||||
</h3>
|
||||
|
||||
<p>We would like to improve Delta Chat with your help,
|
||||
which is why Delta Chat for Android asks whether you want
|
||||
to send anonymous usage statistics.</p>
|
||||
<p>Wir möchten Delta Chat mit deiner Hilfe verbessern.
|
||||
Deshalb fragt Delta Chat für Android, ob du
|
||||
anonyme Nutzungsstatistiken senden möchtest.</p>
|
||||
|
||||
<p>You can turn it on and off at
|
||||
<strong>Settings → Advanced → Send statistics to Delta Chat’s developers</strong>.</p>
|
||||
<p>Du kannst dies unter
|
||||
<strong>Einstellungen → Erweitert → Statistik an Delta Chat Entwickler senden</strong> ein- und ausschalten.</p>
|
||||
|
||||
<p>When you turn it on,
|
||||
weekly statistics will be automatically sent to a bot.</p>
|
||||
<p>Wenn eingeschaltet,
|
||||
werden wöchentlich Statistiken automatisch an einen Bot gesendet.</p>
|
||||
|
||||
<p>We are interested e.g. in statistics like:</p>
|
||||
<p>Wir sind beispielsweise an folgenden Statistiken interessiert:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>How many contacts are introduced by personally scanning a QR code?</p>
|
||||
<p>Wie viele Kontakte werden durch das persönliche Scannen eines QR-Codes hergestellt?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Which versions of Delta Chat are being used?</p>
|
||||
<p>Welche Versionen von Delta Chat werden verwendet?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>What errors occur for users?</p>
|
||||
<p>Welche Fehler treten bei Benutzern auf?</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>We will <em>not</em> collect any personally identifiable information about you.</p>
|
||||
<p>Wir werden <em>keinerlei</em> personenbezogene Daten über dich sammeln.</p>
|
||||
|
||||
<h3 id="ich-bin-an-technischen-details-interessiert-gibt-es-hierzu-weitere-infos">
|
||||
|
||||
@@ -1107,6 +1107,10 @@ zum Austausch von Verschlüsselungsinformationen durch Scannen von QR-Codes oder
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> wird verwendet, um automatisch eine Ende-zu-Ende-Verschlüsselung zwischen Kontakten und allen Mitgliedern einer Gruppe herzustellen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wir post-quantum-resistente Verschlüsselung und Forward Secrecy einführen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Teilen eines Kontakts im Chat</a>
|
||||
ermöglicht es den Empfängern, eine Ende-zu-Ende-Verschlüsselung mit dem Kontakt zu verwenden.</p>
|
||||
@@ -1280,15 +1284,13 @@ selbst wenn die Nachricht nicht Ende-zu-Ende-verschlüsselt war.</p>
|
||||
speichern Delta-Chat-Apps keine Metadaten über Kontakte oder Gruppen auf Servern. Auch nicht in verschlüsselter Form.
|
||||
Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und ausschließlich auf den Endgeräten der Nutzer gespeichert.</p>
|
||||
|
||||
<p>Servers can therefore only see:</p>
|
||||
<p>Server können daher nur das folgende sehen:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Absender- und Empfängeradressen, standardmäßig zufällig generiert</li>
|
||||
<li>Größe der Nachricht</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>Alle anderen Metadaten zu Nachrichten, Kontakten und Gruppen befinden sich im Ende-zu-Ende-verschlüsselten Teil der Nachrichten.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1299,15 +1301,15 @@ Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und aussch
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Both for protecting against metadata-collecting servers
|
||||
as well as against the threat of device seizure
|
||||
we recommend to use a <a href="https://chatmail.at/relays">chatmail relay</a>
|
||||
to create chat profiles using random addresses for transport.
|
||||
Note that Delta Chat apps on all platforms support multiple profiles
|
||||
so you can easily use situation-specific profiles next to your “main” profile
|
||||
with the knowledge that all their data, along with all metadata, will be deleted.
|
||||
Moreover, if a device is seized then chat contacts using short-lived profiles
|
||||
can not be identified easily.</p>
|
||||
<p>Sowohl zum Schutz vor Servern, die Metadaten sammeln,
|
||||
als auch als Schutz bei Beschlagnahmung von Geräten
|
||||
empfehlen wir die Verwendung eines <a href="https://chatmail.at/relays">Chatmail-Relays</a>,
|
||||
um Chat-Profile mit zufälligen Adressen für den Transport zu erstellen.
|
||||
Beachte, dass Delta-Chat-Apps mehrere Profile unterstützen,
|
||||
sodass du neben deinem „Hauptprofil” ganz einfach situationsspezifische Profile verwenden kannst,
|
||||
mit der Gewissheit, dass alle Daten sowie alle Metadaten gelöscht werden.
|
||||
Darüber hinaus können Chat-Kontakte, die kurzlebige Profile verwenden,
|
||||
im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert werden.</p>
|
||||
|
||||
<h3 id="wer-sieht-meine-ip-adresse">
|
||||
|
||||
@@ -1350,11 +1352,10 @@ um seine Serverinfrastruktur darüber im Unklaren zu lassen, wer eine Nachricht
|
||||
Dies ist besonders wichtig, weil der Signal-Server die Handynummer jedes Kontos kennt,
|
||||
die in der Regel mit einer Passidentität verbunden ist.</p>
|
||||
|
||||
<p>Even if <a href="https://chatmail.at/relays">chatmail relays</a>
|
||||
do not ask for any private data (including no phone numbers),
|
||||
it might still be worthwhile to protect relational metadata between addresses.
|
||||
We don’t foresee bigger problems in using random throw-away addresses for sealed sending
|
||||
but an implementation has not been agreed as a priority yet.</p>
|
||||
<p>Auch wenn <a href="https://chatmail.at/relays">Chatmail-Relays</a>
|
||||
keine privaten Daten (einschließlich Telefonnummern) abfragen,
|
||||
könnte es dennoch sinnvoll sein, Metadaten zwischen Adressen zu schützen.
|
||||
Wir sehen keine größeren Probleme bei der Verwendung von zufälligen Wegwerfadressen für aber eine Umsetzung wurde noch nicht als priorisiert.</p>
|
||||
|
||||
<h3 id="pfs">
|
||||
|
||||
@@ -1364,23 +1365,20 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nein, noch nicht.</p>
|
||||
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
and someone has collected your prior in-transit messages,
|
||||
they will be able to decrypt and read them using the leaked decryption key.
|
||||
Note that Forward Secrecy only increases security if you delete messages.
|
||||
Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
<p>Delta Chat unterstützt derzeit keine Perfect Forward Secrecy (PFS).
|
||||
Das bedeutet, dass, wenn Ihr privater Schlüssel offengelegt wird
|
||||
und jemand Ihre früheren Nachrichten während der Übertragung gesammelt hat,
|
||||
diese mit dem offengelegten Schlüssel entschlüsselt und gelesen werden können.
|
||||
Beachten Sie, dass Forward Secrecy die Sicherheit nur erhöht, wenn du Nachrichten löschst.
|
||||
Andernfalls kann jemand, der deinen Schlüssel erhält,
|
||||
in der Regel auch alle deine nicht gelöschten Nachrichten abrufen
|
||||
und muss zuvor gesammelte Nachrichten nicht einmal entschlüsseln.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wird durch automatische Schlüsselrotation eine zuverlässige Löschung (Forward Secrecy) gewährleisten.
|
||||
Dieser Ansatz ist im Entwurf <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1390,11 +1388,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nein, noch nicht.</p>
|
||||
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat verwendet die Rust OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP Entwurf</a> unterstützt.
|
||||
Wir beabsichtigen, PQC-Unterstützung zum <a href="https://github.com/chatmail/core">chatmail core</a> hinzuzufügen, sobald der Entwurf bei der IETF in Zusammenarbeit mit anderen OpenPGP-Implementierern fertiggestellt ist.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wird eine post-quantum-resistente Verschlüsselung zum Schutz vor Angriffen durch Quantencomputer bieten.
|
||||
Delta Chat verwendet die Rust-OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>,
|
||||
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Kryptografie OpenPGP Entfurf</a> unterstützt.
|
||||
Die Implementierung ist in <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
|
||||
|
||||
<h3 id="wie-kann-ich-die-verschlüsselung-manuell-überprüfen">
|
||||
|
||||
@@ -1420,12 +1420,11 @@ ist die Verbindung sicher.</p>
|
||||
|
||||
<p>Nein.</p>
|
||||
|
||||
<p>Delta Chat generates secure OpenPGP keys according to the Autocrypt specification 1.1.
|
||||
We do not recommend or offer users to perform manual key management.
|
||||
We want to ensure that security audits can focus on a few proven cryptographic algorithms
|
||||
instead of the full breadth of possible algorithms allowed with OpenPGP.
|
||||
If you want to extract your OpenPGP key, there only is an expert method:
|
||||
you need to look it up in the “keypairs” SQLite table of a profile backup tar-file.</p>
|
||||
<p>Delta Chat generiert sichere OpenPGP-Schlüssel gemäß der Autocrypt-Spezifikation 1.1.
|
||||
Wir bieten Benutzern keine manuelle Schlüsselverwaltung an, noch empfehlen diese.
|
||||
Wir wollen sicherstellen, dass sich Sicherheitsüberprüfungen auf einige wenige bewährte kryptografische Algorithmen konzentrieren können,
|
||||
anstatt auf die gesamte Bandbreite der mit OpenPGP zulässigen Algorithmen.
|
||||
Wenn Sie Ihren OpenPGP-Schlüssel extrahieren möchten, gibt es nur eine Methode für Experten: Sie müssen ihn in der SQLite-Tabelle „keypairs” des Backups nachschlagen.</p>
|
||||
|
||||
<h3 id="security-audits">
|
||||
|
||||
@@ -1535,31 +1534,24 @@ Wir nutzen vielmehr öffentliche Finanzierungsquellen, die bisher aus der EU und
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>2023 und 2024 wurden wir in das Next-Generation-Internet-Programm (NGI)
|
||||
für unsere Arbeit an <a href="https://nlnet.nl/project/WebXDC-Push/">Webxdc-PUSH</a> aufgenommen,
|
||||
zusammen mit Kooperationspartnern, die an
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">Webxdc-Evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">Webxdc-XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> und
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Alle diese Projekte sind teilweise abgeschlossen oder sollen Anfang 2025 abgeschlossen werden.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im Jahr 2021 erhielten wir weitere EU-Mittel für zwei “Next-Generation-Internet”-Anträge, nämlich für <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - E-Mail-Provider-Portabilitätsverzeichnis</a> (~97K EUR) und <a href="https://nlnet.nl/project/EmailPorting/">AEAP - E-Mail-Adressportierung</a> (~90K EUR). Ziel sind bessere Unterstützung von Mehrfachkonten, verbesserten QR-Code-Kontakt- und -Gruppen-Setups sowie Netzwerkverbesserungen auf allen Plattformen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Die <a href="https://nlnet.nl/">NLnet-Stiftung</a> bewilligte 2019/2020 46K EUR für die Fertigstellung von Rust-/Python-Bindungs und die Einrichtung eines Chat-Bot-Ökosystems.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>Der <a href="https://opentechfund.org">Open Technology Fund</a> hat Delta Chat erstmals 2018/2019 bezuschusst; mit dieser Förderung (~$200K) wurden hauptsächlich die Android-App verbessert sowie das Release der Desktop-App in einer Betaversion ermöglicht. Basierend auf Nutzererfahrungen im Menschenrechtskontext wurden zudem verschiedene Funktionen entwickelt, siehe unseren Bericht <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Die zweite Förderung 2019/2020 (~$300K) half uns bei der Erstellung der iOS-Version, unsere Kernbibliothek in die Programmiersprache “Rust” zu konvertieren und neue Funktionen für alle Plattformen bereitzustellen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Das EU-Projekt <a href="https://nextleap.eu">NEXTLEAP</a> finanzierte 2017 und 2018 die Entwicklung und Implementierung von “Verifizierten Gruppen” und “Setup Kontakt” und half auch bei der Integration der Ende-zu-Ende-Verschlüsselung durch <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1184,6 +1184,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1365,12 +1369,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1446,7 +1448,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1457,12 +1459,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1472,12 +1471,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1175,6 +1175,9 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, qui est prévu pour 2026, amènera un chiffrement avec résistance post-quantique ainsi que la confidentialité persistante (“forward secrecy”).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
chat</a>
|
||||
@@ -1358,12 +1361,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1418,7 +1419,7 @@ For example, tapping a link exposes IP Addresses to unknown parties and is the b
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Non, pas encore.</p>
|
||||
|
||||
<p>The Signal messenger introduced <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” in 2018</a>
|
||||
to keep their server infrastructure ignorant of who is sending a message to a set of recipients.
|
||||
@@ -1439,7 +1440,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1450,12 +1451,8 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, permettra la suppression fiable (forward secrecy) grâce à une rotation automatique des clefs.
|
||||
Cette approche est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1465,12 +1462,11 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, amènera un chiffrement avec résistance post-quantique pour protéger contre les attaques effectuées par des ordinateurs quantiques.
|
||||
Delta Chat utilise la librairie Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> qui supporte les dernières <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
L’implémentation est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1173,6 +1173,10 @@ per scambiare informazioni sulla configurazione della crittografia tramite la sc
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> viene utilizzato per stabilire
|
||||
automaticamente la crittografia end-to-end tra i contatti e tutti i membri di una chat di gruppo.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
introdurrà una crittografia post-quantistica resistente e una segretezza avanzata.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Condivisione di un contatto con una
|
||||
@@ -1436,7 +1440,7 @@ ma un’implementazione non è stata ancora concordata come priorità.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, non ancora.</p>
|
||||
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat al momento non supporta la tecnologia Perfect Forward Secrecy (PFS).
|
||||
Ciò significa che se la tua chiave di decrittazione privata viene divulgata
|
||||
@@ -1447,12 +1451,9 @@ In caso contrario, chi ottiene le tue chiavi di decrittazione
|
||||
in genere è in grado di ottenere anche tutti i tuoi messaggi non eliminati
|
||||
e non ha nemmeno bisogno di decifrare i messaggi raccolti in precedenza.</p>
|
||||
|
||||
<p>Abbiamo progettato un approccio Forward Secrecy che ha superato
|
||||
l’esame iniziale di alcuni crittografi ed esperti di implementazione
|
||||
ma è in attesa di una stesura più formale
|
||||
per accertarne l’affidabilità nella messaggistica federata e nell’utilizzo su più dispositivi,
|
||||
prima di poter essere implementato in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">clients di chatmail</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
garantirà un’eliminazione affidabile (segretezza in avanti) tramite rotazione automatica delle chiavi.
|
||||
Questo approccio è specificato nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificati OpenPGP di Autocrypt v2</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1462,12 +1463,13 @@ che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">c
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, non ancora.</p>
|
||||
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
offrirà una crittografia post-quantistica resistente per proteggere dagli attacchi ai computer quantistici.
|
||||
Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
che supporta l’ultima <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">bozza IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Il nostro obiettivo è aggiungere il supporto PQC nel <a href="https://github.com/chatmail/core">core di chatmail</a> dopo che la bozza sarà stata finalizzata dall’IETF
|
||||
in collaborazione con altri implementatori di OpenPGP.</p>
|
||||
L’implementazione è specificata nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Certificati OpenPGP Autocrypt v2</a>.</p>
|
||||
|
||||
<h3 id="come-posso-controllare-manualmente-le-informazioni-di-crittografia">
|
||||
|
||||
@@ -1647,31 +1649,31 @@ ordinate cronologicamente:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>Nel 2023 e nel 2024 siamo stati accettati nel programma Next Generation Internet (NGI)
|
||||
per il nostro lavoro in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
insieme ai partner di collaborazione che lavorano su
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> e
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Tutti questi progetti sono parzialmente completati o saranno completati all’inizio del 2025.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Nel 2021 abbiamo ricevuto ulteriori finanziamenti dall’UE per due proposte di Next-Generation-Internet, ovvero per <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - directory di portabilità dei provider di posta elettronica</a> (~97.000 EUR) e <a href="https://nlnet.nl/project/EmailPorting/">AEAP - portabilità degli indirizzi email</a> (~90.000 EUR), che hanno portato a un migliore supporto multi-profilo, a un miglioramento delle impostazioni di contatto e di gruppo tramite codice QR e a numerosi miglioramenti di rete su tutte le piattaforme.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>La <a href="https://nlnet.nl/">fondazione NLnet</a> ha concesso nel 2019/2020 46.000 EUR per
|
||||
completando i collegamenti Rust/Python e avviando un ecosistema Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>L’<a href="https://opentechfund.org">Open Technology Fund</a> ci ha dato una
|
||||
prima sovvenzione 2018/2019 (~$200K) durante la quale abbiamo notevolmente migliorato l’app Android
|
||||
e ha rilasciato una prima versione beta dell’app desktop, e che inoltre
|
||||
ancorato i nostri sviluppi delle funzionalità nella ricerca sulla UX nei contesti dei diritti umani,
|
||||
vedete il nostro <a href="https://delta.chat/en/2019-07-19-uxreport">Rapporto Needfinding e UX</a> conclusivo.
|
||||
La seconda sovvenzione 2019/2020 (~$300K) ci ha aiutato a farlo
|
||||
rilasciare nelle versioni Delta/iOS, per convertire la nostra libreria principale in Rust, e
|
||||
per fornire nuove funzionalità per tutte le piattaforme.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Il progetto UE <a href="https://nextleap.eu">NEXTLEAP</a> ha finanziato la ricerca
|
||||
|
||||
@@ -1182,6 +1182,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1366,12 +1370,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1447,7 +1449,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1458,12 +1460,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1473,12 +1472,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1102,6 +1102,10 @@ weekly statistics will be automatically sent to a bot.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> służy do automatycznego ustanawiania szyfrowania typu end-to-end między kontaktami a wszystkimi członkami czatu grupowego.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Udostępnienie kontaktu na czacie</a> umożliwia odbiorcom korzystanie z szyfrowania typu end-to-end z tym kontaktem.</p>
|
||||
</li>
|
||||
@@ -1236,12 +1240,10 @@ even if the message was not end-to-end encrypted.</p>
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>Wszystkie pozostałe metadane dotyczące wiadomości, kontaktów i grup znajdują się w zaszyfrowanej metodą end-to-end części wiadomości.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1314,11 +1316,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nie, jeszcze nie.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat obecnie nie obsługuje mechanizmu Perfect Forward Secrecy (PFS). Oznacza to, że jeśli twój prywatny klucz deszyfrujący zostanie ujawniony, a ktoś zdobędzie twoje wcześniejsze wiadomości w trakcie transmisji, będzie mógł je odszyfrować i odczytać za pomocą ujawnionego klucza deszyfrującego. Należy pamiętać, że mechanizm Forward Secrecy zwiększa bezpieczeństwo tylko w przypadku usuwania wiadomości. W przeciwnym razie osoba, która uzyska twoje klucze deszyfrujące, zazwyczaj będzie mogła uzyskać dostęp do wszystkich nieusuniętych wiadomości i nie będzie musiała odszyfrowywać żadnych wcześniej zebranych wiadomości.</p>
|
||||
|
||||
<p>Opracowaliśmy metodę Forward Secrecy, która przeszła wstępną analizę niektórych kryptografów i ekspertów ds. wdrożeń, ale oczekuje na bardziej formalne opracowanie, które potwierdzi jej niezawodne działanie w federacyjnym przesyłaniu wiadomości i w przypadku korzystania z wielu urządzeń, zanim zostanie zaimplementowana w <a href="https://github.com/chatmail/core">rdzeniu chatmail</a>, co uczyniłoby ją dostępną we wszystkich <a href="https://chatmail.at/clients">klientach chatmail</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1328,9 +1332,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nie, jeszcze nie.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat korzysta z biblioteki Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>, która obsługuje najnowszy <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">projekt OpenPGP IETF Post-Quantum-Cryptography</a>. Planujemy dodać obsługę PQC do <a href="https://github.com/chatmail/core">rdzenia chatmail</a> po sfinalizowaniu projektu w IETF we współpracy z innymi implementatorami OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="jak-mogę-ręcznie-sprawdzić-informacje-o-szyfrowaniu">
|
||||
|
||||
@@ -1463,32 +1471,17 @@ Raczej korzystamy z publicznych źródeł finansowania, jak dotąd pochodzących
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>W latach 2023 i 2024 zostaliśmy przyjęci do programu Next Generation Internet (NGI) za naszą pracę w <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, wraz z partnerami współpracującymi pracującymi nad <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> i <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Wszystkie te projekty są częściowo ukończone lub zostaną ukończone na początku 2025 r.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>W 2021 r. otrzymaliśmy kolejne dofinansowanie z UE na dwie propozycje dotyczące Internetu nowej generacji, a mianowicie na <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD – katalog przenośności dostawcy poczty e-mail</a> ( ~97 tys. EUR) i <a href="https://nlnet.nl/project/EmailPorting/">AEAP – przenoszenie adresu e-mail</a> (~90 tys. EUR), co zaowocowało lepszą obsługą wielu kont, ulepszonymi kontaktami i ustawieniami grup za pomocą kodów QR oraz wieloma ulepszeniami sieciowymi na wszystkich platformach.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">Fundacja NLnet</a> przekazała w latach 2019/2020 kwotę 46 tys. EUR na wykonanie wiązań Rust/Python i uruchomienie ekosystemu Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> przyznał nam pierwszy grant w 2018/2019 (~200 000 $), dzięki któremu znacznie ulepszyliśmy aplikację na Androida i wydaliśmy pierwszą wersję beta aplikacji na komputery stacjonarne, a także ugruntował rozwój naszych funkcji w badaniach UX w kontekście praw człowieka, zobacz nasz końcowy raport <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX</a>.
|
||||
Druga dotacja w 2019/2020 (~300 000 4) pomogła nam wydać wersje Delta/iOS, przekonwertować naszą podstawową bibliotekę na Rust i zapewnić nowe funkcje dla wszystkich platform.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Unijny projekt <a href="https://nextleap.eu">NEXTLEAP</a> sfinansował badania i wdrożenie zweryfikowanych grup i ustawień protokołów kontaktowych w latach 2017 i 2018, a także pomógł zintegrować szyfrowanie end-to-end poprzez <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1183,6 +1183,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1367,12 +1371,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1448,7 +1450,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1459,12 +1461,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1474,12 +1473,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1181,6 +1181,10 @@ Chatmail использует INBOX по умолчанию для ретран
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> используется для автоматической
|
||||
настройки сквозного шифрования между контактами и всеми членами группового чата.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит поддержку постквантового шифрования и прямой секретности.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Обмен контактом в
|
||||
@@ -1427,7 +1431,7 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Нет, пока нет.</p>
|
||||
|
||||
<p>Мессенджер Signal внедрил функцию <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” (Засекреченный отправитель) в 2018 году</a>,
|
||||
чтобы их серверная инфраструктура не имела информации о том, кто отправляет сообщение группе получателей.
|
||||
@@ -1448,7 +1452,7 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Пока нет, но это будет реализовано в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>На данный момент, Delta Chat не поддерживает Perfect Forward Secrecy (PFS) (Совершенную прямую секретность).
|
||||
Это означает, что если ваш приватный ключ дешифрования будет скомпрометирован,
|
||||
@@ -1459,12 +1463,9 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
также может получить все ваши не удалённые сообщения
|
||||
и ему даже не нужно расшифровывать какие-либо ранее собранные сообщения.</p>
|
||||
|
||||
<p>Мы разработали подход к Forward Secrecy (Прямой секретности), который прошёл
|
||||
первичную проверку некоторыми криптографами и экспертами по реализации
|
||||
но требует более формального описания
|
||||
чтобы убедиться, что он надёжно работает в федеративном обмене сообщениями и при использовании нескольких устройств,
|
||||
прежде чем он может быть внедрён в <a href="https://github.com/chatmail/core">ядро chatmail</a>,
|
||||
что сделает его доступным во всех <a href="https://chatmail.at/clients">клиентах clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит надёжное удаление (прямую секретность) за счёт автоматической ротации ключей.
|
||||
Этот подход описан в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1474,12 +1475,13 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Пока нет, но эта возможность появится в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat использует библиотеку OpenPGP на Rust <a href="https://github.com/rpgp/rpgp">rPGP</a>,
|
||||
которая поддерживает последний <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">черновик IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Мы планируем добавить поддержку PQC в <a href="https://github.com/chatmail/core">ядро chatmail</a> после того, как черновик будет окончательно утвержден в IETF
|
||||
в сотрудничестве с другими разработчиками OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит поддержку постквантового шифрования для защиты от атак с использованием квантовых компьютеров.
|
||||
Delta Chat использует Rust-библиотеку OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
которая поддерживает актуальный черновик IETF <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Особенности реализации описаны в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
|
||||
|
||||
<h3 id="как-можно-вручную-проверить-информацию-о-шифровании">
|
||||
|
||||
@@ -1658,32 +1660,32 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS и macOS App Store, Microsof
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>В 2023 и 2024 годах мы были приняты в программу Next Generation Internet (NGI)
|
||||
за нашу работу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
в сотрудничестве с партнерами, работающими над
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> и
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Все эти проекты частично завершены или будут завершены в начале 2025 года.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В 2021 г. мы получили дополнительное финансирование из ЕС для двух Next-Generation-Internet
|
||||
целей, а именно для <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97 тыс. евро) и <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90 тыс. евро). Это привело к улучшению поддержки нескольких профилей, улучшению настройки контактов и групп с помощью QR-кода и многим улучшениям в сетевом взаимодействии на всех платформах.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Фонд <a href="https://nlnet.nl/">NLnet Foundation</a> выделил в 2019/2020 году 46 тысяч евро на
|
||||
доработку связки Rust/Python и создание экосистемы чат-ботов..</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>Фонд <a href="https://opentechfund.org">Open Technology Fund</a> предоставил нам
|
||||
первый грант в 2018/2019 году (~$200 тыс.), благодаря которому мы существенно улучшили приложение для Android
|
||||
и выпустили первую бета-версию приложения для настольных систем, а также провели
|
||||
исследования в области UX в контексте прав человека,
|
||||
см. наш заключительный отчет <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Второй грант, полученный в 2019/2020 году (~$300 тыс.), помог нам
|
||||
выпустить версии Delta/iOS, перевести наш основной код на Rust и
|
||||
предоставить новые функции для всех платформ.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Проект ЕС <a href="https://nextleap.eu">NEXTLEAP</a> финансировал исследование
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1190,6 +1190,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1374,12 +1378,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1455,7 +1457,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1466,12 +1468,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1481,12 +1480,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
@@ -1675,28 +1675,31 @@ along with collaboration partners working on
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>Më 2021-n morëm financime të mëtejshme nga BE për dy propozime që shtrihen në
|
||||
“Internetin e Brezit Tjetër”, konkretisht për <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97K euro) dhe <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K euro) që sollën mbulim më të mirë për përdorues me shumë
|
||||
llogari, përmirësim të gjërave për kontakte me kod QR dhe grupe, si dhe mjaft
|
||||
përmirësime në punën në rrjet për krejt platformat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">Fondacioni NLnet</a> dhuroi 46K euro gjatë 2019/2020 për
|
||||
plotësimin e <em>Rust/Python bindings</em> dhe për t’i dhënë udhë një ekosistemi
|
||||
Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> na dha grantin e parë
|
||||
për 2018/2019 (~200 mijë dollarë) me të cilin përmirësuam ndjeshëm aplikacionin
|
||||
për Android dhe hodhëm në qarkullim një version të parë beta aplikacioni për Desktop,
|
||||
si dhe i afroi më tepër zhvillimet tona për veçori me kërkime UX në kontekste të drejtash të njeriut,
|
||||
shihni <a href="https://delta.chat/en/2019-07-19-uxreport">raportin tonë përfundimtar “Needfinding and UX”</a>.
|
||||
Granti i dytë për 2019/2020 (~$300K) na ndihmoi të hedhim në qarkullim
|
||||
versione Delta/iOS, për të shndërruar bibliotekën tonë bazë në Rust, si dhe
|
||||
për të sjellë veçori të reja për krejt platformat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
|
||||
and implementation of verified groups and setup contact protocols
|
||||
in 2017 and 2018 and also helped to integrate end-to-end Encryption
|
||||
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
<p>Projekti <a href="https://nextleap.eu">NEXTLEAP</a> i BE-së financoi kërkimin
|
||||
për dhe sendërtimin e grupeve të verifikuara dhe protokolleve të
|
||||
ujdisjes së kontakteve më 2017-n dhe 2018-n dhe ndihmoi gjithashtu
|
||||
të integrohet Fshehtëzim Skaj-më-Skaj përmes <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ndonjëherë marrim dhurime unike nga individë privatë.
|
||||
|
||||
@@ -140,17 +140,10 @@
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you are <strong>face to face</strong> with your friend or family,
|
||||
tap the <strong>QR Code</strong> icon <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
|
||||
on the main screen.<br />
|
||||
Ask your chat partner to <strong>scan</strong> the QR image
|
||||
with their Delta Chat app.</p>
|
||||
<p>Якщо ви перебуваєте <strong>віч-на-віч</strong> зі своїм другом або родиною, торкніться піктограми <strong>QR-код</strong> на головному екрані <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> на головному екрані. Попросіть вашого партнера по чату <strong>сканувати</strong> QR-зображення за допомогою їхнього застосунку Delta Chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For a <strong>remote</strong> contact setup,
|
||||
from the same screen,
|
||||
click “Copy” or “Share” and send the <strong>invite link</strong>
|
||||
through another private chat.</p>
|
||||
<p>Для <strong>віддаленого</strong> налаштування контакту, на тому ж самому екрані, натисніть “Копіювати” або “Поділитися” і відправте <strong>запрошувальне посилання</strong> через інший приватний чат.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1095,6 +1088,10 @@ to send anonymous usage statistics.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> використовується для автоматичного встановлення наскрізного шифрування між контактами і всіма учасниками групового чату.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Поширення контакту в чаті</a> дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
|
||||
</li>
|
||||
@@ -1298,11 +1295,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ні, поки ще ні.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat наразі не підтримує ідеальну пряму секретність (Perfect Forward Secrecy, PFS). Це означає, що якщо ваш приватний ключ для розшифрування буде скомпрометовано, а хтось заздалегідь зібрав ваші повідомлення під час передачі, він зможе розшифрувати та прочитати їх, використовуючи зламаний ключ. Зверніть увагу, що пряма секретність підвищує рівень безпеки лише в тому разі, якщо ви видаляєте повідомлення. Інакше, якщо хтось отримує доступ до ваших ключів розшифрування, він зазвичай також має доступ до всіх ваших невидалених повідомлень і навіть не потребує розшифровувати заздалегідь перехоплені дані.</p>
|
||||
|
||||
<p>Ми розробили підхід Forward Secrecy, який витримав початкову експертизу від деяких криптографів та експертів з реалізації але чекає на більш офіційний звіт щоб переконатися, що він надійно працює в об’єднаних системах обміну повідомленнями та при використанні декількох пристроїв, перш ніж його можна буде реалізувати в <a href="https://github.com/chatmail/core">ядрі чату</a>, що зробить його доступним у всіх <a href="https://chatmail.at/clients">клієнтах чату</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1312,9 +1311,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ні, поки ще ні.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat використовує бібліотеку Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> яка підтримує останню версію <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>. Ми плануємо додати підтримку PQC у <a href="https://github.com/chatmail/core">chatmail core</a> після того, як проект буде завершено у IETF у співпраці з іншими розробниками OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="як-я-можу-вручну-перевірити-інформацію-про-шифрування">
|
||||
|
||||
@@ -1445,32 +1448,24 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>У 2023 та 2024 роках нас прийняли до програми Next Generation Internet (NGI) за нашу роботу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, а також у співпраці з партнерами, які працюють над <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> та <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Усі ці проєкти частково завершені або будуть завершені на початку 2025 року.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>У 2021 році ми отримали подальше фінансування від ЄС на дві пропозиції щодо Інтернету наступного покоління а саме на <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - каталог перенесення провайдерів електронної пошти</a> (~97 тис. євро) та <a href="https://nlnet.nl/project/EmailPorting/">AEAP - перенесення адрес електронної пошти</a> (~90 тис. євро), що дозволило нам покращити багатопрофільну підтримку, вдосконалити налаштування контактів та груп за допомогою QR-коду та багато інших мережевих покращень на всіх платформах.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Фонд <a href="https://nlnet.nl/">NLnet</a> виділив у 2019/2020 роках 46 тисяч євро на
|
||||
завершення прив’язок Rust/Python та запуск екосистеми чат-ботів.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> надав нам два гранти.
|
||||
Перший грант 2018/2019 року (~$200K), допоміг значно покращили додаток для Android
|
||||
і випустили першу бета-версію додатка для ПК, і який до того ж
|
||||
закріпив наші розробки функцій у дослідженнях UX у контексті прав людини,
|
||||
дивіться наш підсумковий звіт <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Другий грант 2019/2020 року (~$300K) допоміг нам
|
||||
випустити Delta/iOS версію, конвертувати нашу основному бібліотеку на Rust,
|
||||
і додати нові функції для всіх платформ.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Проект ЄС <a href="https://nextleap.eu">NEXTLEAP</a> фінансував дослідження та впровадження верифікованих груп і протоколів встановлення контактів у 2017 та 2018 роках, а також допоміг інтегрувати наскрізне шифрування через <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1161,6 +1161,10 @@ weekly statistics will be automatically sent to a bot.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
用于在联系人和群聊的所有成员之间自动建立端到端加密。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">将联系人分享到聊天中
|
||||
@@ -1342,12 +1346,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1423,7 +1425,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1434,12 +1436,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1449,12 +1448,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
@@ -1624,32 +1624,22 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>在 2023 年和 2024 年,我们的 <a href="https://nlnet.nl/project/WebXDC-Push/">WebXDC PUSH</a> 工作已在下一代互联网 (NGI) 中获得认可,
|
||||
并与致力于
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">WebXDC evolve</a>、
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">WebXDC XMPP</a>、
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> 和
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a> 的合作伙伴合作。
|
||||
所有这些项目都已部分完成或将在 2025 年初完成。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>在 2021 年,我们从两项下一代互联网提案收到了欧盟的进一步资助,即 <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - 电子邮件提供商可移植性目录</a>(约 9.7 万欧元)和 <a href="https://nlnet.nl/project/EmailPorting/">AEAP - 电子邮件地址移植</a>(约 9 万欧元)。这带来了更好的多账户支持,改进的二维码联系人和群组设置,和所有平台上的多处网络改进。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">NLnet 基金会</a> 2019/2020 年拨款 4.6 万欧元,用于完成 Rust/Python 绑定并建立聊天机器人生态系统。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>在<a href="https://opentechfund.org">开放技术基金</a> 2018/2019 年提供的第一笔赠款(约 20 万美元)期间,我们显著改善了安卓应用,发布了第一个桌面测试版,并根据人权方面的用户体验研究进行了功能开发,请参阅我们的结论<a href="https://delta.chat/en/2019-07-19-uxreport">《需求发现与用户体验报告》</a>。2019/2020 年的第二笔赠款(约 30 万美元)对发布 Delta/iOS 版本,将核心库转换到 Rust ,以及为所有平台开发新功能提供了帮助。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://nextleap.eu">NEXTLEAP</a>欧盟项目资助了以下研究和实施工作:在 2017 年和 2018 年实施的验证组和设置联系协议和通过 <a href="https://autocrypt.org">Autocrypt</a>整合了端到端加密。</p>
|
||||
|
||||
@@ -682,7 +682,8 @@ public class Rpc {
|
||||
* Set group name.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
@@ -690,11 +691,37 @@ public class Rpc {
|
||||
transport.call("set_chat_name", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group or broadcast channel description.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
* <p>
|
||||
* See also [`Self::get_chat_description`] / `getChatDescription()`.
|
||||
*/
|
||||
public void setChatDescription(Integer accountId, Integer chatId, String description) throws RpcException {
|
||||
transport.call("set_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the chat description from the database.
|
||||
* <p>
|
||||
* UIs show this in the profile page of the chat,
|
||||
* it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
|
||||
*/
|
||||
public String getChatDescription(Integer accountId, Integer chatId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "get_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
* <p>
|
||||
@@ -1315,8 +1342,8 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/** Starts an outgoing call. */
|
||||
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo));
|
||||
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo, Boolean hasVideo) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo), mapper.valueToTree(hasVideo));
|
||||
}
|
||||
|
||||
/** Accepts an incoming call. */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
package chat.delta.rpc.types;
|
||||
|
||||
public class CallInfo {
|
||||
/** True if SDP offer has a video. */
|
||||
/** True if the call is started as a video call. */
|
||||
public Boolean hasVideo;
|
||||
/**
|
||||
* SDP offer.
|
||||
|
||||
@@ -4,6 +4,7 @@ package chat.delta.rpc.types;
|
||||
public enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
GroupDescriptionChanged,
|
||||
GroupImageChanged,
|
||||
MemberAddedToGroup,
|
||||
MemberRemovedFromGroup,
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcAccounts {
|
||||
|
||||
public DcAccounts(String dir) {
|
||||
accountsCPtr = createAccountsCPtr(dir);
|
||||
public DcAccounts(String dir, DcEventChannel channel) {
|
||||
accountsCPtr = createAccountsCPtr(dir, channel);
|
||||
if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer");
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class DcAccounts {
|
||||
|
||||
// working with raw c-data
|
||||
private long accountsCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native long createAccountsCPtr (String dir);
|
||||
private native long createAccountsCPtr (String dir, DcEventChannel channel);
|
||||
private native void unrefAccountsCPtr ();
|
||||
private native long getEventEmitterCPtr ();
|
||||
private native long getJsonrpcInstanceCPtr ();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
public class DcEventChannel {
|
||||
|
||||
public DcEventChannel() {
|
||||
eventChannelCPtr = createEventChannelCPtr();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
if (eventChannelCPtr != 0) {
|
||||
unrefEventChannelCPtr();
|
||||
eventChannelCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public DcEventEmitter getEventEmitter() {
|
||||
return new DcEventEmitter(getEventEmitterCPtr());
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long eventChannelCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native long createEventChannelCPtr ();
|
||||
private native void unrefEventChannelCPtr ();
|
||||
private native long getEventEmitterCPtr ();
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
@@ -18,11 +26,13 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -30,6 +40,7 @@ import java.util.ArrayList;
|
||||
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
implements DcEventCenter.DcEventDelegate
|
||||
{
|
||||
private static final String TAG = AllMediaActivity.class.getSimpleName();
|
||||
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String CONTACT_ID_EXTRA = "contact_id";
|
||||
@@ -57,6 +68,10 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme = new DynamicNoActionBarTheme();
|
||||
@@ -91,11 +106,19 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -124,6 +147,40 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken = new SessionToken(this,
|
||||
new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
|
||||
.buildAsync();
|
||||
mediaControllerFuture.addListener(() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
addActivityContext(
|
||||
this.getIntent().getExtras(),
|
||||
this.getClass().getName()
|
||||
);
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
private boolean isGlobalGallery() {
|
||||
return contactId==0 && chatId==0;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import androidx.annotation.NonNull;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -31,7 +32,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
|
||||
private BucketedThreadMedia media;
|
||||
private BucketedThreadMedia media;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
||||
private final DocumentView documentView;
|
||||
@@ -71,6 +73,10 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
@@ -97,7 +103,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
viewHolder.webxdcView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.audioView.setVisibility(View.VISIBLE);
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
|
||||
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide);
|
||||
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.audioView.disablePlayer(!selected.isEmpty());
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -25,6 +26,7 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
||||
@@ -72,9 +74,11 @@ public class AllMediaDocumentsFragment
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
|
||||
|
||||
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this));
|
||||
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this);
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
@@ -239,12 +243,13 @@ public class AllMediaDocumentsFragment
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
if (itemId == R.id.details) {
|
||||
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.delete) {
|
||||
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
|
||||
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.share) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.work.WorkManager;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcEventChannel;
|
||||
import com.b44t.messenger.DcEventEmitter;
|
||||
import com.b44t.messenger.FFITransport;
|
||||
|
||||
@@ -164,7 +165,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(stringWriter, true));
|
||||
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
|
||||
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + " Crash Report";
|
||||
errorMsg += "\n" + LogViewFragment.grabLogcat();
|
||||
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
|
||||
@@ -195,7 +197,29 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
Util.runOnBackground(() -> {
|
||||
synchronized (initLock) {
|
||||
try {
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
|
||||
DcEventChannel eventChannel = new DcEventChannel();
|
||||
DcEventEmitter emitter = eventChannel.getEventEmitter();
|
||||
eventCenter = new DcEventCenter(this);
|
||||
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event == null) {
|
||||
break;
|
||||
}
|
||||
if (isInitialized) {
|
||||
eventCenter.handleEvent(event);
|
||||
} else {
|
||||
// not fully initialized, only handle logging events,
|
||||
// ex. account migrations during DcAccounts initialization
|
||||
eventCenter.handleLogging(event);
|
||||
}
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
}, "eventThread").start();
|
||||
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
|
||||
Log.i(TAG, "DcAccounts created");
|
||||
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
|
||||
Log.i(TAG, "Rpc created");
|
||||
@@ -235,28 +259,12 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
dcContext = dcAccounts.getSelectedAccount();
|
||||
notificationCenter = new NotificationCenter(this);
|
||||
eventCenter = new DcEventCenter(this);
|
||||
dcLocationManager = new DcLocationManager(this, dcContext);
|
||||
|
||||
// Mark as initialized before starting threads that depend on it
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
Log.i(TAG, "DcAccounts initialization complete");
|
||||
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
DcEventEmitter emitter = dcAccounts.getEventEmitter();
|
||||
Log.i(TAG, "DcEventEmitter obtained");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event==null) {
|
||||
break;
|
||||
}
|
||||
eventCenter.handleEvent(event);
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
}, "eventThread").start();
|
||||
|
||||
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
|
||||
DcHelper.setStockTranslations(this);
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
this.rpc = DcHelper.getRpc(context);
|
||||
}
|
||||
|
||||
protected void bind(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
protected void bindPartial(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.dcChat = dcChat;
|
||||
@@ -126,6 +126,8 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
|
||||
public void onClick(View v) {
|
||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
// The click workaround on ConversationItem shall be revised.
|
||||
// In fact, it is probably better rethinking accessibility approach for the items.
|
||||
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
|
||||
BaseConversationItem.this.onAccessibilityClick();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.annotation.Nullable;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
@@ -17,7 +19,9 @@ public interface BindableConversationItem extends Unbindable {
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight);
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener);
|
||||
|
||||
DcMsg getMessageRecord();
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.List;
|
||||
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
|
||||
|
||||
public static final String CONTACTS_EXTRA = "contacts_extra";
|
||||
public static final String DESELECTED_CONTACTS_EXTRA = "deselected_contacts_extra";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
@@ -71,7 +72,9 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
|
||||
private void saveSelection() {
|
||||
Intent resultIntent = getIntent();
|
||||
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<Integer> deselectedContacts = contactsFragment.getDeselectedContacts();
|
||||
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
|
||||
resultIntent.putIntegerArrayListExtra(DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts));
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -58,6 +60,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -83,12 +86,14 @@ public class ContactSelectionListFragment extends Fragment
|
||||
private DcContext dcContext;
|
||||
|
||||
private Set<Integer> selectedContacts;
|
||||
private Set<Integer> deselectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private TextView emptyView;
|
||||
private ActionMode actionMode;
|
||||
private ActionMode.Callback actionModeCallback;
|
||||
private ActivityResultLauncher<Intent> newContactLauncher;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle icicle) {
|
||||
@@ -96,6 +101,22 @@ public class ContactSelectionListFragment extends Fragment
|
||||
|
||||
dcContext = DcHelper.getContext(getActivity());
|
||||
DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
|
||||
// Register activity result launcher
|
||||
newContactLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
int contactId = result.getData().getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
|
||||
if (contactId != 0) {
|
||||
selectedContacts.add(contactId);
|
||||
deselectedContacts.remove(contactId);
|
||||
}
|
||||
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
initializeCursor();
|
||||
}
|
||||
|
||||
@@ -127,6 +148,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.contact_list, menu);
|
||||
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
|
||||
updateActionModeState(actionMode);
|
||||
return true;
|
||||
}
|
||||
@@ -226,6 +248,15 @@ public class ContactSelectionListFragment extends Fragment
|
||||
return selected;
|
||||
}
|
||||
|
||||
public @NonNull List<Integer> getDeselectedContacts() {
|
||||
List<Integer> deselected = new LinkedList<>();
|
||||
if (deselectedContacts != null) {
|
||||
deselected.addAll(deselectedContacts);
|
||||
}
|
||||
|
||||
return deselected;
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
@@ -241,6 +272,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
isMulti(),
|
||||
true);
|
||||
selectedContacts = adapter.getSelectedContacts();
|
||||
deselectedContacts = new HashSet<>();
|
||||
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
|
||||
if(preselectedContacts!=null) {
|
||||
selectedContacts.addAll(preselectedContacts);
|
||||
@@ -301,7 +333,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
intent.putExtra(NewContactActivity.ADDR_EXTRA, cursorFilter);
|
||||
}
|
||||
if (isMulti()) {
|
||||
startActivityForResult(intent, CONTACT_ADDR_RESULT_CODE);
|
||||
newContactLauncher.launch(intent);
|
||||
} else {
|
||||
requireContext().startActivity(intent);
|
||||
}
|
||||
@@ -309,12 +341,14 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
selectedContacts.add(contactId);
|
||||
deselectedContacts.remove(contactId);
|
||||
contact.setChecked(true);
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contactId);
|
||||
}
|
||||
} else {
|
||||
selectedContacts.remove(contactId);
|
||||
deselectedContacts.add(contactId);
|
||||
contact.setChecked(false);
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contactId);
|
||||
@@ -347,16 +381,4 @@ public class ContactSelectionListFragment extends Fragment
|
||||
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
if (resultCode == Activity.RESULT_OK && reqCode == CONTACT_ADDR_RESULT_CODE) {
|
||||
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
|
||||
if (contactId != 0) {
|
||||
selectedContacts.add(contactId);
|
||||
}
|
||||
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
@@ -65,7 +66,12 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
@@ -76,7 +82,7 @@ import com.b44t.messenger.DcMsg;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
@@ -86,6 +92,8 @@ import org.thoughtcrime.securesms.components.InputPanel;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||
import org.thoughtcrime.securesms.components.ScaleStableImageView;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -104,19 +112,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -138,14 +146,13 @@ import chat.delta.util.SettableFuture;
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener
|
||||
{
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener, AudioView.OnActionListener {
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
@@ -185,6 +192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private MediaKeyboard emojiPicker;
|
||||
protected HidingLinearLayout quickAttachmentToggle;
|
||||
private InputPanel inputPanel;
|
||||
private @Nullable MediaController mediaController;
|
||||
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private ApplicationContext context;
|
||||
private Recipient recipient;
|
||||
@@ -217,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeActionBar();
|
||||
initializeViews();
|
||||
initializeResources();
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
|
||||
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -267,6 +281,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken = new SessionToken(this,
|
||||
new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
|
||||
.buildAsync();
|
||||
mediaControllerFuture.addListener(() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -337,7 +381,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DcHelper.getNotificationCenter(this).clearVisibleChat();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
||||
inputPanel.onPause();
|
||||
AudioSlidePlayer.stopAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -357,6 +400,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -569,8 +617,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
} else if (itemId == R.id.menu_show_map) {
|
||||
WebxdcActivity.openMaps(this, chatId);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_call) {
|
||||
CallUtil.startCall(this, chatId);
|
||||
} else if (itemId == R.id.menu_start_audio_call) {
|
||||
CallUtil.startCall(this, chatId, false);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_video_call) {
|
||||
CallUtil.startCall(this, chatId, true);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_all_media) {
|
||||
handleAllMedia();
|
||||
@@ -631,6 +682,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
|
||||
}
|
||||
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
|
||||
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -1004,20 +1057,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient = new Recipient(this, dcChat);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(true);
|
||||
initializeContactRequest();
|
||||
}
|
||||
|
||||
private void setComposePanelVisibility() {
|
||||
private void setComposePanelVisibility(boolean isInitialization) {
|
||||
if (dcChat.canSend()) {
|
||||
composePanel.setVisibility(View.VISIBLE);
|
||||
attachmentManager.setHidden(false);
|
||||
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
|
||||
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
|
||||
// fragment.handleRemoveBottomInsets();
|
||||
} else {
|
||||
composePanel.setVisibility(View.GONE);
|
||||
attachmentManager.setHidden(true);
|
||||
hideSoftKeyboard();
|
||||
inputPanel.setSubjectVisible(false);
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
|
||||
/*
|
||||
if (isInitialization) {
|
||||
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
|
||||
fragment.handleAddBottomInsets();
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,11 +1117,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private void addAttachmentContactInfo(int contactId) {
|
||||
@@ -1110,6 +1173,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel.clearSubject();
|
||||
}
|
||||
|
||||
// Stop draft audio playback regardless, since it is unlikely
|
||||
// we will need background playback for drafts
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcMsg msg = null;
|
||||
@@ -1422,6 +1489,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Listeners
|
||||
|
||||
@Override
|
||||
public void onPlayPauseButtonClicked(View view) {
|
||||
addActivityContext(
|
||||
this.getIntent().getExtras(),
|
||||
this.getClass().getName()
|
||||
);
|
||||
}
|
||||
|
||||
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
|
||||
@Override
|
||||
public void onClick(int type) {
|
||||
@@ -1590,7 +1665,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dcChat = dcContext.getChat(chatId);
|
||||
titleView.setTitle(glideRequests, dcChat);
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(false);
|
||||
initializeContactRequest();
|
||||
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|
||||
|| eventId == DcContext.DC_EVENT_MSG_READ)
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -59,6 +61,7 @@ import java.util.Set;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
// FIXME: this breaks type checks, that is why there are so many casts.
|
||||
public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
extends RecyclerView.Adapter
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
||||
@@ -97,6 +100,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
private long pulseHighlightingSince = -1;
|
||||
private int lastSeenPosition = -1;
|
||||
private long lastSeen = -1;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
private AudioView.OnActionListener audioPlayPauseListener;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
||||
@@ -170,6 +175,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
return fromDb;
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
|
||||
this.audioPlayPauseListener = audioPlayPauseListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the message with msgId in the chat list, counted from the top
|
||||
*/
|
||||
@@ -237,7 +250,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
long elapsed = now - pulseHighlightingSince;
|
||||
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
|
||||
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -41,6 +41,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
@@ -52,6 +53,7 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -85,7 +87,6 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
private static final String TAG = ConversationFragment.class.getSimpleName();
|
||||
|
||||
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 77;
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
|
||||
@@ -101,6 +102,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
private StickyHeaderDecoration dateDecoration;
|
||||
private View scrollToBottomButton;
|
||||
private View floatingLocationButton;
|
||||
private View bottomDivider;
|
||||
private AddReactionView addReactionView;
|
||||
private TextView noMessageTextView;
|
||||
private Timer reloadTimer;
|
||||
@@ -108,6 +110,8 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
public boolean isPaused;
|
||||
private Debouncer markseenDebouncer;
|
||||
private Rpc rpc;
|
||||
private boolean pendingAddBottomInsets;
|
||||
private boolean pendingRemoveBottomInsets;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
@@ -141,6 +145,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
|
||||
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
|
||||
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
|
||||
bottomDivider = ViewUtil.findById(view, R.id.bottom_divider);
|
||||
|
||||
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
|
||||
|
||||
@@ -159,18 +164,32 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
|
||||
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
|
||||
if (pendingAddBottomInsets) {
|
||||
bottomDivider.setVisibility(View.GONE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true);
|
||||
pendingAddBottomInsets = false;
|
||||
}
|
||||
|
||||
if (pendingRemoveBottomInsets) {
|
||||
bottomDivider.setVisibility(View.VISIBLE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false);
|
||||
pendingRemoveBottomInsets = false;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initializeResources();
|
||||
initializeListAdapter();
|
||||
}
|
||||
|
||||
private void setNoMessageText() {
|
||||
private void setNoMessageText() {
|
||||
DcChat dcChat = getListAdapter().getChat();
|
||||
if(dcChat.isMultiUser()){
|
||||
if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) {
|
||||
@@ -197,6 +216,28 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
}
|
||||
}
|
||||
|
||||
public void handleAddBottomInsets() {
|
||||
if (bottomDivider != null) {
|
||||
bottomDivider.setVisibility(View.GONE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true);
|
||||
pendingAddBottomInsets = false;
|
||||
} else {
|
||||
pendingAddBottomInsets = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRemoveBottomInsets() {
|
||||
if (bottomDivider != null) {
|
||||
bottomDivider.setVisibility(View.VISIBLE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false);
|
||||
pendingRemoveBottomInsets = false;
|
||||
} else {
|
||||
pendingRemoveBottomInsets = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
DcHelper.getEventCenter(getContext()).removeObservers(this);
|
||||
@@ -291,6 +332,10 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
if (this.recipient != null && this.chatId != -1) {
|
||||
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
|
||||
list.setAdapter(adapter);
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
adapter.setPlaybackViewModel(playbackViewModel);
|
||||
adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity()));
|
||||
|
||||
if (dateDecoration != null) {
|
||||
list.removeItemDecoration(dateDecoration);
|
||||
@@ -407,7 +452,14 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
}
|
||||
|
||||
public void handleClearChat() {
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
|
||||
handleDeleteMessages(
|
||||
(int) chatId,
|
||||
getListAdapter().getMessageIds(),
|
||||
playbackViewModel::stopByIds,
|
||||
playbackViewModel::stopByIds);
|
||||
}
|
||||
|
||||
private ConversationAdapter getListAdapter() {
|
||||
@@ -891,22 +943,11 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
|
||||
@Override
|
||||
public void onReactionClicked(DcMsg messageRecord) {
|
||||
ReactionsDetailsFragment dialog = new ReactionsDetailsFragment(messageRecord.getId());
|
||||
ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId());
|
||||
dialog.show(getActivity().getSupportFragmentManager(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
|
||||
// ApplicationContext.getInstance(getContext().getApplicationContext())
|
||||
// .getJobManager()
|
||||
// .add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
@Override
|
||||
@@ -940,12 +981,15 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
hideAddReactionView();
|
||||
int itemId = item.getItemId();
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
|
||||
if (itemId == R.id.menu_context_copy) {
|
||||
handleCopyMessage(getListAdapter().getSelectedItems());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_context_delete_message) {
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_context_share) {
|
||||
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
|
||||
|
||||
@@ -41,8 +41,7 @@ import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.CallItemView;
|
||||
@@ -52,6 +51,8 @@ import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -71,7 +72,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -181,9 +181,11 @@ public class ConversationItem extends BaseConversationItem
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight)
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
this.glideRequests = glideRequests;
|
||||
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
|
||||
|
||||
@@ -204,7 +206,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
setGutterSizes(messageRecord, showSender);
|
||||
setMessageShape(messageRecord);
|
||||
setMediaAttributes(messageRecord, showSender);
|
||||
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
|
||||
setBodyText(messageRecord);
|
||||
setBubbleState(messageRecord);
|
||||
setContactPhoto();
|
||||
@@ -482,24 +484,10 @@ public class ConversationItem extends BaseConversationItem
|
||||
}
|
||||
|
||||
private void setMediaAttributes(@NonNull DcMsg messageRecord,
|
||||
boolean showSender)
|
||||
boolean showSender,
|
||||
AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
class SetDurationListener implements AudioSlidePlayer.Listener {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onStop() {}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
messageRecord.lateFilingMediaSize(0,0, millis);
|
||||
audioViewStub.get().setDuration(millis);
|
||||
}
|
||||
}
|
||||
if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
@@ -509,15 +497,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
|
||||
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
int duration = messageRecord.getDuration();
|
||||
if (duration == 0) {
|
||||
AudioSlide audio = new AudioSlide(context, messageRecord);
|
||||
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
|
||||
audioSlidePlayer.requestDuration();
|
||||
}
|
||||
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
|
||||
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
|
||||
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord));
|
||||
audioViewStub.get().setOnClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
@@ -1000,9 +982,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
int chatId = messageRecord.getChatId();
|
||||
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
|
||||
int callId = messageRecord.getId();
|
||||
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
|
||||
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer, callInfo.hasVideo);
|
||||
} else {
|
||||
CallUtil.startCall(getContext(), accId, chatId);
|
||||
CallUtil.startCall(getContext(), accId, chatId, callInfo.hasVideo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
public static final String CLEAR_NOTIFICATIONS = "clear_notifications";
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
public static final String FROM_WELCOME = "from_welcome";
|
||||
public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr";
|
||||
private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1;
|
||||
|
||||
private ConversationListFragment conversationListFragment;
|
||||
@@ -193,6 +194,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
if (BuildConfig.DEBUG) checkNdkArchitecture();
|
||||
|
||||
DcHelper.maybeShowMigrationError(this);
|
||||
|
||||
String rawQrString = getIntent().getStringExtra(FROM_WELCOME_RAW_QR);
|
||||
// Launch chat directly, if coming from onboarding with a join chat/group QR
|
||||
if (rawQrString != null) {
|
||||
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
|
||||
qrCodeHandler.secureJoinByQr(rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,7 +629,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
refreshAvatar();
|
||||
refreshUnreadIndicator();
|
||||
refreshTitle();
|
||||
conversationListFragment.loadChatlist();
|
||||
conversationListFragment.loadChatlistAsync();
|
||||
}
|
||||
|
||||
public void onDeleteProfile(int profileId) {
|
||||
|
||||
@@ -233,7 +233,7 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
private final Object loadChatlistLock = new Object();
|
||||
private boolean inLoadChatlist;
|
||||
private boolean needsAnotherLoad;
|
||||
private void loadChatlistAsync() {
|
||||
public void loadChatlistAsync() {
|
||||
synchronized (loadChatlistLock) {
|
||||
needsAnotherLoad = true;
|
||||
if (inLoadChatlist) {
|
||||
@@ -260,7 +260,7 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
});
|
||||
}
|
||||
|
||||
public void loadChatlist() {
|
||||
private void loadChatlist() {
|
||||
int listflags = 0;
|
||||
if (archive) {
|
||||
listflags |= DcContext.DC_GCL_ARCHIVED_ONLY;
|
||||
|
||||
@@ -271,12 +271,6 @@ public class ConversationListItem extends RelativeLayout
|
||||
} else {
|
||||
deliveryStatusIndicator.setNone();
|
||||
}
|
||||
|
||||
if (state == DcMsg.DC_STATE_OUT_FAILED) {
|
||||
deliveryStatusIndicator.setTint(Color.RED);
|
||||
} else {
|
||||
deliveryStatusIndicator.resetTint();
|
||||
}
|
||||
}
|
||||
|
||||
int unreadCount = thread.getUnreadCount();
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
@@ -61,9 +63,11 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean pulseUpdate)
|
||||
boolean pulseUpdate,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
setGenericInfoRecord(messageRecord);
|
||||
}
|
||||
|
||||
@@ -119,12 +123,6 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
|
||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
||||
else deliveryStatusView.setNone();
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
deliveryStatusView.setTint(Color.RED);
|
||||
} else {
|
||||
deliveryStatusView.setTint(textColor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -44,12 +45,14 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ItemClickListener
|
||||
{
|
||||
|
||||
private static final String TAG = GroupCreateActivity.class.getSimpleName();
|
||||
public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id";
|
||||
public static final String CREATE_BROADCAST = "create_broadcast";
|
||||
public static final String UNENCRYPTED = "unencrypted";
|
||||
@@ -63,6 +66,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
private boolean unencrypted;
|
||||
private boolean broadcast;
|
||||
private EditText groupName;
|
||||
private EditText chatDescription;
|
||||
private ListView lv;
|
||||
private ImageView avatar;
|
||||
private Bitmap avatarBmp;
|
||||
@@ -140,6 +144,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
|
||||
avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
groupName = ViewUtil.findById(this, R.id.group_name);
|
||||
chatDescription = ViewUtil.findById(this, R.id.chat_description);
|
||||
TextView chatHints = ViewUtil.findById(this, R.id.chat_hints);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
@@ -178,6 +183,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
} else if (unencrypted) {
|
||||
avatar.setVisibility(View.GONE);
|
||||
groupName.setHint(R.string.subject);
|
||||
findViewById(R.id.chat_description_container).setVisibility(View.GONE);
|
||||
chatHints.setVisibility(View.GONE);
|
||||
} else {
|
||||
chatHints.setVisibility(View.GONE);
|
||||
@@ -186,6 +192,14 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
if(isEdit()) {
|
||||
groupName.setText(dcContext.getChat(groupChatId).getName());
|
||||
lv.setVisibility(View.GONE);
|
||||
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
try {
|
||||
String description = rpc.getChatDescription(rpc.getSelectedAccountId(), groupChatId);
|
||||
chatDescription.setText(description);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,22 +275,22 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void createGroup(String groupName) {
|
||||
if (broadcast) {
|
||||
try {
|
||||
groupChatId = DcHelper.getRpc(this).createBroadcast(dcContext.getAccountId(), groupName);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
int accId;
|
||||
try {
|
||||
accId = rpc.getSelectedAccountId();
|
||||
if (broadcast) {
|
||||
groupChatId = rpc.createBroadcast(accId, groupName);
|
||||
} else if (unencrypted) {
|
||||
groupChatId = rpc.createGroupChatUnencrypted(accId, groupName);
|
||||
} else {
|
||||
groupChatId = rpc.createGroupChat(accId, groupName, false);
|
||||
}
|
||||
} else if (unencrypted) {
|
||||
try {
|
||||
groupChatId = DcHelper.getRpc(this).createGroupChatUnencrypted(dcContext.getAccountId(), groupName);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
groupChatId = dcContext.createGroupChat(groupName);
|
||||
|
||||
rpc.setChatDescription(accId, groupChatId, getChatDescription());
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int contactId : getAdapter().getContacts()) {
|
||||
@@ -307,6 +321,14 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
dcContext.setChatName(groupChatId, groupName);
|
||||
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
String description = getChatDescription();
|
||||
try {
|
||||
rpc.setChatDescription(rpc.getSelectedAccountId(), groupChatId, description);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
|
||||
if (avatarChanged) AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
|
||||
|
||||
attachmentManager.cleanup();
|
||||
@@ -331,6 +353,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
return ret;
|
||||
}
|
||||
|
||||
private @Nullable String getChatDescription() {
|
||||
return chatDescription.getText() != null ? chatDescription.getText().toString().trim() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
@@ -63,6 +63,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
@@ -79,6 +81,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private ImageView avatar;
|
||||
private EditText name;
|
||||
private TextView invitationText;
|
||||
private TextView privacyPolicyBtn;
|
||||
private Button signUpBtn;
|
||||
|
||||
@@ -86,7 +89,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
private boolean imageLoaded;
|
||||
private String providerHost;
|
||||
private String providerQrData;
|
||||
private String rawQrData;
|
||||
private DcLot parsedQrData;
|
||||
private boolean isDcLogin;
|
||||
private boolean isContactInvitation;
|
||||
private boolean isGroupInvitation;
|
||||
|
||||
private AttachmentManager attachmentManager;
|
||||
private Bitmap avatarBmp;
|
||||
@@ -96,6 +103,8 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private DcContext dcContext;
|
||||
|
||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -206,17 +215,31 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private void setProviderFromQr(String rawQr) {
|
||||
DcLot qrParsed = dcContext.checkQr(rawQr);
|
||||
boolean isDcLogin = qrParsed.getState() == DcContext.DC_QR_LOGIN;
|
||||
if (isDcLogin || qrParsed.getState() == DcContext.DC_QR_ACCOUNT) {
|
||||
this.isDcLogin = isDcLogin;
|
||||
providerHost = qrParsed.getText1();
|
||||
providerQrData = rawQr;
|
||||
updateProvider();
|
||||
} else {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
switch (qrParsed.getState()) {
|
||||
case DcContext.DC_QR_LOGIN:
|
||||
isDcLogin = true; // Intentional fall-through
|
||||
case DcContext.DC_QR_ACCOUNT:
|
||||
providerHost = qrParsed.getText1();
|
||||
providerQrData = rawQr;
|
||||
updateProvider();
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_VERIFYCONTACT:
|
||||
isContactInvitation = true;
|
||||
rawQrData = rawQr;
|
||||
parsedQrData = qrParsed;
|
||||
updateProvider();
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_VERIFYGROUP:
|
||||
isGroupInvitation = true;
|
||||
rawQrData = rawQr;
|
||||
parsedQrData = qrParsed;
|
||||
updateProvider();
|
||||
break;
|
||||
default:
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +283,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
private void handleIntent() {
|
||||
@@ -311,6 +335,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
private void initializeResources() {
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.name = findViewById(R.id.name_text);
|
||||
this.invitationText = findViewById(R.id.invitation_label);
|
||||
this.privacyPolicyBtn = findViewById(R.id.privacy_policy_button);
|
||||
this.signUpBtn = findViewById(R.id.signup_button);
|
||||
|
||||
@@ -358,6 +383,19 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
privacyPolicyBtn.setText(TextUtil.markAsExternal(
|
||||
getString(R.string.instant_onboarding_agree_instance, providerHost)));
|
||||
}
|
||||
|
||||
if (parsedQrData != null) {
|
||||
if (isContactInvitation) {
|
||||
String name = dcContext.getContact(parsedQrData.getId()).getDisplayName();
|
||||
invitationText.setText(this.getString(R.string.instant_onboarding_contact_info, name));
|
||||
invitationText.setVisibility(View.VISIBLE);
|
||||
} else if (isGroupInvitation) {
|
||||
String groupName = parsedQrData.getText1();
|
||||
invitationText.setText(this.getString(R.string.instant_onboarding_group_info, groupName));
|
||||
invitationText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,11 +457,14 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
|
||||
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
|
||||
if (isContactInvitation || isGroupInvitation) {
|
||||
intent.putExtra(ConversationListActivity.FROM_WELCOME_RAW_QR, rawQrData);
|
||||
}
|
||||
|
||||
startActivity(intent);
|
||||
finishAffinity();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void createProfile() {
|
||||
if (TextUtils.isEmpty(this.name.getText())) {
|
||||
Toast.makeText(this, R.string.please_enter_name, Toast.LENGTH_LONG).show();
|
||||
@@ -431,37 +472,31 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
final String name = this.name.getText().toString();
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = InstantOnboardingActivity.this;
|
||||
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
|
||||
executor.execute(() -> {
|
||||
Context context = InstantOnboardingActivity.this;
|
||||
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
|
||||
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
boolean result = true;
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
if (result) {
|
||||
boolean finalResult = result;
|
||||
runOnUiThread(() -> {
|
||||
if (finalResult) {
|
||||
attachmentManager.cleanup();
|
||||
startQrAccountCreation(providerQrData);
|
||||
} else {
|
||||
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void startQrAccountCreation(String qrCode)
|
||||
|
||||
@@ -129,7 +129,7 @@ public class LogViewFragment extends Fragment {
|
||||
return logFile;
|
||||
}
|
||||
|
||||
private static String grabLogcat(LogViewFragment fragment) {
|
||||
public static String grabLogcat() {
|
||||
String command = "logcat -v threadtime -d -t 10000 *:I";
|
||||
try {
|
||||
final Process process = Runtime.getRuntime().exec(command);
|
||||
@@ -165,7 +165,7 @@ public class LogViewFragment extends Fragment {
|
||||
if (fragment == null) return null;
|
||||
|
||||
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" +
|
||||
buildDescription(fragment) + "\n" + grabLogcat(fragment);
|
||||
buildDescription(fragment) + "\n" + grabLogcat();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
@@ -12,6 +13,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
@@ -57,10 +59,14 @@ public abstract class MessageSelectorFragment
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords) {
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords));
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final int[] messageIds) {
|
||||
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
|
||||
DcContext dcContext = DcHelper.getContext(getContext());
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
boolean canDeleteForAll = true;
|
||||
@@ -79,20 +85,24 @@ public abstract class MessageSelectorFragment
|
||||
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
|
||||
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
|
||||
|
||||
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
|
||||
};
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(text)
|
||||
.setCancelable(true)
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(positiveBtnLabel, (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
});
|
||||
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
|
||||
|
||||
if(canDeleteForAll) {
|
||||
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
|
||||
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
});
|
||||
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
|
||||
};
|
||||
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
|
||||
AlertDialog dialog = builder.show();
|
||||
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
|
||||
Util.redPositiveButton(dialog);
|
||||
|
||||
@@ -22,10 +22,7 @@ import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageConte
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
@@ -37,7 +34,6 @@ import com.google.zxing.integration.android.IntentResult;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.qr.QrActivity;
|
||||
import org.thoughtcrime.securesms.qr.QrCodeHandler;
|
||||
import org.thoughtcrime.securesms.util.MailtoUtil;
|
||||
|
||||
import chat.delta.rpc.types.SecurejoinSource;
|
||||
import chat.delta.rpc.types.SecurejoinUiPath;
|
||||
@@ -57,53 +53,6 @@ public class NewConversationActivity extends ContactSelectionActivity {
|
||||
super.onCreate(bundle, ready);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
handleIntent();
|
||||
}
|
||||
|
||||
private void handleIntent() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
if(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) {
|
||||
try {
|
||||
Uri uri = intent.getData();
|
||||
if(uri != null) {
|
||||
String scheme = uri.getScheme();
|
||||
if(MailtoUtil.isMailto(uri)) {
|
||||
String textToShare = MailtoUtil.getText(uri);
|
||||
String[] recipientsArray = MailtoUtil.getRecipients(uri);
|
||||
if (recipientsArray.length >= 1) {
|
||||
if (!textToShare.isEmpty()) {
|
||||
getIntent().putExtra(TEXT_EXTRA, textToShare);
|
||||
}
|
||||
final String addr = recipientsArray[0];
|
||||
final DcContext dcContext = DcHelper.getContext(this);
|
||||
int contactId = dcContext.lookupContactIdByAddr(addr);
|
||||
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
|
||||
contactId = dcContext.createContact(null, recipientsArray[0]);
|
||||
}
|
||||
if (contactId == 0) {
|
||||
Toast.makeText(this, R.string.bad_email_address, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
onContactSelected(contactId);
|
||||
}
|
||||
} else {
|
||||
Intent shareIntent = new Intent(this, ShareActivity.class);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, textToShare);
|
||||
startActivity(shareIntent);
|
||||
finish();
|
||||
}
|
||||
} else if(scheme != null && scheme.startsWith("http")) {
|
||||
Intent shareIntent = new Intent(this, ShareActivity.class);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, uri.toString());
|
||||
startActivity(shareIntent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
Log.e(TAG, "start activity from external 'mailto:' link failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -30,8 +31,13 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class ProfileAdapter extends RecyclerView.Adapter
|
||||
{
|
||||
private static final String TAG = ProfileAdapter.class.getSimpleName();
|
||||
|
||||
public static final int ITEM_AVATAR = 10;
|
||||
public static final int ITEM_DIVIDER = 20;
|
||||
public static final int ITEM_SIGNATURE = 25;
|
||||
@@ -195,7 +201,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
|
||||
}
|
||||
else if(holder.itemView instanceof ProfileStatusItem) {
|
||||
ProfileStatusItem item = (ProfileStatusItem) holder.itemView;
|
||||
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(); return true;});
|
||||
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(dcContact == null); return true;});
|
||||
item.set(data.label);
|
||||
}
|
||||
else if(holder.itemView instanceof ProfileAvatarItem) {
|
||||
@@ -230,7 +236,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
|
||||
|
||||
public interface ItemClickListener {
|
||||
void onSettingsClicked(int settingsId);
|
||||
void onStatusLongClicked();
|
||||
void onStatusLongClicked(boolean isMultiUser);
|
||||
void onSharedChatClicked(int chatId);
|
||||
void onMemberClicked(int contactId);
|
||||
void onMemberLongClicked(int contactId);
|
||||
@@ -278,8 +284,21 @@ public class ProfileAdapter extends RecyclerView.Adapter
|
||||
|
||||
itemData.add(new ItemData(ITEM_AVATAR, null, 0));
|
||||
|
||||
if (isSelfTalk || dcContact != null && !dcContact.getStatus().isEmpty()) {
|
||||
itemDataStatusText = isSelfTalk ? context.getString(R.string.saved_messages_explain) : dcContact.getStatus();
|
||||
if (isSelfTalk) {
|
||||
itemDataStatusText = context.getString(R.string.saved_messages_explain);
|
||||
} else if (dcContact != null) {
|
||||
itemDataStatusText = dcContact.getStatus();
|
||||
} else if (dcChat != null && dcChat.isEncrypted()) {
|
||||
// Load group or channel description
|
||||
try {
|
||||
Rpc rpc = DcHelper.getRpc(context);
|
||||
itemDataStatusText = rpc.getChatDescription(rpc.getSelectedAccountId(), dcChat.getId());
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemDataStatusText.isEmpty()) {
|
||||
itemData.add(new ItemData(ITEM_SIGNATURE, itemDataStatusText, 0));
|
||||
} else {
|
||||
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
|
||||
|
||||
@@ -11,6 +11,8 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -48,6 +50,7 @@ public class ProfileFragment extends Fragment
|
||||
private ActionMode actionMode;
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
|
||||
private ActivityResultLauncher<Intent> pickContactLauncher;
|
||||
|
||||
private DcContext dcContext;
|
||||
protected int chatId;
|
||||
@@ -60,6 +63,41 @@ public class ProfileFragment extends Fragment
|
||||
chatId = getArguments() != null ? getArguments().getInt(CHAT_ID_EXTRA, -1) : -1;
|
||||
contactId = getArguments().getInt(CONTACT_ID_EXTRA, -1);
|
||||
dcContext = DcHelper.getContext(requireContext());
|
||||
|
||||
// Register activity result launcher
|
||||
pickContactLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Intent data = result.getData();
|
||||
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
|
||||
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
if (deselected != null) {
|
||||
// Remove members that were deselected
|
||||
int[] members = dcContext.getChatContacts(chatId);
|
||||
for (int contactId : deselected) {
|
||||
for (int memberId : members) {
|
||||
if (memberId == contactId) {
|
||||
dcContext.removeContactFromChat(chatId, memberId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selected != null) {
|
||||
// Add new members
|
||||
for (Integer contactId : selected) {
|
||||
if (contactId != null) {
|
||||
dcContext.addContactToChat(chatId, contactId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,10 +178,10 @@ public class ProfileFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusLongClicked() {
|
||||
public void onStatusLongClicked(boolean isMultiUser) {
|
||||
Context context = requireContext();
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.pref_default_status_label)
|
||||
.setTitle(isMultiUser? R.string.chat_description : R.string.pref_default_status_label)
|
||||
.setItems(new CharSequence[]{
|
||||
context.getString(R.string.menu_copy_to_clipboard)
|
||||
},
|
||||
@@ -210,7 +248,7 @@ public class ProfileFragment extends Fragment
|
||||
preselectedContacts.add(memberId);
|
||||
}
|
||||
intent.putExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS, preselectedContacts);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
|
||||
pickContactLauncher.launch(intent);
|
||||
}
|
||||
|
||||
public void onQrInvite() {
|
||||
@@ -304,20 +342,4 @@ public class ProfileFragment extends Fragment
|
||||
adapter.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
|
||||
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
|
||||
if(selected == null) return;
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
for (Integer contactId : selected) {
|
||||
if (contactId!=null) {
|
||||
dcContext.addContactToChat(chatId, contactId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,26 +104,25 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
|
||||
resolvedExtras = new ArrayList<>();
|
||||
|
||||
List<Uri> streamExtras = new ArrayList<>();
|
||||
if (MailtoUtil.isMailto(getIntent().getData())) {
|
||||
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
|
||||
if (extraEmail == null || extraEmail.length == 0) {
|
||||
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(getIntent().getData()));
|
||||
}
|
||||
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
if (text == null || text.isEmpty()) {
|
||||
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(getIntent().getData()));
|
||||
}
|
||||
}
|
||||
|
||||
if (Intent.ACTION_SEND.equals(getIntent().getAction()) &&
|
||||
getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
|
||||
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
streamExtras.add(uri);
|
||||
} else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) {
|
||||
streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
} else {
|
||||
Uri uri = getIntent().getData();
|
||||
if (MailtoUtil.isMailto(uri)) {
|
||||
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
|
||||
if (extraEmail == null || extraEmail.length == 0) {
|
||||
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(uri));
|
||||
}
|
||||
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
if (text == null || text.isEmpty()) {
|
||||
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(uri));
|
||||
}
|
||||
} else if (uri != null) {
|
||||
streamExtras.add(uri);
|
||||
}
|
||||
} else if (getIntent().getData() != null) {
|
||||
streamExtras.add(getIntent().getData());
|
||||
}
|
||||
|
||||
if (needsFilePermission(streamExtras)) {
|
||||
@@ -265,6 +264,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
|
||||
}
|
||||
|
||||
chatId = dcContext.createChatByContactId(contactId);
|
||||
accId = dcContext.getAccountId();
|
||||
}
|
||||
Intent composeIntent;
|
||||
if (accId != -1 && chatId != -1) {
|
||||
|
||||
@@ -77,7 +77,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
|
||||
} else {
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
|
||||
}
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
|
||||
+13
-7
@@ -47,21 +47,24 @@ import chat.delta.rpc.RpcException;
|
||||
public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate
|
||||
{
|
||||
private static final String TAG = AccountSelectionListFragment.class.getSimpleName();
|
||||
private final ConversationListActivity activity;
|
||||
private static final String ARG_SELECT_ONLY = "select_only";
|
||||
private RecyclerView recyclerView;
|
||||
private AccountSelectionListAdapter adapter;
|
||||
private final boolean selectOnly;
|
||||
private boolean selectOnly;
|
||||
|
||||
public AccountSelectionListFragment(ConversationListActivity activity, boolean selectOnly) {
|
||||
super();
|
||||
this.activity = activity;
|
||||
this.selectOnly = selectOnly;
|
||||
public static AccountSelectionListFragment newInstance(boolean selectOnly) {
|
||||
AccountSelectionListFragment fragment = new AccountSelectionListFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean(ARG_SELECT_ONLY, selectOnly);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
selectOnly = getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.switch_account)
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
if (!selectOnly) {
|
||||
@@ -179,6 +182,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
|
||||
}
|
||||
|
||||
private void onSetTag(int accountId) {
|
||||
ConversationListActivity activity = (ConversationListActivity)requireActivity();
|
||||
AccountSelectionListFragment.this.dismiss();
|
||||
|
||||
DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId);
|
||||
@@ -202,6 +206,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
|
||||
|
||||
private void onDeleteProfile(int accountId) {
|
||||
AccountSelectionListFragment.this.dismiss();
|
||||
ConversationListActivity activity = (ConversationListActivity)requireActivity();
|
||||
DcAccounts accounts = DcHelper.getAccounts(activity);
|
||||
Rpc rpc = DcHelper.getRpc(activity);
|
||||
|
||||
@@ -254,6 +259,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
|
||||
@Override
|
||||
public void onItemClick(AccountSelectionListItem contact) {
|
||||
AccountSelectionListFragment.this.dismiss();
|
||||
ConversationListActivity activity = (ConversationListActivity)requireActivity();
|
||||
int accountId = contact.getAccountId();
|
||||
if (accountId == DC_CONTACT_ID_ADD_ACCOUNT) {
|
||||
AccountManager.getInstance().switchAccountAndStartActivity(activity, 0);
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class AudioSlidePlayer {
|
||||
|
||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||
|
||||
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull AudioSlide slide;
|
||||
private final @NonNull Handler progressEventHandler;
|
||||
|
||||
private @NonNull WeakReference<Listener> listener;
|
||||
private @Nullable SimpleExoPlayer mediaPlayer;
|
||||
private @Nullable SimpleExoPlayer durationCalculator;
|
||||
|
||||
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
||||
playing.get().setListener(listener);
|
||||
return playing.get();
|
||||
} else {
|
||||
return new AudioSlidePlayer(context, slide, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private AudioSlidePlayer(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
this.context = context;
|
||||
this.slide = slide;
|
||||
this.listener = new WeakReference<>(listener);
|
||||
this.progressEventHandler = new ProgressEventHandler(this);
|
||||
}
|
||||
|
||||
public void requestDuration() {
|
||||
try {
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
|
||||
durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
|
||||
.setTrackSelector(new DefaultTrackSelector(context))
|
||||
.setLoadControl(loadControl)
|
||||
.build();
|
||||
durationCalculator.setPlayWhenReady(false);
|
||||
durationCalculator.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
Util.runOnMain(() -> {
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (durationCalculator == null) return;
|
||||
Log.d(TAG, "request duration " + durationCalculator.getDuration());
|
||||
getListener().onReceivedDuration(Long.valueOf(durationCalculator.getDuration()).intValue());
|
||||
durationCalculator.release();
|
||||
durationCalculator.removeListener(this);
|
||||
durationCalculator = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
durationCalculator.prepare(createMediaSource(slide.getUri()));
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
getListener().onReceivedDuration(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void play(final double progress) throws IOException {
|
||||
play(progress, false);
|
||||
}
|
||||
|
||||
private void play(final double progress, boolean earpiece) throws IOException {
|
||||
if (this.mediaPlayer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slide.getUri() == null) {
|
||||
throw new IOException("Slide has no URI!");
|
||||
}
|
||||
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
|
||||
this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
|
||||
.setTrackSelector(new DefaultTrackSelector(context))
|
||||
.setLoadControl(loadControl)
|
||||
.build();
|
||||
|
||||
mediaPlayer.prepare(createMediaSource(slide.getUri()));
|
||||
mediaPlayer.setPlayWhenReady(true);
|
||||
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
||||
.build(), false);
|
||||
mediaPlayer.addListener(new Player.Listener() {
|
||||
|
||||
boolean started = false;
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
|
||||
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (mediaPlayer == null) return;
|
||||
Log.d(TAG, "DURATION: " + mediaPlayer.getDuration());
|
||||
|
||||
if (started) {
|
||||
Log.d(TAG, "Already started. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = true;
|
||||
|
||||
if (progress > 0) {
|
||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||
}
|
||||
|
||||
setPlaying(AudioSlidePlayer.this);
|
||||
}
|
||||
|
||||
keepScreenOn(true);
|
||||
notifyOnStart();
|
||||
progressEventHandler.sendEmptyMessage(0);
|
||||
break;
|
||||
|
||||
case Player.STATE_ENDED:
|
||||
Log.i(TAG, "onComplete");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
getListener().onReceivedDuration(Long.valueOf(mediaPlayer.getDuration()).intValue());
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
|
||||
keepScreenOn(false);
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
|
||||
// Failed to play media file, maybe another app can handle it
|
||||
int msgId = getAudioSlide().getDcMsgId();
|
||||
DcHelper.openForViewOrShare(context, msgId, Intent.ACTION_VIEW);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MediaSource createMediaSource(@NonNull Uri uri) {
|
||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory);
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||
|
||||
return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
Log.i(TAG, "Stop called!");
|
||||
|
||||
keepScreenOn(false);
|
||||
removePlaying(this);
|
||||
|
||||
if (this.mediaPlayer != null) {
|
||||
this.mediaPlayer.stop();
|
||||
this.mediaPlayer.release();
|
||||
}
|
||||
|
||||
this.mediaPlayer = null;
|
||||
}
|
||||
|
||||
public static void stopAll() {
|
||||
if (playing.isPresent()) {
|
||||
synchronized (AudioSlidePlayer.class) {
|
||||
if (playing.isPresent()) {
|
||||
playing.get().stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
|
||||
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
||||
notifyOnStart();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull AudioSlide getAudioSlide() {
|
||||
return slide;
|
||||
}
|
||||
|
||||
|
||||
private Pair<Double, Integer> getProgress() {
|
||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||
return new Pair<>(0D, 0);
|
||||
} else {
|
||||
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
||||
(int) mediaPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnStop() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnProgress(final double progress, final long millis) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onProgress(slide, progress, millis);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull Listener getListener() {
|
||||
Listener listener = this.listener.get();
|
||||
|
||||
if (listener != null) return listener;
|
||||
else return new Listener() {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
@Override
|
||||
public void onStop() {}
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {}
|
||||
};
|
||||
}
|
||||
|
||||
public void keepScreenOn(boolean keepOn) {
|
||||
if (context instanceof Activity) {
|
||||
if (keepOn) {
|
||||
((Activity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
((Activity) context).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() != player) {
|
||||
playing.get().notifyOnStop();
|
||||
playing.get().stop();
|
||||
}
|
||||
|
||||
playing = Optional.of(player);
|
||||
}
|
||||
|
||||
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() == player) {
|
||||
playing = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onStart();
|
||||
void onStop();
|
||||
void onProgress(AudioSlide slide, double progress, long millis);
|
||||
void onReceivedDuration(int millis);
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
||||
private final WeakReference<AudioSlidePlayer> playerReference;
|
||||
|
||||
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
||||
this.playerReference = new WeakReference<>(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
AudioSlidePlayer player = playerReference.get();
|
||||
|
||||
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
||||
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
||||
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,14 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
|
||||
public static final String EXTRA_CHAT_ID = "chat_id";
|
||||
public static final String EXTRA_CALL_ID = "call_id";
|
||||
public static final String EXTRA_HASH = "hash";
|
||||
public static final String EXTRA_HAS_VIDEO = "has_video";
|
||||
|
||||
private DcContext dcContext;
|
||||
private Rpc rpc;
|
||||
private int accId;
|
||||
private int chatId;
|
||||
private int callId;
|
||||
private boolean hasVideo;
|
||||
private boolean ended = false;
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@@ -58,7 +60,9 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
|
||||
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
assert bundle != null;
|
||||
hasVideo = bundle.getBoolean(EXTRA_HAS_VIDEO, true);
|
||||
String hash = bundle.getString(EXTRA_HASH, "");
|
||||
String query = hasVideo? "" : "?noOutgoingVideoInitially";
|
||||
accId = bundle.getInt(EXTRA_ACCOUNT_ID, -1);
|
||||
chatId = bundle.getInt(EXTRA_CHAT_ID, 0);
|
||||
callId = bundle.getInt(EXTRA_CALL_ID, 0);
|
||||
@@ -94,7 +98,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
|
||||
.onAllGranted(() -> {
|
||||
String url = "file:///android_asset/calls/index.html";
|
||||
webView.loadUrl(url + hash);
|
||||
webView.loadUrl(url + query + hash);
|
||||
}).onAnyDenied(this::finish)
|
||||
.execute();
|
||||
}
|
||||
@@ -168,7 +172,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
|
||||
@JavascriptInterface
|
||||
public void startCall(String payload) {
|
||||
try {
|
||||
callId = rpc.placeOutgoingCall(accId, chatId, payload);
|
||||
callId = rpc.placeOutgoingCall(accId, chatId, payload, hasVideo);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Error", e);
|
||||
}
|
||||
|
||||
@@ -18,28 +18,29 @@ import java.nio.charset.StandardCharsets;
|
||||
public class CallUtil {
|
||||
private static final String TAG = CallUtil.class.getSimpleName();
|
||||
|
||||
public static void startCall(Activity activity, int chatId) {
|
||||
public static void startCall(Activity activity, int chatId, boolean hasVideo) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
|
||||
.onAllGranted(() -> {
|
||||
int accId = DcHelper.getContext(activity).getAccountId();
|
||||
startCall(activity, accId, chatId);
|
||||
startCall(activity, accId, chatId, hasVideo);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void startCall(Context context, int accId, int chatId) {
|
||||
public static void startCall(Context context, int accId, int chatId, boolean hasVideo) {
|
||||
Intent intent = new Intent(context, CallActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
|
||||
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
|
||||
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
|
||||
intent.putExtra(CallActivity.EXTRA_HASH, "#startCall");
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void openCall(Context context, int accId, int chatId, int callId, String payload) {
|
||||
public static void openCall(Context context, int accId, int chatId, int callId, String payload, boolean hasVideo) {
|
||||
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
|
||||
String hash = "";
|
||||
try {
|
||||
@@ -53,6 +54,7 @@ public class CallUtil {
|
||||
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
|
||||
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
|
||||
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
|
||||
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
|
||||
intent.putExtra(CallActivity.EXTRA_HASH, hash);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||
|
||||
private static final String TAG = AudioView.class.getSimpleName();
|
||||
|
||||
private final @NonNull AnimatingToggle controlToggle;
|
||||
private final @NonNull ImageView playButton;
|
||||
private final @NonNull ImageView pauseButton;
|
||||
private final @NonNull SeekBar seekBar;
|
||||
private final @NonNull TextView timestamp;
|
||||
private final @NonNull TextView title;
|
||||
private final @NonNull View mask;
|
||||
|
||||
private @Nullable AudioSlidePlayer audioSlidePlayer;
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||
private int backwardsCounter;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
|
||||
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
|
||||
this.playButton = (ImageView) findViewById(R.id.play);
|
||||
this.pauseButton = (ImageView) findViewById(R.id.pause);
|
||||
this.seekBar = (SeekBar) findViewById(R.id.seek);
|
||||
this.timestamp = (TextView) findViewById(R.id.timestamp);
|
||||
this.title = (TextView) findViewById(R.id.title);
|
||||
this.mask = findViewById(R.id.interception_mask);
|
||||
|
||||
this.timestamp.setText("00:00");
|
||||
|
||||
this.playButton.setOnClickListener(new PlayClickedListener());
|
||||
this.pauseButton.setOnClickListener(new PauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
|
||||
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
|
||||
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
|
||||
setTint(getContext().getResources().getColor(R.color.audio_icon));
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio, int duration)
|
||||
{
|
||||
controlToggle.displayQuick(playButton);
|
||||
seekBar.setEnabled(true);
|
||||
seekBar.setProgress(0);
|
||||
audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
|
||||
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
title.setText(audio.getFileName().get());
|
||||
title.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
super.setOnClickListener(listener);
|
||||
this.mask.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnLongClickListener(OnLongClickListener listener) {
|
||||
super.setOnLongClickListener(listener);
|
||||
this.mask.setOnLongClickListener(listener);
|
||||
this.playButton.setOnLongClickListener(listener);
|
||||
this.pauseButton.setOnLongClickListener(listener);
|
||||
}
|
||||
|
||||
public void togglePlay() {
|
||||
if (this.playButton.getVisibility() == View.VISIBLE) {
|
||||
playButton.performClick();
|
||||
} else {
|
||||
pauseButton.performClick();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
String desc;
|
||||
if (this.title.getVisibility() == View.VISIBLE) {
|
||||
desc = getContext().getString(R.string.audio);
|
||||
} else {
|
||||
desc = getContext().getString(R.string.voice_message);
|
||||
}
|
||||
desc += "\n" + this.timestamp.getText();
|
||||
if (title.getVisibility() == View.VISIBLE) {
|
||||
desc += "\n" + this.title.getText();
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
if (getProgress()==0)
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
this.audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (this.pauseButton.getVisibility() != View.VISIBLE) {
|
||||
togglePlayToPause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (this.playButton.getVisibility() != View.VISIBLE) {
|
||||
togglePauseToPlay();
|
||||
}
|
||||
|
||||
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
backwardsCounter = 4;
|
||||
onProgress(audioSlidePlayer.getAudioSlide(), 0.0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public void disablePlayer(boolean disable) {
|
||||
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {
|
||||
if (!audioSlidePlayer.getAudioSlide().equals(slide)) {
|
||||
return;
|
||||
}
|
||||
int seekProgress = (int) Math.floor(progress * this.seekBar.getMax());
|
||||
|
||||
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
|
||||
backwardsCounter = 0;
|
||||
this.seekBar.setProgress(seekProgress);
|
||||
if (millis != -1) {
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
|
||||
}
|
||||
} else {
|
||||
backwardsCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTint(int foregroundTint) {
|
||||
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
|
||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
|
||||
seekBar.getGlobalVisibleRect(rect);
|
||||
}
|
||||
|
||||
private double getProgress() {
|
||||
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePlayToPause() {
|
||||
controlToggle.displayQuick(pauseButton);
|
||||
|
||||
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
|
||||
pauseButton.setImageDrawable(playToPauseDrawable);
|
||||
playToPauseDrawable.start();
|
||||
}
|
||||
|
||||
private void togglePauseToPlay() {
|
||||
controlToggle.displayQuick(playButton);
|
||||
|
||||
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
|
||||
playButton.setImageDrawable(pauseToPlayDrawable);
|
||||
pauseToPlayDrawable.start();
|
||||
}
|
||||
|
||||
private class PlayClickedListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
Log.w(TAG, "playbutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
if (audioFocusChangeListener == null) {
|
||||
audioFocusChangeListener = focusChange -> {
|
||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
||||
pauseButton.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build();
|
||||
|
||||
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(playbackAttributes)
|
||||
.setAcceptsDelayedFocusGain(false)
|
||||
.setWillPauseWhenDucked(false)
|
||||
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
||||
.build();
|
||||
|
||||
AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.requestAudioFocus(focusRequest);
|
||||
}
|
||||
|
||||
togglePlayToPause();
|
||||
audioSlidePlayer.play(getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PauseClickedListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.w(TAG, "pausebutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePauseToPlay();
|
||||
audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
|
||||
|
||||
@Override
|
||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
|
||||
try {
|
||||
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
audioSlidePlayer.play(getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,14 @@ public class CallItemView extends FrameLayout {
|
||||
title.setText(R.string.canceled_call);
|
||||
} else if (callInfo.state instanceof CallState.Declined) {
|
||||
title.setText(R.string.declined_call);
|
||||
} else if (callInfo.hasVideo) {
|
||||
title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call);
|
||||
} else {
|
||||
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
|
||||
title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call);
|
||||
}
|
||||
|
||||
icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24);
|
||||
|
||||
int[] attrs;
|
||||
if (isOutgoing) {
|
||||
attrs = new int[]{
|
||||
|
||||
@@ -151,12 +151,6 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
else if (messageRecord.isDelivered()) deliveryStatusView.setSent();
|
||||
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
|
||||
else deliveryStatusView.setPending();
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
deliveryStatusView.setTint(Color.RED);
|
||||
} else {
|
||||
deliveryStatusView.setTint(textColor); // Reset the color to the standard color (because the footer is re-used in a RecyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
|
||||
+20
-2
@@ -11,6 +11,9 @@ import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RemovableEditableMediaView extends FrameLayout {
|
||||
|
||||
private final @NonNull ImageView remove;
|
||||
@@ -19,6 +22,7 @@ public class RemovableEditableMediaView extends FrameLayout {
|
||||
private final int removeSize;
|
||||
|
||||
private @Nullable View current;
|
||||
private final List<OnClickListener> removeClickListeners = new ArrayList<>();
|
||||
|
||||
public RemovableEditableMediaView(Context context) {
|
||||
this(context, null);
|
||||
@@ -72,8 +76,22 @@ public class RemovableEditableMediaView extends FrameLayout {
|
||||
return current;
|
||||
}
|
||||
|
||||
public void setRemoveClickListener(View.OnClickListener listener) {
|
||||
this.remove.setOnClickListener(listener);
|
||||
public void addRemoveClickListener(View.OnClickListener listener) {
|
||||
removeClickListeners.add(listener);
|
||||
updateRemoveClickListener();
|
||||
}
|
||||
|
||||
public void removeRemoveClickListener(View.OnClickListener listener) {
|
||||
removeClickListeners.remove(listener);
|
||||
updateRemoveClickListener();
|
||||
}
|
||||
|
||||
private void updateRemoveClickListener() {
|
||||
this.remove.setOnClickListener(v -> {
|
||||
for (OnClickListener listener : removeClickListeners) {
|
||||
listener.onClick(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setEditClickListener(View.OnClickListener listener) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AudioPlaybackState {
|
||||
private final int msgId;
|
||||
private final @Nullable Uri audioUri;
|
||||
private final PlaybackStatus status;
|
||||
private final long currentPosition;
|
||||
private final long duration;
|
||||
|
||||
public enum PlaybackStatus {
|
||||
IDLE,
|
||||
LOADING,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
public AudioPlaybackState(int msgId,
|
||||
@Nullable Uri audioUri,
|
||||
PlaybackStatus status,
|
||||
long currentPosition,
|
||||
long duration) {
|
||||
this.msgId = msgId;
|
||||
this.audioUri = audioUri;
|
||||
this.status = status;
|
||||
this.currentPosition = currentPosition;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public static AudioPlaybackState idle() {
|
||||
return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0);
|
||||
}
|
||||
|
||||
public int getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getAudioUri() {
|
||||
return audioUri;
|
||||
}
|
||||
|
||||
public PlaybackStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public long getCurrentPosition() {
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.session.MediaController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
||||
public class AudioPlaybackViewModel extends ViewModel {
|
||||
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
|
||||
|
||||
private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id.
|
||||
|
||||
private final MutableLiveData<AudioPlaybackState> playbackState;
|
||||
|
||||
private final MutableLiveData<Map<Integer, Long>> durations = new MutableLiveData<>(new HashMap<>());
|
||||
private final Set<Integer> extractionInProgress = new HashSet<>();
|
||||
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private final Handler handler;
|
||||
private boolean isUserSeeking = false;
|
||||
|
||||
public AudioPlaybackViewModel() {
|
||||
playbackState = new MutableLiveData<>(AudioPlaybackState.idle());
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
public LiveData<AudioPlaybackState> getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
public void setMediaController(@Nullable MediaController controller) {
|
||||
this.mediaController = controller;
|
||||
if (mediaController != null && mediaController.isPlaying()) {
|
||||
startUpdateProgress();
|
||||
}
|
||||
updateCurrentState(true);
|
||||
setupPlayerListener();
|
||||
}
|
||||
|
||||
// Public methods
|
||||
public void loadAudioAndPlay(int msgId, Uri audioUri) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
// Set media item if we have a different audio.
|
||||
if (isDifferentAudio(msgId, audioUri)) {
|
||||
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(String.valueOf(msgId))
|
||||
.setUri(audioUri)
|
||||
.build();
|
||||
mediaController.setMediaItem(mediaItem);
|
||||
mediaController.prepare();
|
||||
}
|
||||
|
||||
play(msgId, audioUri);
|
||||
}
|
||||
private boolean isSameAudio(int msgId, Uri audioUri) {
|
||||
return !isDifferentAudio(msgId, audioUri);
|
||||
}
|
||||
|
||||
private boolean isDifferentAudio(int msgId, Uri audioUri) {
|
||||
AudioPlaybackState currentState = playbackState.getValue();
|
||||
|
||||
return currentState != null && (
|
||||
msgId != currentState.getMsgId() ||
|
||||
currentState.getAudioUri() == null ||
|
||||
currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
|
||||
}
|
||||
|
||||
public LiveData<Map<Integer, Long>> getDurations() {
|
||||
return durations;
|
||||
}
|
||||
|
||||
public void ensureDurationLoaded(Context context, int msgId, Uri audioUri) {
|
||||
// Check cache
|
||||
Map<Integer, Long> currentDurations = durations.getValue();
|
||||
if (currentDurations != null && currentDurations.containsKey(msgId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check extracting
|
||||
synchronized (extractionInProgress) {
|
||||
if (extractionInProgress.contains(msgId)) {
|
||||
return;
|
||||
}
|
||||
extractionInProgress.add(msgId);
|
||||
}
|
||||
|
||||
// Extract in background
|
||||
extractionExecutor.execute(() -> {
|
||||
long duration = extractDurationFromAudio(context, audioUri);
|
||||
|
||||
handler.post(() -> {
|
||||
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
|
||||
updatedDurations.put(msgId, duration);
|
||||
durations.setValue(updatedDurations);
|
||||
});
|
||||
|
||||
synchronized (extractionInProgress) {
|
||||
extractionInProgress.remove(msgId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private long extractDurationFromAudio(Context context, Uri audioUri) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(context, audioUri);
|
||||
String durationStr = retriever.extractMetadata(
|
||||
MediaMetadataRetriever.METADATA_KEY_DURATION
|
||||
);
|
||||
return durationStr != null ? Long.parseLong(durationStr) : 0;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
} finally {
|
||||
try {
|
||||
retriever.release();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void pause(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public void play(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.play();
|
||||
}
|
||||
}
|
||||
|
||||
public void seekTo(long position, int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.seekTo(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.stop();
|
||||
stopUpdateProgress();
|
||||
playbackState.setValue(AudioPlaybackState.idle());
|
||||
}
|
||||
}
|
||||
|
||||
public void stopNonMessageAudioPlayback() {
|
||||
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
|
||||
}
|
||||
|
||||
// A special method for deleting message, where we only use message Ids
|
||||
public void stopByIds(int... msgIds) {
|
||||
AudioPlaybackState currentState = playbackState.getValue();
|
||||
|
||||
if (mediaController != null && currentState != null) {
|
||||
for (int msgId : msgIds) {
|
||||
if (msgId == currentState.getMsgId()) {
|
||||
mediaController.stop();
|
||||
stopUpdateProgress();
|
||||
playbackState.setValue(AudioPlaybackState.idle());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setUserSeeking(boolean isUserSeeking) {
|
||||
this.isUserSeeking = isUserSeeking;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private void setupPlayerListener() {
|
||||
if (mediaController == null) return;
|
||||
|
||||
mediaController.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onEvents(Player player, Player.Events events) {
|
||||
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
|
||||
if (player.isPlaying()) {
|
||||
startUpdateProgress();
|
||||
} else {
|
||||
stopUpdateProgress();
|
||||
}
|
||||
updateCurrentState(false);
|
||||
}
|
||||
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
if (player.getPlaybackState() == Player.STATE_READY) {
|
||||
updateCurrentState(false);
|
||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
// This is to prevent automatically playing after the audio
|
||||
// has been play to the end once, then user dragged the seek bar again
|
||||
mediaController.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
|
||||
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCurrentState(boolean queryPlaying) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
AudioPlaybackState.PlaybackStatus status;
|
||||
if (mediaController.isPlaying()) {
|
||||
status = AudioPlaybackState.PlaybackStatus.PLAYING;
|
||||
} else if (mediaController.getPlaybackState() == Player.STATE_READY
|
||||
|| mediaController.getPlaybackState() == Player.STATE_ENDED) {
|
||||
status = AudioPlaybackState.PlaybackStatus.PAUSED;
|
||||
} else {
|
||||
status = AudioPlaybackState.PlaybackStatus.IDLE;
|
||||
}
|
||||
|
||||
Uri currentUri = null;
|
||||
int currentMsgId = 0;
|
||||
if (playbackState.getValue() != null) {
|
||||
currentMsgId = playbackState.getValue().getMsgId();
|
||||
currentUri = playbackState.getValue().getAudioUri();
|
||||
}
|
||||
if (queryPlaying || playbackState.getValue() == null) {
|
||||
MediaItem item = mediaController.getCurrentMediaItem();
|
||||
if (item != null) {
|
||||
try {
|
||||
currentMsgId = Integer.parseInt(item.mediaId);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Invalid integer", e);
|
||||
}
|
||||
if (item.localConfiguration != null) {
|
||||
currentUri = item.localConfiguration.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateState(
|
||||
currentMsgId,
|
||||
currentUri,
|
||||
status,
|
||||
mediaController.getCurrentPosition(),
|
||||
mediaController.getDuration());
|
||||
}
|
||||
|
||||
private void updateState(int msgId,
|
||||
Uri audioUri,
|
||||
AudioPlaybackState.PlaybackStatus status,
|
||||
long position,
|
||||
long duration) {
|
||||
// Sanitize longs
|
||||
if (position < 0 || position > Integer.MAX_VALUE) {
|
||||
position = 0;
|
||||
}
|
||||
if (duration < 0 || duration > Integer.MAX_VALUE) {
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
playbackState.setValue(new AudioPlaybackState(
|
||||
msgId, audioUri, status, position, duration
|
||||
));
|
||||
}
|
||||
|
||||
private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status,
|
||||
long position,
|
||||
long duration) {
|
||||
AudioPlaybackState current = playbackState.getValue();
|
||||
|
||||
if (current != null) {
|
||||
updateState(current.getMsgId(), current.getAudioUri(), status, position, duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Progress tracking
|
||||
private final Runnable progressRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) {
|
||||
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING,
|
||||
mediaController.getCurrentPosition(),
|
||||
mediaController.getDuration());
|
||||
handler.postDelayed(this, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void startUpdateProgress() {
|
||||
stopUpdateProgress();
|
||||
handler.post(progressRunnable);
|
||||
}
|
||||
|
||||
private void stopUpdateProgress() {
|
||||
handler.removeCallbacks(progressRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
stopUpdateProgress();
|
||||
extractionExecutor.shutdown();
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class AudioView extends FrameLayout {
|
||||
|
||||
private static final String TAG = AudioView.class.getSimpleName();
|
||||
|
||||
private final @NonNull ImageView playPauseButton;
|
||||
private final AnimatedVectorDrawableCompat playToPauseDrawable;
|
||||
private final AnimatedVectorDrawableCompat pauseToPlayDrawable;
|
||||
private final Drawable playDrawable;
|
||||
private final Drawable pauseDrawable;
|
||||
private final Animatable2Compat.AnimationCallback animationCallback;
|
||||
private final @NonNull SeekBar seekBar;
|
||||
private final @NonNull TextView timestamp;
|
||||
private final @NonNull TextView title;
|
||||
private final @NonNull View mask;
|
||||
private OnActionListener listener;
|
||||
|
||||
private int msgId = -1;
|
||||
private Uri audioUri;
|
||||
private int progress;
|
||||
private int duration;
|
||||
private AudioPlaybackViewModel viewModel;
|
||||
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
|
||||
private final Observer<Map<Integer, Long>> durationObserver = this::onDurationsChanged;
|
||||
private boolean isPlaying;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
|
||||
this.playPauseButton = findViewById(R.id.play_pause);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
this.title = findViewById(R.id.title);
|
||||
this.mask = findViewById(R.id.interception_mask);
|
||||
|
||||
updateTimestampsAndSeekBar();
|
||||
|
||||
// Load drawables once
|
||||
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
|
||||
getContext(), R.drawable.play_to_pause_animation);
|
||||
this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create(
|
||||
getContext(), R.drawable.pause_to_play_animation);
|
||||
this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon);
|
||||
this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon);
|
||||
|
||||
this.animationCallback = new Animatable2Compat.AnimationCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable drawable) {
|
||||
Drawable endState = isPlaying ? pauseDrawable : playDrawable;
|
||||
playPauseButton.setImageDrawable(endState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
setupControls();
|
||||
}
|
||||
|
||||
private void setupControls() {
|
||||
// Set up observer in a very specific case when the view is detached and then re-attached,
|
||||
// but binding from adapter has not happened yet
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
viewModel.getPlaybackState().observeForever(stateObserver);
|
||||
|
||||
viewModel.getDurations().removeObserver(durationObserver);
|
||||
viewModel.getDurations().observeForever(durationObserver);
|
||||
}
|
||||
|
||||
playPauseButton.setOnClickListener(v -> {
|
||||
Log.w(TAG, "playPauseButton onClick");
|
||||
|
||||
if (viewModel == null || audioUri == null) return;
|
||||
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
|
||||
// Same audio
|
||||
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
|
||||
viewModel.pause(msgId, audioUri);
|
||||
} else {
|
||||
viewModel.play(msgId, audioUri);
|
||||
}
|
||||
} else {
|
||||
// Different audio
|
||||
// Note: they can be the same *physical* file, but in different messages
|
||||
viewModel.loadAudioAndPlay(msgId, audioUri);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onPlayPauseButtonClicked(v);
|
||||
}
|
||||
});
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
AudioView.this.progress = progress;
|
||||
updateTimestampsAndSeekBar();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
viewModel.setUserSeeking(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
viewModel.setUserSeeking(false);
|
||||
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
|
||||
}
|
||||
});
|
||||
|
||||
if (playToPauseDrawable != null) {
|
||||
playToPauseDrawable.registerAnimationCallback(animationCallback);
|
||||
}
|
||||
if (pauseToPlayDrawable != null) {
|
||||
pauseToPlayDrawable.registerAnimationCallback(animationCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
viewModel.getDurations().removeObserver(durationObserver);
|
||||
}
|
||||
if (playToPauseDrawable != null) {
|
||||
playToPauseDrawable.clearAnimationCallbacks();
|
||||
}
|
||||
if (pauseToPlayDrawable != null) {
|
||||
pauseToPlayDrawable.clearAnimationCallbacks();
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
|
||||
if (this.viewModel != null) {
|
||||
this.viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
this.viewModel.getDurations().removeObserver(durationObserver);
|
||||
}
|
||||
|
||||
// ViewModel is used directly for simplicity, since there is no reuse yet
|
||||
this.viewModel = viewModel;
|
||||
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().observeForever(stateObserver);
|
||||
viewModel.getDurations().observeForever(durationObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio)
|
||||
{
|
||||
msgId = audio.getDcMsgId();
|
||||
audioUri = audio.getUri();
|
||||
playPauseButton.setImageDrawable(playDrawable);
|
||||
|
||||
seekBar.setEnabled(true);
|
||||
|
||||
// Get duration
|
||||
Map<Integer, Long> durations = viewModel.getDurations().getValue();
|
||||
if (durations != null && durations.containsKey(msgId)) {
|
||||
this.duration = Math.toIntExact(durations.get(msgId));
|
||||
updateTimestampsAndSeekBar();
|
||||
} else {
|
||||
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
|
||||
}
|
||||
|
||||
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
title.setText(audio.getFileName().get());
|
||||
title.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
super.setOnClickListener(listener);
|
||||
this.mask.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnLongClickListener(OnLongClickListener listener) {
|
||||
super.setOnLongClickListener(listener);
|
||||
this.mask.setOnLongClickListener(listener);
|
||||
this.playPauseButton.setOnLongClickListener(listener);
|
||||
}
|
||||
|
||||
public int getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
public Uri getAudioUri() {
|
||||
return audioUri;
|
||||
}
|
||||
|
||||
public interface OnActionListener {
|
||||
void onPlayPauseButtonClicked(View view);
|
||||
}
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void togglePlay() {
|
||||
playPauseButton.performClick();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
String desc;
|
||||
if (this.title.getVisibility() == View.VISIBLE) {
|
||||
desc = getContext().getString(R.string.audio);
|
||||
} else {
|
||||
desc = getContext().getString(R.string.voice_message);
|
||||
}
|
||||
desc += "\n" + this.timestamp.getText();
|
||||
if (title.getVisibility() == View.VISIBLE) {
|
||||
desc += "\n" + this.title.getText();
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
private void updateProgress(AudioPlaybackState state) {
|
||||
int duration = Math.toIntExact(state.getDuration());
|
||||
int position = Math.toIntExact(state.getCurrentPosition());
|
||||
|
||||
if (duration > 0) {
|
||||
this.progress = position;
|
||||
this.duration = duration;
|
||||
updateTimestampsAndSeekBar();
|
||||
}
|
||||
}
|
||||
|
||||
public void disablePlayer(boolean disable) {
|
||||
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
|
||||
seekBar.getGlobalVisibleRect(rect);
|
||||
}
|
||||
|
||||
private void togglePlayPause(boolean expectedPlaying) {
|
||||
isPlaying = expectedPlaying;
|
||||
Drawable expectedDrawable = expectedPlaying ? pauseDrawable : playDrawable;
|
||||
|
||||
boolean isAnimating = false;
|
||||
Drawable currentDrawable = playPauseButton.getDrawable();
|
||||
if (currentDrawable instanceof AnimatedVectorDrawableCompat) {
|
||||
isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning();
|
||||
}
|
||||
if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) {
|
||||
AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable;
|
||||
String contentDescription = getContext().getString(
|
||||
expectedPlaying ? R.string.menu_pause : R.string.menu_play);
|
||||
|
||||
if (animDrawable != null) {
|
||||
playPauseButton.setImageDrawable(animDrawable);
|
||||
playPauseButton.setContentDescription(contentDescription);
|
||||
|
||||
animDrawable.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(AudioPlaybackState state) {
|
||||
if (audioUri == null || state == null) return;
|
||||
|
||||
// Check if this state is about this message
|
||||
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
|
||||
|
||||
if (isThisMessage) {
|
||||
updateUIForPlaybackState(state);
|
||||
} else {
|
||||
togglePlayPause(false);
|
||||
|
||||
// Also clear progress to avoid confusion
|
||||
this.progress = 0;
|
||||
updateTimestampsAndSeekBar();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUIForPlaybackState(AudioPlaybackState state) {
|
||||
switch (state.getStatus()) {
|
||||
case PLAYING:
|
||||
togglePlayPause(true);
|
||||
updateProgress(state);
|
||||
break;
|
||||
|
||||
case PAUSED:
|
||||
togglePlayPause(false);
|
||||
updateProgress(state);
|
||||
break;
|
||||
|
||||
case LOADING:
|
||||
case ERROR:
|
||||
// No special handling yet
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDurationsChanged(Map<Integer, Long> durations) {
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
// When there is no playback happening, msgId can be -1 and audioUri is null
|
||||
if (state != null &&
|
||||
msgId >= 0 && msgId == state.getMsgId() &&
|
||||
audioUri != null && audioUri.equals(state.getAudioUri())) {
|
||||
return; // Is playing this message
|
||||
}
|
||||
|
||||
Long duration = durations.get(msgId);
|
||||
if (duration != null && seekBar.getMax() <= 100) {
|
||||
this.duration = Math.toIntExact(duration);
|
||||
updateTimestampsAndSeekBar();
|
||||
seekBar.setMax(this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimestampsAndSeekBar() {
|
||||
String progressText = DateUtils.getFormatedDuration(progress);
|
||||
String durationText = DateUtils.getFormatedDuration(duration);
|
||||
timestamp.setText(String.format("%s / %s", progressText, durationText));
|
||||
seekBar.setProgress(progress);
|
||||
seekBar.setMax(duration);
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public class AccountManager {
|
||||
// ui
|
||||
|
||||
public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) {
|
||||
AccountSelectionListFragment dialog = new AccountSelectionListFragment(activity, selectOnly);
|
||||
AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly);
|
||||
dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,23 @@ public class DcEventCenter {
|
||||
});
|
||||
}
|
||||
|
||||
public void handleLogging(@NonNull DcEvent event) {
|
||||
final String logPrefix = "[accId="+event.getAccountId()+ "] ";
|
||||
switch (event.getId()) {
|
||||
case DcContext.DC_EVENT_INFO:
|
||||
Log.i("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_WARNING:
|
||||
Log.w("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_ERROR:
|
||||
Log.e("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public long handleEvent(@NonNull DcEvent event) {
|
||||
int accountId = event.getAccountId();
|
||||
int id = event.getId();
|
||||
@@ -205,20 +222,7 @@ public class DcEventCenter {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final String logPrefix = "[accId="+accountId + "] ";
|
||||
switch (id) {
|
||||
case DcContext.DC_EVENT_INFO:
|
||||
Log.i("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_WARNING:
|
||||
Log.w("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_ERROR:
|
||||
Log.e("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
}
|
||||
handleLogging(event);
|
||||
|
||||
if (accountId != context.getDcContext().getAccountId()) {
|
||||
return 0;
|
||||
|
||||
@@ -132,7 +132,7 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(68, context.getString(R.string.device_talk));
|
||||
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
|
||||
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
|
||||
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=RxuXoa0vhvTs0QLsWM45Ues0&a=adb%40arcanechat.me&n=adb&b=ArcaneChat+Channel"));
|
||||
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=3KvvQZfzU4t-9u5s0PF3USGp&i=X-QDZ681F6Plz_uBu47CKdg4&s=IwE4LraLDcdPBW597wB7DBnI&a=arcanechat%40arcanechat.me&n=ArcaneChat&g=ArcaneChat+Community"));
|
||||
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
|
||||
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
|
||||
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
|
||||
@@ -210,8 +210,6 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
|
||||
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
|
||||
dcContext.setStockTranslation(193, context.getString(R.string.donate_device_msg));
|
||||
dcContext.setStockTranslation(194, context.getString(R.string.outgoing_call));
|
||||
dcContext.setStockTranslation(195, context.getString(R.string.incoming_call));
|
||||
dcContext.setStockTranslation(196, context.getString(R.string.declined_call));
|
||||
dcContext.setStockTranslation(197, context.getString(R.string.canceled_call));
|
||||
dcContext.setStockTranslation(198, context.getString(R.string.missed_call));
|
||||
@@ -223,6 +221,12 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
|
||||
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));
|
||||
dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation));
|
||||
dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call));
|
||||
dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call));
|
||||
dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call));
|
||||
dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call));
|
||||
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
|
||||
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
|
||||
}
|
||||
|
||||
public static File getImexDir() {
|
||||
|
||||
@@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity;
|
||||
import org.thoughtcrime.securesms.WebxdcStoreActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
|
||||
@@ -121,7 +121,7 @@ public class AttachmentManager {
|
||||
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
|
||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
|
||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.setEditClickListener(new EditButtonListener());
|
||||
thumbnail.setOnClickListener(new ThumbnailClickListener());
|
||||
}
|
||||
@@ -152,8 +152,6 @@ public class AttachmentManager {
|
||||
|
||||
markGarbage(getSlideUri());
|
||||
slide = Optional.absent();
|
||||
|
||||
audioView.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +233,8 @@ public class AttachmentManager {
|
||||
@NonNull final MediaType mediaType,
|
||||
final int width,
|
||||
final int height,
|
||||
final int chatId)
|
||||
final int chatId,
|
||||
AudioPlaybackViewModel playbackViewModel)
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
@@ -285,26 +284,12 @@ public class AttachmentManager {
|
||||
setAttachmentPresent(true);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
class SetDurationListener implements AudioSlidePlayer.Listener {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onStop() {}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
((AudioView) removableMediaView.getCurrent()).setDuration(millis);
|
||||
}
|
||||
}
|
||||
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener());
|
||||
audioSlidePlayer.requestDuration();
|
||||
|
||||
audioView.setAudio((AudioSlide) slide, 0);
|
||||
audioView.setPlaybackViewModel(playbackViewModel);
|
||||
audioView.setAudio((AudioSlide) slide);
|
||||
removableMediaView.display(audioView, false);
|
||||
removableMediaView.addRemoveClickListener(v -> {
|
||||
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
|
||||
});
|
||||
result.set(true);
|
||||
} else if (slide.isVcard()) {
|
||||
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
|
||||
|
||||
@@ -61,6 +61,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class NotificationCenter {
|
||||
private static final String TAG = NotificationCenter.class.getSimpleName();
|
||||
@NonNull private final ApplicationContext context;
|
||||
@@ -164,7 +166,7 @@ public class NotificationCenter {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
||||
}
|
||||
|
||||
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept) {
|
||||
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept, boolean hasVideo) {
|
||||
final Intent chatIntent = new Intent(context, ConversationActivity.class)
|
||||
.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId)
|
||||
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId)
|
||||
@@ -184,6 +186,7 @@ public class NotificationCenter {
|
||||
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatData.chatId);
|
||||
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
|
||||
intent.putExtra(CallActivity.EXTRA_HASH, hash);
|
||||
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
|
||||
intent.setPackage(context.getPackageName());
|
||||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(chatIntent)
|
||||
@@ -427,6 +430,13 @@ public class NotificationCenter {
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accId);
|
||||
boolean hasVideo;
|
||||
try {
|
||||
hasVideo = context.getRpc().callInfo(accId, callId).hasVideo;
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Rpc.callInfo() failed", e);
|
||||
hasVideo = false;
|
||||
}
|
||||
int chatId = dcContext.getMsg(callId).getChatId();
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
String name = dcChat.getName();
|
||||
@@ -434,7 +444,7 @@ public class NotificationCenter {
|
||||
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
|
||||
|
||||
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
|
||||
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false);
|
||||
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false, hasVideo);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
@@ -459,7 +469,7 @@ public class NotificationCenter {
|
||||
new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_videocam_white_24dp,
|
||||
context.getString(R.string.answer_call),
|
||||
getOpenCallIntent(chatData, callId, payload, true)).build());
|
||||
getOpenCallIntent(chatData, callId, payload, true, hasVideo)).build());
|
||||
|
||||
Bitmap bitmap = getAvatar(dcChat);
|
||||
if (bitmap != null) {
|
||||
|
||||
+24
-7
@@ -18,6 +18,8 @@ import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -55,11 +57,22 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
CheckBoxPreference multiDeviceCheckbox;
|
||||
CheckBoxPreference mvboxMoveCheckbox;
|
||||
CheckBoxPreference onlyFetchMvboxCheckbox;
|
||||
private ActivityResultLauncher<Intent> screenLockLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
// Register activity result launcher for screen lock
|
||||
screenLockLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
openRelayListActivity();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
showEmails = (ListPreference) this.findPreference("pref_show_emails");
|
||||
if (showEmails != null) {
|
||||
showEmails.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
@@ -158,8 +171,7 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
Preference relayListBtn = this.findPreference("pref_relay_list_button");
|
||||
if (relayListBtn != null) {
|
||||
relayListBtn.setOnPreferenceClickListener(((preference) -> {
|
||||
boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT);
|
||||
if (!result) {
|
||||
if (!applyScreenLockWithLauncher()) {
|
||||
openRelayListActivity();
|
||||
}
|
||||
return true;
|
||||
@@ -191,12 +203,17 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
onlyFetchMvboxCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT) {
|
||||
openRelayListActivity();
|
||||
private boolean applyScreenLockWithLauncher() {
|
||||
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) requireActivity().getSystemService(Context.KEYGUARD_SERVICE);
|
||||
Intent intent;
|
||||
if (keyguardManager != null) {
|
||||
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.transports), getString(R.string.enter_system_secret_to_continue));
|
||||
if (intent != null) {
|
||||
screenLockLauncher.launch(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected File copyToCacheDir(Uri uri) throws IOException {
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -30,11 +32,22 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
private ListPreference mediaQuality;
|
||||
private ListPreference autoDownload;
|
||||
private ActivityResultLauncher<Intent> screenLockLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
// Register activity result launcher for screen lock
|
||||
screenLockLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
performBackup();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
mediaQuality = (ListPreference) this.findPreference("pref_compression");
|
||||
if (mediaQuality != null) {
|
||||
mediaQuality.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
@@ -113,14 +126,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_BACKUP) {
|
||||
performBackup();
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final String quality;
|
||||
if (Prefs.isHardCompressionEnabled(context)) {
|
||||
@@ -131,6 +136,19 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
return context.getString(R.string.pref_outgoing_media_quality) + " " + quality;
|
||||
}
|
||||
|
||||
private boolean applyScreenLockWithLauncher() {
|
||||
android.app.KeyguardManager keyguardManager = (android.app.KeyguardManager) requireActivity().getSystemService(Context.KEYGUARD_SERVICE);
|
||||
Intent intent;
|
||||
if (keyguardManager != null) {
|
||||
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.pref_backup), getString(R.string.enter_system_secret_to_continue));
|
||||
if (intent != null) {
|
||||
screenLockLauncher.launch(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/***********************************************************************************************
|
||||
* Backup
|
||||
**********************************************************************************************/
|
||||
@@ -138,8 +156,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
private class BackupListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.pref_backup), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_BACKUP);
|
||||
if (!result) {
|
||||
if (!applyScreenLockWithLauncher()) {
|
||||
performBackup();
|
||||
}
|
||||
return true;
|
||||
|
||||
+22
-17
@@ -16,6 +16,8 @@ import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
@@ -40,11 +42,30 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
private CheckBoxPreference notificationsEnabled;
|
||||
private CheckBoxPreference mentionNotifEnabled;
|
||||
private CheckBoxPreference reliableService;
|
||||
private ActivityResultLauncher<Intent> ringtonePickerLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
// Register activity result launcher for ringtone picker
|
||||
ringtonePickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
|
||||
Prefs.removeNotificationRingtone(getContext());
|
||||
} else {
|
||||
Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY);
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.findPreference(Prefs.LED_COLOR_PREF)
|
||||
.setOnPreferenceChangeListener(new ListSummaryListener());
|
||||
this.findPreference(Prefs.RINGTONE_PREF)
|
||||
@@ -66,7 +87,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
|
||||
|
||||
startActivityForResult(intent, REQUEST_CODE_NOTIFICATION_SELECTED);
|
||||
ringtonePickerLauncher.launch(intent);
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -137,22 +158,6 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
reliableService.setOnPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_NOTIFICATION_SELECTED && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
|
||||
Prefs.removeNotificationRingtone(getContext());
|
||||
} else {
|
||||
Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY);
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
Context context = getContext();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.qr;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.journeyapps.barcodescanner.BarcodeResult;
|
||||
import com.journeyapps.barcodescanner.CaptureManager;
|
||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
|
||||
|
||||
public class CustomCaptureManager extends CaptureManager {
|
||||
|
||||
private OnResultInterceptor interceptor;
|
||||
|
||||
public CustomCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) {
|
||||
super(activity, barcodeView);
|
||||
}
|
||||
|
||||
public void setResultInterceptor(OnResultInterceptor interceptor) {
|
||||
this.interceptor = interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void returnResult(BarcodeResult rawResult) {
|
||||
if (interceptor != null) {
|
||||
interceptor.onResult(rawResult, () -> {
|
||||
super.returnResult(rawResult);
|
||||
});
|
||||
} else {
|
||||
super.returnResult(rawResult);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnResultInterceptor {
|
||||
void onResult(BarcodeResult result, Runnable finishCallback);
|
||||
}
|
||||
}
|
||||
@@ -325,25 +325,29 @@ public class QrCodeHandler {
|
||||
}
|
||||
builder.setMessage(msg);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||
try {
|
||||
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
|
||||
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
|
||||
|
||||
Intent intent = new Intent(activity, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
|
||||
builder1.setMessage(e.getMessage());
|
||||
builder1.setPositiveButton(android.R.string.ok, null);
|
||||
builder1.create().show();
|
||||
}
|
||||
secureJoinByQr(qrRawString, source, uipath);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
public void addRelay(String qrData) {
|
||||
public void secureJoinByQr(String qrRawString, SecurejoinSource source, SecurejoinUiPath uipath) {
|
||||
try {
|
||||
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
|
||||
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
|
||||
|
||||
Intent intent = new Intent(activity, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
|
||||
builder1.setMessage(e.getMessage());
|
||||
builder1.setPositiveButton(android.R.string.ok, null);
|
||||
builder1.create().show();
|
||||
}
|
||||
}
|
||||
|
||||
public void addRelay(String qrData) {
|
||||
ProgressDialog progressDialog = new ProgressDialog(activity);
|
||||
progressDialog.setMessage(activity.getResources().getString(R.string.one_moment));
|
||||
progressDialog.setCanceledOnTouchOutside(false);
|
||||
|
||||
@@ -10,8 +10,12 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureManager;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import com.journeyapps.barcodescanner.CompoundBarcodeView;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
@@ -26,10 +30,12 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
|
||||
public static final String ADD_AS_SECOND_DEVICE_EXTRA = "add_as_second_device";
|
||||
public static final String QRDATA_EXTRA = "qrdata";
|
||||
|
||||
private CaptureManager capture;
|
||||
private CustomCaptureManager capture;
|
||||
|
||||
private CompoundBarcodeView barcodeScannerView;
|
||||
|
||||
private DcContext dcContext;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -66,6 +72,8 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
|
||||
.onAnyResult(this::handleQrScanWithPermissions)
|
||||
.onAnyDenied(this::handleQrScanWithDeniedPermission)
|
||||
.execute();
|
||||
|
||||
dcContext = DcHelper.getContext(this);
|
||||
}
|
||||
|
||||
private void handleQrScanWithPermissions() {
|
||||
@@ -89,23 +97,70 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
|
||||
DcHelper.openHelp(this, "#multiclient");
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_paste) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QRDATA_EXTRA, Util.getTextFromClipboard(this));
|
||||
setResult(Activity.RESULT_OK, intent);
|
||||
finish();
|
||||
String rawQr = Util.getTextFromClipboard(this);
|
||||
|
||||
Runnable okCallback = () -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QRDATA_EXTRA, rawQr);
|
||||
setResult(Activity.RESULT_OK, intent);
|
||||
finish();
|
||||
};
|
||||
|
||||
showConfirmDialog(rawQr, okCallback, null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
private void showConfirmDialog(String rawQr, @NonNull Runnable okCallback, @Nullable Runnable cancelCallback) {
|
||||
DcLot qrParsed = dcContext.checkQr(rawQr);
|
||||
|
||||
String dialogMsg = "";
|
||||
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT) {
|
||||
String name = dcContext.getContact(qrParsed.getId()).getDisplayName();
|
||||
dialogMsg = getString(R.string.instant_onboarding_confirm_contact, name);
|
||||
} else if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
|
||||
String groupName = qrParsed.getText1();
|
||||
dialogMsg = getString(R.string.instant_onboarding_confirm_group, groupName);
|
||||
}
|
||||
|
||||
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT
|
||||
|| qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
|
||||
AlertDialog confirmDialog = new AlertDialog.Builder(this)
|
||||
.setMessage(dialogMsg)
|
||||
.setPositiveButton("OK", (dialog, which) -> {
|
||||
okCallback.run();
|
||||
})
|
||||
.setNegativeButton("Cancel", (dialog, which) -> {
|
||||
if (cancelCallback != null) {
|
||||
cancelCallback.run();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
okCallback.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void init(CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) {
|
||||
capture = new CaptureManager(this, barcodeScannerView);
|
||||
capture = new CustomCaptureManager(this, barcodeScannerView);
|
||||
|
||||
capture.setResultInterceptor((result, finishCallback) -> {
|
||||
String rawQr = result.getText();
|
||||
|
||||
showConfirmDialog(rawQr, finishCallback, () -> {
|
||||
barcodeScannerView.resume();
|
||||
capture.decode();
|
||||
});
|
||||
});
|
||||
|
||||
capture.initializeFromIntent(intent, savedInstanceState);
|
||||
capture.decode();
|
||||
}
|
||||
|
||||
@@ -36,19 +36,24 @@ import chat.delta.rpc.types.Reactions;
|
||||
|
||||
public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate {
|
||||
private static final String TAG = ReactionsDetailsFragment.class.getSimpleName();
|
||||
private static final String ARG_MSG_ID = "msg_id";
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private ReactionRecipientsAdapter adapter;
|
||||
private final int msgId;
|
||||
private int msgId;
|
||||
|
||||
public ReactionsDetailsFragment(int msgId) {
|
||||
super();
|
||||
this.msgId = msgId;
|
||||
public static ReactionsDetailsFragment newInstance(int msgId) {
|
||||
ReactionsDetailsFragment fragment = new ReactionsDetailsFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_MSG_ID, msgId);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
msgId = getArguments() != null? getArguments().getInt(ARG_MSG_ID, 0) : 0;
|
||||
adapter = new ReactionRecipientsAdapter(requireActivity(), GlideApp.with(requireActivity()), new ListClickListener());
|
||||
|
||||
LayoutInflater inflater = requireActivity().getLayoutInflater();
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.relay;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@@ -47,6 +49,9 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
/** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */
|
||||
private String qrData = null;
|
||||
|
||||
/** Relay selected for context menu via onRelayLongClick() */
|
||||
private EnteredLoginParam contextMenuRelay = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -143,14 +148,54 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelayEdit(EnteredLoginParam relay) {
|
||||
public void onRelayLongClick(View view, EnteredLoginParam relay) {
|
||||
contextMenuRelay = relay;
|
||||
registerForContextMenu(view);
|
||||
openContextMenu(view);
|
||||
unregisterForContextMenu(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.relay_item_context, menu);
|
||||
|
||||
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.addr != null;
|
||||
boolean isMain = nonNullAddr && contextMenuRelay.addr.equals(adapter.getMainRelay());
|
||||
menu.findItem(R.id.menu_delete_relay).setVisible(!isMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(android.view.Menu menu) {
|
||||
super.onContextMenuClosed(menu);
|
||||
contextMenuRelay = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(@NonNull MenuItem item) {
|
||||
if (contextMenuRelay == null) return super.onContextItemSelected(item);
|
||||
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.menu_edit_relay) {
|
||||
onRelayEdit(contextMenuRelay);
|
||||
contextMenuRelay = null;
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_delete_relay) {
|
||||
onRelayDelete(contextMenuRelay);
|
||||
contextMenuRelay = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
private void onRelayEdit(EnteredLoginParam relay) {
|
||||
Intent intent = new Intent(this, EditRelayActivity.class);
|
||||
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelayDelete(EnteredLoginParam relay) {
|
||||
private void onRelayDelete(EnteredLoginParam relay) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.remove_transport)
|
||||
.setMessage(getString(R.string.confirm_remove_transport, relay.addr))
|
||||
|
||||
@@ -25,8 +25,7 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
|
||||
public interface OnRelayClickListener {
|
||||
void onRelayClick(EnteredLoginParam relay);
|
||||
void onRelayEdit(EnteredLoginParam relay);
|
||||
void onRelayDelete(EnteredLoginParam relay);
|
||||
void onRelayLongClick(View view, EnteredLoginParam relay);
|
||||
}
|
||||
|
||||
public RelayListAdapter(OnRelayClickListener listener) {
|
||||
@@ -67,16 +66,12 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
private final TextView titleText;
|
||||
private final TextView subtitleText;
|
||||
private final ImageView mainIndicator;
|
||||
private final ImageView editButton;
|
||||
private final ImageView deleteButton;
|
||||
|
||||
public RelayViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
titleText = itemView.findViewById(R.id.title);
|
||||
subtitleText = itemView.findViewById(R.id.subtitle);
|
||||
mainIndicator = itemView.findViewById(R.id.main_indicator);
|
||||
editButton = itemView.findViewById(R.id.edit_button);
|
||||
deleteButton = itemView.findViewById(R.id.delete_button);
|
||||
}
|
||||
|
||||
public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) {
|
||||
@@ -84,7 +79,6 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
titleText.setText(parts.length == 2? parts[1] : parts[0]);
|
||||
subtitleText.setText(parts.length == 2? parts[0] : "");
|
||||
mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE);
|
||||
deleteButton.setVisibility(isMain ? View.GONE : View.VISIBLE);
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
@@ -92,16 +86,11 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
}
|
||||
});
|
||||
|
||||
editButton.setOnClickListener(v -> {
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onRelayEdit(relay);
|
||||
}
|
||||
});
|
||||
|
||||
deleteButton.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onRelayDelete(relay);
|
||||
listener.onRelayLongClick(v, relay);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
|
||||
public static final int SELECT_STICKER_REQUEST_CODE = 123;
|
||||
|
||||
private EditorModel restoredModel;
|
||||
|
||||
@Nullable
|
||||
@@ -195,12 +193,6 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
startTextEntityEditing(element, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
imageEditorHud.enterMode(ImageEditorHud.Mode.NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
|
||||
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.session.MediaSession;
|
||||
import androidx.media3.session.MediaSessionService;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionCommands;
|
||||
import androidx.media3.session.SessionResult;
|
||||
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
|
||||
public class AudioPlaybackService extends MediaSessionService {
|
||||
|
||||
private static final String TAG = AudioPlaybackService.class.getSimpleName();
|
||||
|
||||
private ExoPlayer player;
|
||||
private MediaSession session;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
|
||||
.build();
|
||||
|
||||
player = new ExoPlayer.Builder(this)
|
||||
.setAudioAttributes(audioAttributes, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.build();
|
||||
|
||||
// This is for click on the notification to go back to app
|
||||
Intent intent = new Intent(this, ConversationListActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent initialIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
session = new MediaSession.Builder(this, player)
|
||||
.setSessionActivity(initialIntent)
|
||||
.setCallback(new MediaSession.Callback() {
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session,
|
||||
MediaSession.ControllerInfo controller
|
||||
) {
|
||||
SessionCommands sessionCommands = MediaSession
|
||||
.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle()))
|
||||
.build();
|
||||
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(sessionCommands)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<SessionResult> onCustomCommand(
|
||||
MediaSession session,
|
||||
MediaSession.ControllerInfo controller,
|
||||
SessionCommand customCommand,
|
||||
Bundle args
|
||||
) {
|
||||
if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) {
|
||||
updateSessionActivity(args);
|
||||
}
|
||||
return Futures.immediateFuture(
|
||||
new SessionResult(SessionResult.RESULT_SUCCESS));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void updateSessionActivity(Bundle args) {
|
||||
try {
|
||||
// Put all the original extras back into the intent
|
||||
if (args != null && !args.isEmpty()) {
|
||||
String activityClassName = args.getString("activity_class");
|
||||
args.remove("activity_class");
|
||||
|
||||
if (activityClassName != null) {
|
||||
Class<?> activityClass = Class.forName(activityClassName);
|
||||
Intent intent = new Intent(this, activityClass);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtras(args);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
session.setSessionActivity(pendingIntent);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Activity class not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (session != null) {
|
||||
session.release();
|
||||
session = null;
|
||||
}
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,15 @@ public class ViewUtil {
|
||||
* @param view The view to apply insets to
|
||||
*/
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view) {
|
||||
applyWindowInsetsAsMargin(view, true, true, true, true);
|
||||
applyWindowInsetsAsMargin(view, true, true, true, true, false);
|
||||
}
|
||||
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsetsAsMargin(view, left, top, right, bottom, false);
|
||||
}
|
||||
|
||||
public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsetsAsMargin(view, left, top, right, bottom, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,8 +345,9 @@ public class ViewUtil {
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
* @param forceDispatch Whether to force application of insets
|
||||
*/
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) {
|
||||
// Only enable on API 30+ where WindowInsets APIs work correctly
|
||||
if (!isEdgeToEdgeSupported()) return;
|
||||
|
||||
@@ -371,10 +380,10 @@ public class ViewUtil {
|
||||
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
|
||||
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams;
|
||||
marginParams.leftMargin = baseMarginLeft + insets.left;
|
||||
marginParams.topMargin = baseMarginTop + insets.top;
|
||||
marginParams.rightMargin = baseMarginRight + insets.right;
|
||||
marginParams.bottomMargin = baseMarginBottom + insets.bottom;
|
||||
marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft;
|
||||
marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop;
|
||||
marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight;
|
||||
marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom;
|
||||
v.setLayoutParams(marginParams);
|
||||
}
|
||||
|
||||
@@ -383,18 +392,54 @@ public class ViewUtil {
|
||||
|
||||
// Request the initial insets to be dispatched if the view is attached
|
||||
if (view.isAttachedToWindow()) {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
if (forceDispatch) {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
|
||||
if (insets != null) {
|
||||
ViewCompat.dispatchApplyWindowInsets(view, insets);
|
||||
}
|
||||
} else {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
|
||||
* Convenience method that applies insets to all sides.
|
||||
* IME insets are propagated to child views.
|
||||
*
|
||||
* @param view The view to apply insets to
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view) {
|
||||
applyWindowInsets(view, true, true, true, true);
|
||||
applyWindowInsets(view, true, true, true, true, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
|
||||
*
|
||||
* IME insets are propagated to child views.
|
||||
*
|
||||
* @param view The view to apply insets to
|
||||
* @param left Whether to apply left inset
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsets(view, left, top, right, bottom, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force applying window insets to a view by adding padding to avoid drawing elements behind system bars.
|
||||
*
|
||||
* @param view The view to apply insets to
|
||||
* @param left Whether to apply left inset
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
*/
|
||||
public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsets(view, left, top, right, bottom, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,8 +453,10 @@ public class ViewUtil {
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
* @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views
|
||||
* @param forceDispatch Force application of Insets, regardless if system think it shall dispatch
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) {
|
||||
// Only enable on API 30+ where WindowInsets APIs work correctly
|
||||
if (!isEdgeToEdgeSupported()) return;
|
||||
|
||||
@@ -442,12 +489,24 @@ public class ViewUtil {
|
||||
bottom ? basePaddingBottom + insets.bottom : basePaddingBottom
|
||||
);
|
||||
|
||||
if (consumeImeInsets) {
|
||||
windowInsets = new WindowInsetsCompat.Builder(windowInsets)
|
||||
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE)
|
||||
.build();
|
||||
}
|
||||
return windowInsets;
|
||||
});
|
||||
|
||||
// Request the initial insets to be dispatched if the view is attached
|
||||
if (view.isAttachedToWindow()) {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
if (forceDispatch) {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
|
||||
if (insets != null) {
|
||||
ViewCompat.dispatchApplyWindowInsets(view, insets);
|
||||
}
|
||||
} else {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
@@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
|
||||
public class VideoPlayer extends FrameLayout {
|
||||
|
||||
@Nullable private final PlayerView exoView;
|
||||
@Nullable private final StyledPlayerView exoView;
|
||||
|
||||
@Nullable private SimpleExoPlayer exoPlayer;
|
||||
@Nullable private Window window;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<LinearLayout android:id="@+id/audio_widget_container"
|
||||
android:orientation="vertical"
|
||||
@@ -12,39 +10,19 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/control_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView android:id="@+id/play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_play_circle_fill_white_48dp"
|
||||
android:scaleType="centerInside"
|
||||
tools:visibility="gone"
|
||||
android:contentDescription="@string/menu_play"/>
|
||||
|
||||
<ImageView android:id="@+id/pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_pause_circle_fill_white_48dp"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/menu_pause"/>
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
<ImageView android:id="@+id/play_pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="visible"
|
||||
android:background="@drawable/ic_circle_fill_white_48dp"
|
||||
android:backgroundTint="@color/audio_icon"
|
||||
android:src="@drawable/play_icon"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/menu_play"/>
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
@@ -54,7 +32,11 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="2"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
android:layout_gravity="center_vertical"
|
||||
android:progressTint="@color/audio_icon"
|
||||
android:progressTintMode="src_in"
|
||||
android:thumbTint="@color/audio_icon"
|
||||
android:thumbTintMode="src_in"/>
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_videocam_white_24dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
app:minHeight="100dp"
|
||||
app:maxHeight="300dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
android:id="@+id/attachment_audio"
|
||||
android:layout_width="230dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
/>
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_gravity="bottom"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="210dp"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="210dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -39,6 +39,24 @@
|
||||
</androidx.appcompat.widget.AppCompatEditText>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/chat_description_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/chat_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/chat_description"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="3" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_hints"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -47,6 +47,17 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/invitation_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:visibility="gone"
|
||||
android:textColor="@color/gray50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/information_label"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/audio_view"
|
||||
|
||||
@@ -59,30 +59,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="72dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/remove_transport"
|
||||
android:background="@drawable/touch_highlight_background"
|
||||
android:src="@drawable/ic_delete_white_24dp"
|
||||
app:tint="?attr/conversation_list_item_date_color" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edit_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="72dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/edit_transport"
|
||||
android:background="@drawable/touch_highlight_background"
|
||||
android:src="@drawable/ic_create_white_24dp"
|
||||
app:tint="?attr/conversation_list_item_date_color" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -26,9 +26,22 @@
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item android:id="@+id/menu_start_call"
|
||||
android:title="@string/start_call"
|
||||
android:icon="@drawable/ic_videocam_white_24dp"
|
||||
app:showAsAction="always" />
|
||||
android:title="@string/start_call"
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/baseline_call_24">
|
||||
<menu>
|
||||
<item android:id="@+id/menu_start_audio_call"
|
||||
android:title="@string/start_audio_call"
|
||||
android:icon="@drawable/baseline_call_24"
|
||||
app:iconTint="?attr/menu_icon_tint"
|
||||
/>
|
||||
<item android:id="@+id/menu_start_video_call"
|
||||
android:title="@string/start_video_call"
|
||||
android:icon="@drawable/ic_videocam_white_24dp"
|
||||
app:iconTint="?attr/menu_icon_tint"
|
||||
/>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item android:id="@+id/menu_all_media"
|
||||
android:title="@string/apps_and_media"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:title="@string/edit_transport"
|
||||
android:id="@+id/menu_edit_relay"/>
|
||||
|
||||
<item android:title="@string/remove_transport"
|
||||
android:id="@+id/menu_delete_relay"/>
|
||||
|
||||
</menu>
|
||||
@@ -21,6 +21,7 @@
|
||||
<string name="back">العودة</string>
|
||||
<string name="close">إغلاق</string>
|
||||
<string name="forward">إعادة توجيه</string>
|
||||
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
|
||||
<string name="mute">حجب المكرفون.</string>
|
||||
<string name="ephemeral_messages">الرسائل المُختفية</string>
|
||||
<string name="save">حفظ</string>
|
||||
@@ -55,6 +56,7 @@
|
||||
<string name="image">صورة</string>
|
||||
<string name="gif">صورة GIF</string>
|
||||
<string name="images">الصور</string>
|
||||
<!-- a noun, used for "Music" and other "Audio" files -->
|
||||
<string name="audio">الصوت</string>
|
||||
<string name="voice_message">رسالة صوتية</string>
|
||||
<string name="video">فيديو</string>
|
||||
@@ -163,6 +165,7 @@
|
||||
<string name="pref_your_name">اسمك</string>
|
||||
<!-- Label of the Bio/Signature/Status/Motto field -->
|
||||
<string name="pref_default_status_label">التوقيع</string>
|
||||
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
|
||||
<string name="pref_enter_sends">مفتاح الدخول يقوم بالإرسا</string>
|
||||
<string name="pref_vibrate">إهتز</string>
|
||||
<string name="pref_screen_security">تأمين الشاشة</string>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<string name="archive">Arxivləşmə</string>
|
||||
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
|
||||
<string name="unarchive">Arxivdən çıxarma</string>
|
||||
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
|
||||
<string name="mute">Səssiz</string>
|
||||
<string name="ephemeral_messages">Yox olan mesajlar</string>
|
||||
<string name="save">Saxla</string>
|
||||
@@ -87,6 +88,7 @@
|
||||
<string name="image">Şəkil</string>
|
||||
<string name="gif">Gif</string>
|
||||
<string name="images">Şəkillər</string>
|
||||
<!-- a noun, used for "Music" and other "Audio" files -->
|
||||
<string name="audio">Audio</string>
|
||||
<string name="voice_message">Səsli mesaj</string>
|
||||
<string name="forwarded_message">Köçürülmüş mesaj</string>
|
||||
@@ -278,6 +280,7 @@
|
||||
<string name="pref_your_name">Adın</string>
|
||||
<!-- Label of the Bio/Signature/Status/Motto field -->
|
||||
<string name="pref_default_status_label">Yazını mətni</string>
|
||||
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
|
||||
<string name="pref_enter_sends">Enter açarı ilə göndərmək</string>
|
||||
<string name="pref_enter_sends_explain">Enter düyməsini basaraq mətn mesajları göndərəcək</string>
|
||||
<string name="pref_outgoing_media_quality">Göndərilən medianın keyfiyyatı</string>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<string name="archive">Архивиране</string>
|
||||
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
|
||||
<string name="unarchive">Деархивиране</string>
|
||||
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
|
||||
<string name="mute">Изключване на звука</string>
|
||||
<string name="ephemeral_messages">Изчезващи съобщения</string>
|
||||
<string name="ephemeral_messages_hint">Приложимо за всички членове на този чат, ако използват Delta Chat; в противен случай те пак могат да копират, записват и препращат съобщения или да използват други клиенти за електронна поща.</string>
|
||||
@@ -146,6 +147,7 @@
|
||||
<string name="add_stickers_instructions">За да добавите стикери, натиснете върху \"Отваряне на папка Стикери\", създайте подпапка за Вашия пакет от стикери и дърпайте файлове със стикери и изображения тук</string>
|
||||
<string name="open_sticker_folder">Отваряне на папка Стикери</string>
|
||||
<string name="images">Изображения</string>
|
||||
<!-- a noun, used for "Music" and other "Audio" files -->
|
||||
<string name="audio">Аудио</string>
|
||||
<string name="voice_message">Гласово съобщение</string>
|
||||
<string name="forwarded">Препратено</string>
|
||||
@@ -559,6 +561,7 @@
|
||||
<string name="pref_your_name">Вашето име</string>
|
||||
<!-- Label of the Bio/Signature/Status/Motto field -->
|
||||
<string name="pref_default_status_label">Текст на подписа</string>
|
||||
<!-- Option name for changing behaviour of the "Enter key" to sending out a message directly (instead of adding new line). If in doubt, please refer to a similar option in other messengers. -->
|
||||
<string name="pref_enter_sends">Клавишът Enter изпраща</string>
|
||||
<string name="pref_enter_sends_explain">Натискането на клавиша Enter ще изпраща текстовите съобщения</string>
|
||||
<string name="pref_outgoing_media_quality">Качество на изходящата медия</string>
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
<string name="archive">بایگۊوی</string>
|
||||
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
|
||||
<string name="unarchive">و در زیڌن ز بایگۊوی</string>
|
||||
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
|
||||
<string name="mute">بؽ دونگ</string>
|
||||
<!-- Adjective "smth is muted". used to describe a muted or silenced chat, that e.g. does not make noise or cause notifications -->
|
||||
<string name="muted">بؽ دونگ</string>
|
||||
<string name="ephemeral_messages">پیوما بؽڌارکۊنی</string>
|
||||
<string name="ephemeral_messages_hint">یو ری پوی منتورا ای گوفت وو لوفت، ٱر دلتا چت ن و کار بگرن ائمال ابۊ. ولی هنی ترن پیوما ن لف گیری، زفت وو فوروارد کونن.</string>
|
||||
@@ -163,6 +165,7 @@
|
||||
<string name="add_stickers_instructions">سی ٱووردن برچسب ری «گۊشیڌن دوبلگه برچسب» بزنین، ی لم دوبلگه ای سی کتن برچسب خوتووݩ وورکل کۊنین وو برچسبا ن و اوچو بکشین</string>
|
||||
<string name="open_sticker_folder">گۊشیڌن دوبلگه استیکر</string>
|
||||
<string name="images">شؽواتا</string>
|
||||
<!-- a noun, used for "Music" and other "Audio" files -->
|
||||
<string name="audio">دونگ</string>
|
||||
<string name="voice_message">پیوم دونگ</string>
|
||||
<string name="forwarded">فوروارد وابیڌه</string>
|
||||
@@ -199,6 +202,7 @@
|
||||
<string name="games">بازی یل</string>
|
||||
<string name="tools">ٱوزار</string>
|
||||
<string name="app_size">هندا</string>
|
||||
<!-- Used as a headline for a date. E.g. in the details of a mini app in the app picker -->
|
||||
<string name="app_date_published">تیجنیڌن</string>
|
||||
<string name="add_to_chat">ٱووردن و گوفت وو لوفت</string>
|
||||
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user