mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d2b379e5a | |||
| d2c3ed8be3 | |||
| f870e9c8fd | |||
| d5982ba09f | |||
| 22d70d7bd4 | |||
| 09bdc32a9c | |||
| 7763ba17bf | |||
| 965204c46f | |||
| 815e4def6f | |||
| dc3ca8ff70 | |||
| 3bccb3fb84 | |||
| 402d93b5a4 | |||
| 927dc46431 | |||
| 87c32b1904 | |||
| bb97213460 | |||
| 9ca9bf1acd | |||
| 885c902b14 | |||
| dc4ee3f686 | |||
| 61bf5aaad4 | |||
| 18595cbee1 | |||
| 5d76586ac1 | |||
| dc78307df7 | |||
| 86a9cbfb45 | |||
| 623b20f713 | |||
| 5f4eae798f | |||
| 4a8609822d | |||
| 89ddc1e01f | |||
| 0ce42578fa | |||
| faa7ad0a35 | |||
| 38e8ceb253 | |||
| a32460f253 | |||
| 198268a4c3 | |||
| 71158970ae | |||
| 1383b06e86 | |||
| 24165e311b | |||
| 5ec892db34 | |||
| caef7eda29 | |||
| cf53af4778 | |||
| 415e0c2b5f | |||
| 9a22597473 | |||
| beb45af440 | |||
| 87a21eb0f2 | |||
| dea51cd356 | |||
| ed540e5584 | |||
| 9f80c9f35f | |||
| 5dec1b24cd | |||
| acb4eb2ae1 | |||
| 20c0354938 | |||
| eac112d602 | |||
| 9b4f659f67 | |||
| 0166d4e656 | |||
| 92b3761d2d | |||
| 484cee21c6 | |||
| 0e40318050 | |||
| b2cc76ff2e | |||
| 96acaaf000 | |||
| 400e5ea671 | |||
| 4fac460926 | |||
| 94a5631566 | |||
| ea91075107 | |||
| d765d3ddeb | |||
| 094fb1e2a4 |
@@ -72,14 +72,3 @@ jobs:
|
||||
files: |
|
||||
build/outputs/apk/foss/release/*.apk
|
||||
build/outputs/mapping/fossRelease/mapping-*.txt
|
||||
|
||||
- name: Release on ZapStore
|
||||
run: |
|
||||
export CHECKSUM=6e2c7cf6da53c3f1a78b523a6aacd6316dce3d74ace6f859c2676729ee439990
|
||||
curl -sL https://cdn.zapstore.dev/$CHECKSUM -o zapstore
|
||||
if echo "$CHECKSUM zapstore" | sha256sum -c --status; then
|
||||
chmod +x zapstore
|
||||
SIGN_WITH=${{ secrets.NOSTR_KEY }} ./zapstore publish --indexer-mode
|
||||
else
|
||||
echo "ERROR: checksum doesn't match!"
|
||||
fi
|
||||
|
||||
+11
@@ -243,3 +243,14 @@ $ANDROID_NDK_ROOT/ndk-stack --sym obj/local/armeabi-v7a --dump crash.txt > decod
|
||||
`obj/local/armeabi-v7a` is the extracted path from `deltachat-gplay-release-X.X.X.apk-symbols.zip` file from https://download.delta.chat/android/symbols/
|
||||
|
||||
Replace `armeabi-v7a` by the correct architecture the logs come from (can be guessed by trial and error)
|
||||
|
||||
### Deobfuscating Java/Kotlin Stack Traces
|
||||
|
||||
Because the app uses code minification (ProGuard/R8), Java stack traces in crash reports are obfuscated.
|
||||
To decode them, use `retrace` with the `mapping.txt` file that is included in the symbols zip:
|
||||
|
||||
```
|
||||
retrace mapping.txt crash.txt > decoded-crash.txt
|
||||
```
|
||||
|
||||
`mapping.txt` is extracted from the same `deltachat-gplay-release-X.X.X.apk-symbols.zip` file available at https://download.delta.chat/android/symbols/
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Better incoming call system integration
|
||||
* Calls are not experimental anymore and don't need to be manually enabled
|
||||
* Display a permanent notification when doing location streaming and get rid of dangerous "Access Location in Background" permission
|
||||
* Allow mini-apps to play audio without user interaction
|
||||
* Mark chats as unread (long tap a chat and select the corresponding option from the three-dot-menu)
|
||||
|
||||
## v2.49.0
|
||||
2026-04
|
||||
|
||||
* Fix file sharing to certain apps (e.g. Material Files, etc.)
|
||||
* Fix problem with calls when microphone permission is not granted
|
||||
* Fix taking pictures and videos in devices with SD cards
|
||||
* Fix flipped orientation for some images
|
||||
* Fix: avoid empty contact request chats when using invite links in a multi-device setup
|
||||
* Remove proxy toggle from profile editing to avoid confusion
|
||||
* Updated translations
|
||||
* Update to core 2.49.0
|
||||
|
||||
## v2.48.0
|
||||
2026-03
|
||||
|
||||
+4
-3
@@ -34,8 +34,8 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000741
|
||||
versionName "2.48.0"
|
||||
versionCode 30000742
|
||||
versionName "2.49.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
@@ -223,7 +223,7 @@ spotless {
|
||||
}
|
||||
format 'xml', {
|
||||
target 'src/*/res/**/*.xml', 'src/*/AndroidManifest.xml'
|
||||
targetExclude 'src/*/res/values*/strings.xml' // line-break changes invalidate translations
|
||||
targetExclude 'src/*/res/values*/strings*.xml' // line-break changes invalidate translations
|
||||
eclipseWtp('xml').configFile('spotless/eclipse-wtp-xml.prefs')
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
@@ -307,6 +307,7 @@ dependencies {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
gplayImplementation 'com.google.android.gms:play-services-location:21.3.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.assertj:assertj-core:3.27.3'
|
||||
|
||||
@@ -25,7 +25,7 @@ ArcaneChat is a Delta Chat client and was created with a focus on usability, goo
|
||||
<li>Multiple color themes/skins</li>
|
||||
<li>It is possible to disable profiles to completely disconnect them saving data/bandwidth</li>
|
||||
<li>You can easily see the connection status of all your profiles in the profile switcher</li>
|
||||
<li>Extra option to share location for 12 hours</li>
|
||||
<li>Extra option to share location for 24 hours</li>
|
||||
<li>Clicking on a message with a POI location, will open the POI on the map</li>
|
||||
<li>Last-seen status of contacts is shown in your contact list, like in WhatsApp, Telegram, etc.</li>
|
||||
<li>Videos are played in loop, useful for short GIF videos</li>
|
||||
|
||||
Generated
+15
-15
@@ -7,11 +7,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756239746,
|
||||
"narHash": "sha256-0ibN685tT+u/Nbmbrrq9G3mRUzct2Votyv/a7Wwv26s=",
|
||||
"lastModified": 1775939535,
|
||||
"narHash": "sha256-Zq1U7Vhw5ctvhJQy+3ShqIv7Mplf3XkgJY+QJnhfUGQ=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "256631d162ec883b2341ee59621516e1f65f0f6b",
|
||||
"rev": "d2e16192309bf3101e4998ffa62e914819dee077",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1741473158,
|
||||
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
|
||||
"lastModified": 1768818222,
|
||||
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
|
||||
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -79,11 +79,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756125398,
|
||||
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
|
||||
"lastModified": 1775710090,
|
||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
|
||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -95,11 +95,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1756159630,
|
||||
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
|
||||
"lastModified": 1775823930,
|
||||
"narHash": "sha256-ALT447J7FcxP/97J01A/gp/hgdO5lXRsm+zLMt+gIjc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
|
||||
"rev": "8c11f88bb9573a10a7d6bf87161ef08455ac70b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -138,11 +138,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763347184,
|
||||
"narHash": "sha256-6QH8hpCYJxifvyHEYg+Da0BotUn03BwLIvYo3JAxuqQ=",
|
||||
"lastModified": 1775877051,
|
||||
"narHash": "sha256-wpSQm2PD/w4uRo2wb8utk0b5hOBkkg/CZ1xICY+qB7M=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "08895cce80433978d5bfd668efa41c5e24578cbd",
|
||||
"rev": "08b4f3633471874c8894632ade1b78d75dbda002",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1393,6 +1393,7 @@ JNIEXPORT jint Java_com_b44t_messenger_DcMsg_getId(JNIEnv *env, jobject obj)
|
||||
return dc_msg_get_id(get_dc_msg(env, obj));
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getText(JNIEnv *env, jobject obj)
|
||||
{
|
||||
char* temp = dc_msg_get_text(get_dc_msg(env, obj));
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: 0411ca5fbf...570119830f
@@ -34,6 +34,7 @@ cd ../..
|
||||
SYMBOLS_ZIP="$APK-symbols.zip"
|
||||
rm $SYMBOLS_ZIP
|
||||
zip -r $SYMBOLS_ZIP obj
|
||||
zip $SYMBOLS_ZIP build/outputs/mapping/gplayRelease/mapping.txt
|
||||
ls -l $SYMBOLS_ZIP
|
||||
rsync --progress $SYMBOLS_ZIP jekyll@download.delta.chat:/var/www/html/download/android/symbols/
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/** Non-GMS, always uses the platform LocationManager. */
|
||||
public final class LocationSourceFactory {
|
||||
|
||||
private static final String TAG = LocationSourceFactory.class.getSimpleName();
|
||||
|
||||
private LocationSourceFactory() {}
|
||||
|
||||
public static LocationSource create(Context context) {
|
||||
Log.i(TAG, "Non-GMS build, Using platform LocationManager");
|
||||
return new PlatformLocationSource();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
import com.google.android.gms.location.Priority;
|
||||
|
||||
public class GmsLocationSource implements LocationSource {
|
||||
|
||||
private static final String TAG = GmsLocationSource.class.getSimpleName();
|
||||
private static final long UPDATE_INTERVAL_MS = 3_000;
|
||||
private static final long FASTEST_INTERVAL_MS = 1_000;
|
||||
|
||||
private FusedLocationProviderClient client;
|
||||
private LocationCallback locationCallback;
|
||||
|
||||
@Override
|
||||
public void startUpdates(@NonNull Context context, @NonNull Callback callback) {
|
||||
client = LocationServices.getFusedLocationProviderClient(context);
|
||||
|
||||
LocationRequest request =
|
||||
new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, UPDATE_INTERVAL_MS)
|
||||
.setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
|
||||
.setMinUpdateDistanceMeters(0)
|
||||
.setWaitForAccurateLocation(false)
|
||||
.build();
|
||||
|
||||
locationCallback =
|
||||
new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(@NonNull LocationResult result) {
|
||||
Location loc = result.getLastLocation();
|
||||
if (loc != null) {
|
||||
callback.onLocationUpdate(loc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
client.requestLocationUpdates(request, locationCallback, Looper.getMainLooper());
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Missing location permission", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopUpdates() {
|
||||
if (client != null && locationCallback != null) {
|
||||
client.removeLocationUpdates(locationCallback);
|
||||
client = null;
|
||||
locationCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
|
||||
/**
|
||||
* Prefers FusedLocationProviderClient, falls back to platform LocationManager if Play Services are
|
||||
* somehow unavailable.
|
||||
*/
|
||||
public final class LocationSourceFactory {
|
||||
|
||||
private static final String TAG = LocationSourceFactory.class.getSimpleName();
|
||||
|
||||
private LocationSourceFactory() {}
|
||||
|
||||
public static LocationSource create(Context context) {
|
||||
if (isGmsAvailable(context)) {
|
||||
Log.i(TAG, "Using FusedLocationProviderClient");
|
||||
return new GmsLocationSource();
|
||||
}
|
||||
Log.i(TAG, "GMS unavailable, falling back to LocationManager");
|
||||
return new PlatformLocationSource();
|
||||
}
|
||||
|
||||
private static boolean isGmsAvailable(Context context) {
|
||||
try {
|
||||
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
|
||||
== ConnectionResult.SUCCESS;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<!-- normal/instant permissions - adding them here is enough -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
@@ -70,6 +71,7 @@
|
||||
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||
|
||||
<!-- force compiling libs on older sdk than supported; runtime checks are required -->
|
||||
@@ -80,7 +82,6 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:allowBackup"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:largeHeap="true"
|
||||
@@ -463,10 +464,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".geolocation.LocationBackgroundService"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<service
|
||||
android:name=".service.GenericForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
@@ -475,6 +472,11 @@
|
||||
android:name=".service.FetchForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".geolocation.LocationStreamingService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.AudioPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
|
||||
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/cs/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1468,7 +1468,7 @@ Weitere Informationen findest du in unserem Blogbeitrag über <a href="https://d
|
||||
<p>Im April 2023 haben wir Sicherheits- und Datenschutzprobleme mit dem “In Chats geteilten Apps”-Feature behoben, die mit Fehlern beim Sandboxing, insbesondere mit Chromium zusammenhängen. Wir haben daraufhin eine unabhängige Sicherheitsprüfung von Cure53 durchführen lassen, und alle gefundenen Probleme wurden mit den im April 2023 veröffentlichten 1.36 Releases behoben. Siehe <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier für die vollständige Hintergrundgeschichte</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/de/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
|
||||
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im Jahr 2020 analysierte <a href="https://includesecurity.com">Include Security</a> Delta Chats Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>,<a href="https://github.com/async-email/async-smtp">SMTP</a>, und <a href="https://github.com/async-email/async-native-tls">TLS</a> Bibliotheken.
|
||||
|
||||
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/en/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -105,8 +105,7 @@
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
|
||||
available for mobile and desktop platforms.</p>
|
||||
<p>Delta Chat es una aplicación de mensajería instantánea confiable, descentralizada y segura, disponible para dispositivos móviles y computadoras de escritorio.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -1567,7 +1566,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/es/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1483,7 +1483,7 @@ research paper published afterwards.</p>
|
||||
Vous trouverez <a href="https://delta.chat/en/2023-05-22-webxdc-security">ici un article de fond complet à propos de la sécurité du chiffrement de bout-en-bout sur internet</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement d’acheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/fr/serverguide">recommandée sur ce site</a>.
|
||||
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement d’acheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/serverguide">recommandée sur ce site</a>.
|
||||
Vous trouverez plus d’informations sur cet audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sur notre blog</a> ou dans <a href="https://delta.chat/assets/blog/MER-01-report.pdf">le rapport complet ici</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/id/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1566,7 +1566,7 @@ Vedi <a href="https://delta.chat/en/2023-05-22-webxdc-security">qui per la stori
|
||||
<li>
|
||||
<p>A partire dal 2023, <a href="https://cure53.de">Cure53</a> ha analizzato sia la crittografia del trasporto delle
|
||||
Connessioni di rete di Delta Chat e una configurazione del server di posta riproducibile come
|
||||
<a href="https://delta.chat/it/serverguide">consigliato su questo sito</a>.
|
||||
<a href="https://delta.chat/serverguide">consigliato su questo sito</a>.
|
||||
Puoi leggere ulteriori informazioni sull’audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sul nostro blog</a>
|
||||
o leggere il <a href="https://delta.chat/assets/blog/MER-01-report.pdf">rapporto completo qui</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1572,7 +1572,7 @@ Lees <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier het volledi
|
||||
<li>
|
||||
<p>Aan het begin van 2023 heeft <a href="https://cure53.de">Cure53</a> de transportversleuteling van
|
||||
Delta Chats netwerkverbindingen getest, evenals de e-mailserveropzet zoals
|
||||
<a href="https://delta.chat/nl/serverguide">beschreven op onze site</a>.
|
||||
<a href="https://delta.chat/serverguide">beschreven op onze site</a>.
|
||||
Meer informatie over deze test is te lezen <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">op ons blog</a>
|
||||
of in het <a href="https://delta.chat/assets/blog/MER-01-report.pdf">volledige verslag</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ od najnowszych do najstarszych:</p>
|
||||
<p>W kwietniu 2023 r. naprawiliśmy problemy z bezpieczeństwem i prywatnością w funkcji „aplikacje internetowe udostępniane na czacie”, związane z awariami piaskownicy, szczególnie w przypadku Chromium. Następnie przeprowadziliśmy niezależny audyt bezpieczeństwa od Cure53 i wszystkie wykryte problemy zostały naprawione w aplikacji z serii 1.36 wydanej w kwietniu 2023 r. <a href="https://delta.chat/en/2023-05-22-webxdc-security">Pełną historię bezpieczeństwa end-to-end w sieci można znaleźć tutaj</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/pl/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
|
||||
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W 2020 r. firma <a href="https://includesecurity.com">Include Security</a> przeanalizowała biblioteki Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> i <a href="https://github.com/async-email/async-native-tls">TLS</a> Delta Chat. Nie znalazła żadnych problemów krytycznych ani poważnych. W raporcie zwrócono uwagę na kilka słabych punktów o średniej wadze – same w sobie nie stanowią zagrożenia dla użytkowników Delta Chat, ponieważ zależą od środowiska, w którym używany jest Delta Chat. Ze względu na użyteczność i kompatybilność nie możemy złagodzić wszystkich z nich i zdecydowaliśmy się przedstawić zalecenia dotyczące bezpieczeństwa zagrożonym użytkownikom. Pełny raport można przeczytać <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">tutaj</a>.</p>
|
||||
|
||||
@@ -1573,7 +1573,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/pt/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1571,11 +1571,10 @@ Applied Cryptography в ETH Цюрихе и устранили все выявл
|
||||
См. здесь <a href="https://delta.chat/en/2023-05-22-webxdc-security">полную информацию о безопасности сквозного шифрования в Интернете</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В Марте 2023 года, <a href="https://cure53.de">Cure53</a> проанализировал как протокол защиты транспортного уровня
|
||||
сетевого соединения Delta Chat, так и воспроизводимую установку почтового сервера,
|
||||
<a href="https://delta.chat/ru/serverguide">рекомендуемую на этом сайте</a>.
|
||||
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>
|
||||
или прочитать <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчёт здесь</a>.</p>
|
||||
<p>В марте 2023 года компания <a href="https://cure53.de">Cure53</a> провела анализ шифрования транспортного уровня сетевых соединений Delta Chat, а также проверку воспроизводимой конфигурации почтового сервера,
|
||||
<a href="https://delta.chat/serverguide">рекомендованной на этом сайте</a>.
|
||||
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>,
|
||||
а <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчет доступен по этой ссылке</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>2020 год, <a href="https://includesecurity.com">Include Security</a> проанализировала Delta
|
||||
|
||||
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/sk/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1581,7 +1581,7 @@ Shihni <a href="https://delta.chat/en/2023-05-22-webxdc-security">këtu, për sh
|
||||
<li>
|
||||
<p>Në fillim të 2023-it, <a href="https://cure53.de">Cure53</a> analizoi qoftë fshehtëzimin
|
||||
e transporteve për lidhje rrjeti të Delta Chat-it, qoftë një formësim të riprodhueshëm
|
||||
shërbyesi poste si <a href="https://delta.chat/sq/serverguide">të rekomanduarin në këtë sajt</a>.
|
||||
shërbyesi poste si <a href="https://delta.chat/serverguide">të rekomanduarin në këtë sajt</a>.
|
||||
Mund të lexoni më tepër rreth auditimit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">në blogun tonë</a>,
|
||||
ose të lexoni <a href="https://delta.chat/assets/blog/MER-01-report.pdf">raportin e plotë këtu</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -1391,7 +1391,7 @@ you need to look it up in the “keypairs” SQLite table of a profile backup ta
|
||||
<p>Починаючи з 2023 року, ми виправили проблеми з безпекою та конфіденційністю у функції “веб застосунків, що поширені у чаті”, пов’язані зі збоями в роботі пісочниці особливо в Chromium. Згодом ми отримали незалежний аудит безпеки аудит безпеки від Cure53, і всі знайдені проблеми були виправлені в серії додатків 1.36, випущених у квітні 2023 року. Повну історію про наскрізну безпеку в Інтернеті дивіться <a href="https://delta.chat/en/2023-05-22-webxdc-security">тут</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/uk/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
|
||||
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>У 2020 році <a href="https://includesecurity.com">Include Security</a> проаналізувала Rust-<a href="https://github.com/deltachat/deltachat-core-rust/">ядро</a> Delta Chat і бібліотеки <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> та <a href="https://github.com/async-email/async-native-tls">TLS</a>. Він не виявив критичних або серйозних проблем. У звіті виявлено кілька слабких місць середнього ступеня тяжкості – вони самі по собі не становлять загрози для користувачів Delta Chat оскільки вони залежать від середовища, у якому використовується Delta Chat. З міркувань зручності використання та сумісності ми не можемо пом’якшити їх усі тому вирішили надати рекомендації щодо безпеки користувачам, яким загрожує небезпека. Ви можете прочитати <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">повний звіт тут</a>.</p>
|
||||
|
||||
@@ -1557,7 +1557,7 @@ Delta Chat 长期接受第三方独立机构的安全审计与漏洞分析,
|
||||
</li>
|
||||
<li>
|
||||
<p>2023 年 3 月,<a href="https://cure53.de">Cure53</a> 分析了 Delta Chat 网络连接的传输加密和一个可重现的邮件服务器设置,如
|
||||
<a href="https://delta.chat/zh_CN/serverguide">本网站</a> 推荐的那样。
|
||||
<a href="https://delta.chat/serverguide">本网站</a> 推荐的那样。
|
||||
你可以在 <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">我们的博客</a> 上阅读有关审计的更多信息
|
||||
,或在此处阅读 <a href="https://delta.chat/assets/blog/MER-01-report.pdf">完整报告</a>。</p>
|
||||
</li>
|
||||
|
||||
@@ -396,7 +396,7 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets messages to be processed by the bot and returns their IDs.
|
||||
* (deprecated) Gets messages to be processed by the bot and returns their IDs.
|
||||
* <p>
|
||||
* Only messages with database ID higher than `last_msg_id` config value
|
||||
* are returned. After processing the messages, the bot should
|
||||
@@ -404,6 +404,13 @@ public class Rpc {
|
||||
* or manually updating the value to avoid getting already
|
||||
* processed messages.
|
||||
* <p>
|
||||
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
* even if it is not fully downloaded yet.
|
||||
* The bot needs to wait for the message to be fully downloaded.
|
||||
* Since this is usually not the desired behavior,
|
||||
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
* event for getting notified about new messages.
|
||||
* <p>
|
||||
* [`markseen_msgs`]: Self::markseen_msgs
|
||||
*/
|
||||
public java.util.List<Integer> getNextMsgs(Integer accountId) throws RpcException {
|
||||
@@ -411,7 +418,7 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for messages to be processed by the bot and returns their IDs.
|
||||
* (deprecated) Waits for messages to be processed by the bot and returns their IDs.
|
||||
* <p>
|
||||
* This function is similar to [`get_next_msgs`],
|
||||
* but waits for internal new message notification before returning.
|
||||
@@ -422,6 +429,13 @@ public class Rpc {
|
||||
* To shutdown the bot, stopping I/O can be used to interrupt
|
||||
* pending or next `wait_next_msgs` call.
|
||||
* <p>
|
||||
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
* even if it is not fully downloaded yet.
|
||||
* The bot needs to wait for the message to be fully downloaded.
|
||||
* Since this is usually not the desired behavior,
|
||||
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
* event for getting notified about new messages.
|
||||
* <p>
|
||||
* [`get_next_msgs`]: Self::get_next_msgs
|
||||
*/
|
||||
public java.util.List<Integer> waitNextMsgs(Integer accountId) throws RpcException {
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.connect.KeepAliveService;
|
||||
import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
|
||||
import org.thoughtcrime.securesms.notifications.InChatSounds;
|
||||
@@ -61,7 +60,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
private Rpc rpc;
|
||||
private DcContext dcContext;
|
||||
|
||||
private DcLocationManager dcLocationManager;
|
||||
private DcEventCenter eventCenter;
|
||||
private NotificationCenter notificationCenter;
|
||||
private JobManager jobManager;
|
||||
@@ -126,15 +124,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcLocationManager instance, waiting for initialization if necessary. This method is
|
||||
* thread-safe and will block until initialization is complete.
|
||||
*/
|
||||
public DcLocationManager getLocationManager() {
|
||||
ensureInitialized();
|
||||
return dcLocationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcEventCenter instance, waiting for initialization if necessary. This method is thread-safe
|
||||
* and will block until initialization is complete.
|
||||
@@ -282,7 +271,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
dcContext = dcAccounts.getSelectedAccount();
|
||||
notificationCenter = new NotificationCenter(this);
|
||||
dcLocationManager = new DcLocationManager(this, dcContext);
|
||||
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -26,6 +27,8 @@ import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
@@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
|
||||
public abstract class BaseConversationListFragment extends Fragment implements ActionMode.Callback {
|
||||
private static final String TAG = BaseConversationListFragment.class.getSimpleName();
|
||||
protected ActionMode actionMode;
|
||||
protected PulsingFloatingActionButton fab;
|
||||
|
||||
@@ -170,6 +174,17 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean areSomeSelectedChatsFresh() {
|
||||
DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
|
||||
for (long chatId : selectedChats) {
|
||||
if (dcContext.getFreshMsgCount((int) chatId) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handlePinAllSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations =
|
||||
@@ -229,6 +244,26 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMarkfreshSelected() {
|
||||
final Set<Long> selectedConversations =
|
||||
new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
|
||||
final Rpc rpc = DcHelper.getRpc(requireActivity());
|
||||
try {
|
||||
int accId = rpc.getSelectedAccountId();
|
||||
for (long chatId : selectedConversations) {
|
||||
rpc.markfreshChat(accId, (int) chatId);
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleArchiveAllSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
@@ -409,6 +444,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
if (!isRelayingMessageContent(requireActivity())) {
|
||||
final int selectedCount = getListAdapter().getBatchSelections().size();
|
||||
menu.findItem(R.id.menu_add_to_home_screen).setVisible(selectedCount == 1);
|
||||
|
||||
MenuItem archiveItem = menu.findItem(R.id.menu_archive_selected);
|
||||
if (offerToArchive()) {
|
||||
archiveItem.setIcon(R.drawable.ic_archive_white_24dp);
|
||||
@@ -417,6 +453,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
archiveItem.setIcon(R.drawable.ic_unarchive_white_24dp);
|
||||
archiveItem.setTitle(R.string.menu_unarchive_chat);
|
||||
}
|
||||
|
||||
MenuItem pinItem = menu.findItem(R.id.menu_pin_selected);
|
||||
if (areSomeSelectedChatsUnpinned()) {
|
||||
pinItem.setIcon(R.drawable.ic_pin_white);
|
||||
@@ -425,12 +462,17 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
pinItem.setIcon(R.drawable.ic_unpin_white);
|
||||
pinItem.setTitle(R.string.unpin_chat);
|
||||
}
|
||||
|
||||
MenuItem muteItem = menu.findItem(R.id.menu_mute_selected);
|
||||
if (areSomeSelectedChatsUnmuted()) {
|
||||
muteItem.setTitle(R.string.menu_mute);
|
||||
} else {
|
||||
muteItem.setTitle(R.string.menu_unmute);
|
||||
}
|
||||
|
||||
final boolean someAreFresh = areSomeSelectedChatsFresh();
|
||||
menu.findItem(R.id.menu_marknoticed_selected).setVisible(someAreFresh);
|
||||
menu.findItem(R.id.menu_markfresh_selected).setVisible(!someAreFresh);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +526,9 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
} else if (itemId == R.id.menu_marknoticed_selected) {
|
||||
handleMarknoticedSelected();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_markfresh_selected) {
|
||||
handleMarkfreshSelected();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_add_to_home_screen) {
|
||||
handleAddToHomeScreen();
|
||||
return true;
|
||||
|
||||
@@ -536,8 +536,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
menu.findItem(R.id.menu_start_call)
|
||||
.setVisible(
|
||||
Prefs.isCallsEnabled(this)
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& dcChat.canSend()
|
||||
&& dcChat.isEncrypted()
|
||||
&& !dcChat.isSelfTalk()
|
||||
|
||||
@@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.connect.DirectShareUtil;
|
||||
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
@@ -449,6 +450,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
refreshTitle();
|
||||
invalidateOptionsMenu();
|
||||
DirectShareUtil.triggerRefreshDirectShare(this);
|
||||
if (DcHelper.getContext(this).isSendingLocationsToChat(0)) {
|
||||
LocationStreamingService.ensureRunning(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -688,7 +692,11 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
// Util.copy(inputStream, new FileOutputStream(outputFile));
|
||||
// msg.setFile(outputFile, "image/jpeg");
|
||||
|
||||
msg.setText(getString(R.string.update_2_46ac, "https://arcanechat.me/#contribute", "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
|
||||
msg.setText(
|
||||
getString(
|
||||
R.string.update_2_46ac,
|
||||
"https://arcanechat.me/#contribute",
|
||||
"https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
|
||||
}
|
||||
dcContext.addDeviceMsg(deviceMsgLabel, msg);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class ShareLocationDialog {
|
||||
shareLocationUnit = 6 * 60 * 60;
|
||||
break;
|
||||
case 5:
|
||||
shareLocationUnit = 12 * 60 * 60;
|
||||
shareLocationUnit = 24 * 60 * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setMediaPlaybackRequiresUserGesture(false);
|
||||
webSettings.setAllowFileAccess(false);
|
||||
webSettings.setBlockNetworkLoads(!internetAccess);
|
||||
webSettings.setAllowContentAccess(false);
|
||||
|
||||
@@ -52,7 +52,8 @@ import org.webrtc.VideoTrack;
|
||||
public class CallActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = CallActivity.class.getSimpleName();
|
||||
private static final int PERMISSION_REQUEST_CODE = 1001;
|
||||
private static final int MIC_PERMISSION_REQUEST_CODE = 1001;
|
||||
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1002;
|
||||
|
||||
public static final String ACTION_ANSWER_CALL = BuildConfig.APPLICATION_ID + ".ANSWER_CALL";
|
||||
public static final String ACTION_DECLINE_CALL = BuildConfig.APPLICATION_ID + ".DECLINE_CALL";
|
||||
@@ -94,10 +95,30 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
private PowerManager.WakeLock proximityWakeLock;
|
||||
|
||||
// States
|
||||
private boolean awaitingPermissionResult = false;
|
||||
private boolean pausedWhileAwaitingPermission = false;
|
||||
private boolean intentHandled = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Destructive actions need nothing
|
||||
String action = getIntent() != null ? getIntent().getAction() : null;
|
||||
if (ACTION_DECLINE_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling DECLINE_CALL action from notification");
|
||||
CallCoordinator.getInstance(getApplication()).declineCall();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (ACTION_HANGUP_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling HANGUP_CALL action from notification");
|
||||
CallCoordinator.getInstance(getApplication()).hangUp();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
setupWindowFlags();
|
||||
@@ -108,11 +129,6 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
initializeProximityWakeLock();
|
||||
|
||||
if (!hasRequiredPermissions()) {
|
||||
requestRequiredPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// PiP listener
|
||||
addOnPictureInPictureModeChangedListener(
|
||||
pipModeInfo -> {
|
||||
@@ -139,47 +155,74 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
// Intent handling needs permissions
|
||||
if (!hasMicrophonePermission()) {
|
||||
awaitingPermissionResult = true;
|
||||
ActivityCompat.requestPermissions(
|
||||
this, new String[] {Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRequestCameraPermission()) {
|
||||
awaitingPermissionResult = true;
|
||||
ActivityCompat.requestPermissions(
|
||||
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
handleIntents(getIntent());
|
||||
intentHandled = true;
|
||||
}
|
||||
|
||||
private void handleIntents(Intent intent) {
|
||||
if (intent == null || viewModel == null) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
|
||||
if (!coordinator.hasActiveCall()) {
|
||||
Log.e(TAG, "No active call exists, cannot proceed");
|
||||
Toast.makeText(this, "No active call", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
Log.d(TAG, "handleIntents: action=" + action);
|
||||
|
||||
// Handle notification actions
|
||||
if (ACTION_ANSWER_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling ANSWER_CALL action from notification");
|
||||
viewModel.handleNotificationAnswer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Destructive actions without ViewModel
|
||||
if (ACTION_DECLINE_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling DECLINE_CALL action from notification");
|
||||
viewModel.handleNotificationDecline();
|
||||
Log.d(TAG, "Handling DECLINE_CALL action");
|
||||
if (viewModel != null) {
|
||||
viewModel.handleNotificationDecline();
|
||||
} else {
|
||||
coordinator.declineCall();
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ACTION_HANGUP_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling HANGUP_CALL action from notification");
|
||||
viewModel.handleNotificationHangup();
|
||||
Log.d(TAG, "Handling HANGUP_CALL action");
|
||||
if (viewModel != null) {
|
||||
viewModel.handleNotificationHangup();
|
||||
} else {
|
||||
coordinator.hangUp();
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!coordinator.hasActiveCall()) {
|
||||
Log.e(TAG, "No active call exists, cannot proceed");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ACTION_ANSWER_CALL.equals(action)) {
|
||||
Log.d(TAG, "Handling ANSWER_CALL action from notification");
|
||||
viewModel.handleNotificationAnswer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinator.hasOngoingCall()) {
|
||||
Log.d(TAG, "Resuming existing call");
|
||||
} else if (!coordinator.isIncomingCall()) {
|
||||
@@ -373,6 +416,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
case INITIALIZING:
|
||||
case PROMPTING_USER_ACCEPT:
|
||||
case ENDED:
|
||||
case ANSWERED_ELSEWHERE:
|
||||
case ERROR:
|
||||
default:
|
||||
finish();
|
||||
@@ -504,6 +548,8 @@ public class CallActivity extends AppCompatActivity {
|
||||
viewModel.getVideoEnabled(), v -> videoConfigChanged.setValue(true));
|
||||
videoConfigChanged.addSource(
|
||||
viewModel.getRemoteVideoEnabled(), v -> videoConfigChanged.setValue(true));
|
||||
videoConfigChanged.addSource(
|
||||
viewModel.getIsFrontCamera(), v -> videoConfigChanged.setValue(true));
|
||||
|
||||
// Video layout
|
||||
videoConfigChanged.observe(
|
||||
@@ -578,6 +624,23 @@ public class CallActivity extends AppCompatActivity {
|
||||
statusText.setText(R.string.call_reconnecting);
|
||||
break;
|
||||
|
||||
case ANSWERED_ELSEWHERE:
|
||||
statusText.setText(R.string.call_answered_elsewhere);
|
||||
incomingCallPrompt.setVisibility(View.GONE);
|
||||
bottomLayoutContainer.setVisibility(View.GONE);
|
||||
callerIconContainer.setVisibility(View.GONE);
|
||||
answerModeSelector.setVisibility(View.GONE);
|
||||
|
||||
new Handler(Looper.getMainLooper())
|
||||
.postDelayed(
|
||||
() -> {
|
||||
if (!isFinishing()) {
|
||||
finish();
|
||||
}
|
||||
},
|
||||
1500);
|
||||
break;
|
||||
|
||||
case ENDED:
|
||||
statusText.setText(R.string.call_ended);
|
||||
finish();
|
||||
@@ -705,17 +768,21 @@ public class CallActivity extends AppCompatActivity {
|
||||
VideoTrack localTrack = viewModel.getLocalVideoTrack().getValue();
|
||||
VideoTrack remoteTrack = viewModel.getRemoteVideoTrack().getValue();
|
||||
|
||||
boolean isFront = Boolean.TRUE.equals(viewModel.getIsFrontCamera().getValue());
|
||||
|
||||
boolean showFullScreen = false;
|
||||
|
||||
if (state == CallViewModel.CallState.CONNECTED
|
||||
&& remoteTrack != null
|
||||
&& Boolean.TRUE.equals(remoteVideoEnabled)) {
|
||||
remoteVideoView.setMirror(false);
|
||||
remoteTrack.addSink(remoteVideoView);
|
||||
showFullScreen = true;
|
||||
} else if (!coordinator.isIncomingCall()
|
||||
&& (state == CallViewModel.CallState.RINGING || state == CallViewModel.CallState.CONNECTING)
|
||||
&& localTrack != null
|
||||
&& Boolean.TRUE.equals(videoEnabled)) {
|
||||
remoteVideoView.setMirror(isFront);
|
||||
localTrack.addSink(remoteVideoView);
|
||||
showFullScreen = true;
|
||||
}
|
||||
@@ -729,6 +796,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
&& !isInPictureInPictureMode();
|
||||
|
||||
if (showCorner) {
|
||||
localVideoView.setMirror(isFront);
|
||||
localTrack.addSink(localVideoView);
|
||||
}
|
||||
|
||||
@@ -758,18 +826,46 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
// Permissions
|
||||
|
||||
private boolean hasRequiredPermissions() {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
&& ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
private boolean hasMicrophonePermission() {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestRequiredPermissions() {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
|
||||
PERMISSION_REQUEST_CODE);
|
||||
private boolean hasCameraPermission() {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private boolean shouldRequestCameraPermission() {
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
return coordinator.isStartsWithVideo() && !hasCameraPermission();
|
||||
}
|
||||
|
||||
private void handleMicPermissionDenied() {
|
||||
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
|
||||
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
if (coordinator.hasActiveCall()
|
||||
&& coordinator.isIncomingCall()
|
||||
&& !coordinator.hasOngoingCall()) {
|
||||
coordinator.declineCall();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void proceedAfterPermissions() {
|
||||
if (intentHandled) return;
|
||||
|
||||
if (shouldRequestCameraPermission()) {
|
||||
awaitingPermissionResult = true;
|
||||
ActivityCompat.requestPermissions(
|
||||
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
handleIntents(getIntent());
|
||||
intentHandled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -777,38 +873,34 @@ public class CallActivity extends AppCompatActivity {
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
boolean microphoneGranted = false;
|
||||
boolean cameraGranted = false;
|
||||
awaitingPermissionResult = false;
|
||||
pausedWhileAwaitingPermission = false;
|
||||
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
if (permissions[i].equals(Manifest.permission.RECORD_AUDIO)) {
|
||||
microphoneGranted = (grantResults[i] == PackageManager.PERMISSION_GRANTED);
|
||||
} else if (permissions[i].equals(Manifest.permission.CAMERA)) {
|
||||
cameraGranted = (grantResults[i] == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
}
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
|
||||
if (!microphoneGranted) {
|
||||
Toast.makeText(this, "Microphone permission is required for calls", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
finish();
|
||||
if (requestCode == MIC_PERMISSION_REQUEST_CODE) {
|
||||
boolean micGranted =
|
||||
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (!micGranted) {
|
||||
handleMicPermissionDenied();
|
||||
return;
|
||||
}
|
||||
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
} else if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
boolean cameraGranted =
|
||||
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (!cameraGranted && coordinator.isStartsWithVideo()) {
|
||||
if (!cameraGranted) {
|
||||
Log.w(TAG, "Camera permission denied, switching to audio-only");
|
||||
Toast.makeText(
|
||||
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
coordinator.setStartsWithVideo(false);
|
||||
}
|
||||
|
||||
initializeViewModel();
|
||||
handleIntents(getIntent());
|
||||
}
|
||||
|
||||
proceedAfterPermissions();
|
||||
}
|
||||
|
||||
// Picture-in-Picture
|
||||
@@ -817,6 +909,11 @@ public class CallActivity extends AppCompatActivity {
|
||||
public void onUserLeaveHint() {
|
||||
super.onUserLeaveHint();
|
||||
|
||||
// Do not finish activity when a permission request is pending
|
||||
if (awaitingPermissionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter PiP mode when user presses home button during active call
|
||||
if (viewModel != null) {
|
||||
CallViewModel.CallState state = viewModel.getCallState().getValue();
|
||||
@@ -833,6 +930,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
case INITIALIZING:
|
||||
case PROMPTING_USER_ACCEPT:
|
||||
case ENDED:
|
||||
case ANSWERED_ELSEWHERE:
|
||||
case ERROR:
|
||||
default:
|
||||
finish();
|
||||
@@ -873,17 +971,51 @@ public class CallActivity extends AppCompatActivity {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (awaitingPermissionResult) {
|
||||
pausedWhileAwaitingPermission = true;
|
||||
}
|
||||
|
||||
if (proximityWakeLock != null && proximityWakeLock.isHeld()) {
|
||||
proximityWakeLock.release();
|
||||
Log.d(TAG, "Proximity wake lock released in onDestroy");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Fallback for Android 16 bug: onRequestPermissionsResult not called
|
||||
if (awaitingPermissionResult && pausedWhileAwaitingPermission) {
|
||||
Log.w(TAG, "Permission result callback not received, handling in onResume");
|
||||
|
||||
awaitingPermissionResult = false;
|
||||
pausedWhileAwaitingPermission = false;
|
||||
|
||||
if (!hasMicrophonePermission()) {
|
||||
handleMicPermissionDenied();
|
||||
return;
|
||||
}
|
||||
|
||||
// Mic was granted without callback
|
||||
if (shouldRequestCameraPermission()) {
|
||||
awaitingPermissionResult = true;
|
||||
ActivityCompat.requestPermissions(
|
||||
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
proceedAfterPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
detachAllTracks();
|
||||
if (viewModel != null) {
|
||||
detachAllTracks();
|
||||
}
|
||||
|
||||
// Release video renderers
|
||||
if (localVideoView != null) {
|
||||
|
||||
@@ -95,6 +95,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
private final MutableLiveData<String> displayName = new MutableLiveData<>();
|
||||
private final MutableLiveData<Icon> displayIcon = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> outgoingCallPlaced = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> answeredElsewhere = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> isFrontCamera = new MutableLiveData<>(true);
|
||||
|
||||
// Audio Routing Support
|
||||
private final MediatorLiveData<CallEndpointCompat> currentAudioEndpoint =
|
||||
@@ -317,6 +319,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return outgoingCallPlaced;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getAnsweredElsewhere() {
|
||||
return answeredElsewhere;
|
||||
}
|
||||
|
||||
public LiveData<CallEndpointCompat> getCurrentAudioEndpoint() {
|
||||
return currentAudioEndpoint;
|
||||
}
|
||||
@@ -325,6 +331,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return availableAudioEndpoints;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
// State Update Methods (CallService)
|
||||
|
||||
public void updateConnectionState(PeerConnection.PeerConnectionState state) {
|
||||
@@ -359,6 +369,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
isRelayUsed.postValue(isRelay);
|
||||
}
|
||||
|
||||
public void updateFrontCamera(boolean front) {
|
||||
Log.d(TAG, "updateFrontCamera: " + front);
|
||||
isFrontCamera.postValue(front);
|
||||
}
|
||||
|
||||
public void reportError(String error) {
|
||||
Log.e(TAG, "reportError: " + error);
|
||||
errorMessage.postValue(error);
|
||||
@@ -366,7 +381,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
// Delayed Media Initialization Support
|
||||
|
||||
public void startMediaCapture() {
|
||||
public synchronized void startMediaCapture() {
|
||||
Log.d(TAG, "startMediaCapture");
|
||||
if (callService != null) {
|
||||
callService.startMediaCapture();
|
||||
@@ -375,7 +390,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCallControlScopeAnswer() {
|
||||
public synchronized void handleCallControlScopeAnswer() {
|
||||
Log.d(TAG, "handleCallControlScopeAnswer");
|
||||
|
||||
if (!isIncomingCall) {
|
||||
@@ -512,22 +527,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check microphone and camera permissions
|
||||
if (!hasMicrophonePermission()) {
|
||||
Log.e(TAG, "Microphone permission not granted");
|
||||
Intent intent = new Intent(appContext, CallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
appContext.startActivity(intent);
|
||||
notificationManager.cancel(NOTIFICATION_ID_CALL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWithVideo && !hasCameraPermission()) {
|
||||
Log.w(TAG, "Camera permission not granted");
|
||||
startsWithVideo = false;
|
||||
}
|
||||
|
||||
// Launch CallActivity with answer action
|
||||
// Launch CallActivity
|
||||
Intent intent = new Intent(appContext, CallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
appContext.startActivity(intent);
|
||||
@@ -628,7 +628,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
cleanupCall(activeAccId, activeCallId);
|
||||
}
|
||||
|
||||
public void setAudioEnabled(boolean enabled) {
|
||||
public synchronized void setAudioEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setAudioEnabled: " + enabled);
|
||||
|
||||
localAudioEnabled.postValue(enabled);
|
||||
@@ -640,7 +640,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void setVideoEnabled(boolean enabled) {
|
||||
public synchronized void setVideoEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setVideoEnabled: " + enabled);
|
||||
|
||||
localVideoEnabled.postValue(enabled);
|
||||
@@ -652,14 +652,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
public synchronized void switchCamera() {
|
||||
Log.d(TAG, "switchCamera");
|
||||
if (callService != null) {
|
||||
callService.switchCamera();
|
||||
}
|
||||
}
|
||||
|
||||
public void startOutgoingCall() {
|
||||
public synchronized void startOutgoingCall() {
|
||||
Log.d(TAG, "startOutgoingCall");
|
||||
if (callService != null) {
|
||||
callService.startOutgoingCall();
|
||||
@@ -846,7 +846,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
onIncomingCall(accId, callId, event.getData2Str(), hasVideo);
|
||||
break;
|
||||
case DcContext.DC_EVENT_INCOMING_CALL_ACCEPTED:
|
||||
onIncomingCallAccepted(callId);
|
||||
boolean fromThisDevice = event.getData2Int() != 0; // Data2 is from_this_device
|
||||
onIncomingCallAccepted(callId, fromThisDevice);
|
||||
break;
|
||||
case DcContext.DC_EVENT_OUTGOING_CALL_ACCEPTED:
|
||||
String answerSDP = event.getData2Str();
|
||||
@@ -902,8 +903,13 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
startAndBindService();
|
||||
}
|
||||
|
||||
private synchronized void onIncomingCallAccepted(int callId) {
|
||||
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId);
|
||||
private synchronized void onIncomingCallAccepted(int callId, boolean fromThisDevice) {
|
||||
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId + ", fromThisDevice=" + fromThisDevice);
|
||||
|
||||
if (!fromThisDevice) {
|
||||
onCallAnsweredOnOtherDevice();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeCallId == null || !activeCallId.equals(callId)) {
|
||||
Log.w(TAG, "Accepted call ID doesn't match active call");
|
||||
@@ -918,6 +924,52 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
showOrUpdateOngoingNotification("Call with " + callerName);
|
||||
}
|
||||
|
||||
private synchronized void onCallAnsweredOnOtherDevice() {
|
||||
Log.d(TAG, "Call was answered on another device");
|
||||
|
||||
if (!hasActiveCall()) {
|
||||
Log.d(TAG, "No active call, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent notifyBackendCallEnded() from firing during WebRTC teardown.
|
||||
// The call is still active on the other device.
|
||||
hasNotifiedBackend = true;
|
||||
|
||||
if (callService != null) {
|
||||
callService.stopRingtone();
|
||||
}
|
||||
|
||||
notificationManager.cancel(NOTIFICATION_ID_CALL);
|
||||
|
||||
answeredElsewhere.postValue(true);
|
||||
|
||||
// Disconnect from Telecom CallControlScope
|
||||
CallControlScope scope = activeCallControlScope;
|
||||
if (scope != null) {
|
||||
scope.disconnect(
|
||||
new DisconnectCause(DisconnectCause.REMOTE),
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
Log.d(TAG, "Disconnect (answered elsewhere) completed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (callService != null) {
|
||||
callService.endCall();
|
||||
}
|
||||
|
||||
cleanupCall(activeAccId, activeCallId);
|
||||
}
|
||||
|
||||
private void onOutgoingCallAccepted(int callId, String answerSdp) {
|
||||
Log.d(TAG, "onOutgoingCallAccepted: callId=" + callId + ", got answer SDP");
|
||||
|
||||
@@ -1125,6 +1177,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
private void resetLiveDataForNewCall() {
|
||||
connectionState.postValue(PeerConnection.PeerConnectionState.NEW);
|
||||
answeredElsewhere.postValue(false); // clearLiveData() must not reset answeredElsewhere
|
||||
clearLiveData();
|
||||
}
|
||||
|
||||
@@ -1150,7 +1203,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check camera and microphone permissions
|
||||
// Check microphone permission
|
||||
if (!hasMicrophonePermission()) {
|
||||
Log.e(TAG, "Microphone permission not granted");
|
||||
|
||||
@@ -1160,11 +1213,6 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWithVideo && !hasCameraPermission()) {
|
||||
Log.w(TAG, "Camera permission not granted, will start audio-only");
|
||||
startsWithVideo = false;
|
||||
}
|
||||
|
||||
resetLiveDataForNewCall();
|
||||
|
||||
this.activeCallId = -1; // Placeholder call ID for Intent
|
||||
@@ -1310,10 +1358,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
|
||||
// Check full screen intent permission on Android 14+
|
||||
boolean canUseFullScreen = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
canUseFullScreen = canUseFullScreenIntent();
|
||||
if (!canUseFullScreen) {
|
||||
if (!canUseFullScreenIntent()) {
|
||||
Log.w(TAG, "Full screen intent permission not granted, notification will appear normally");
|
||||
}
|
||||
}
|
||||
@@ -1354,7 +1400,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Notification.Builder builder;
|
||||
if (canUseFullScreen && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Android 12+, CallStyle
|
||||
Person caller =
|
||||
new Person.Builder().setName(callerName).setIcon(callerIcon).setImportant(true).build();
|
||||
|
||||
@@ -142,17 +142,13 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
boolean hasMicrophone = callCoordinator.hasMicrophonePermission();
|
||||
boolean hasCamera = callCoordinator.hasCameraPermission();
|
||||
|
||||
if (!hasMicrophone) {
|
||||
if (!callCoordinator.hasMicrophonePermission()) {
|
||||
Log.e(TAG, "Microphone permission not granted, cannot start call");
|
||||
callCoordinator.reportError("Microphone permission is required for calls");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean startsWithVideo = callCoordinator.isStartsWithVideo() && hasCamera;
|
||||
boolean startsWithVideo = callCoordinator.isStartsWithVideo();
|
||||
|
||||
Log.d(TAG, "Creating media stream with video: " + startsWithVideo);
|
||||
|
||||
@@ -164,14 +160,18 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
|
||||
webRTCClient.setLocalMediaStream(stream);
|
||||
|
||||
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
|
||||
|
||||
callCoordinator.setVideoEnabled(startsWithVideo);
|
||||
|
||||
if (!stream.videoTracks.isEmpty()) {
|
||||
VideoTrack localTrack = stream.videoTracks.get(0);
|
||||
callCoordinator.updateLocalVideoTrack(localTrack);
|
||||
} else {
|
||||
Log.w(TAG, "Camera unavailable, call will be audio-only");
|
||||
callCoordinator.reportError("Camera unavailable, using audio only");
|
||||
Log.w(TAG, "No video track in stream, call will be audio-only");
|
||||
if (startsWithVideo) {
|
||||
callCoordinator.reportError("Camera unavailable, using audio only");
|
||||
}
|
||||
callCoordinator.setVideoEnabled(false);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,9 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Failed to setup media: " + error);
|
||||
callCoordinator.reportError("Camera/microphone error: " + error);
|
||||
if (startsWithVideo) {
|
||||
callCoordinator.reportError("Camera/microphone error: " + error);
|
||||
}
|
||||
callCoordinator.setVideoEnabled(false);
|
||||
}
|
||||
});
|
||||
@@ -410,7 +412,18 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
Log.d(TAG, "switchCamera");
|
||||
|
||||
if (mediaStreamManager != null) {
|
||||
mediaStreamManager.switchCamera();
|
||||
mediaStreamManager.switchCamera(
|
||||
new MediaStreamManager.CameraSwitchCallback() {
|
||||
@Override
|
||||
public void onCameraSwitch(boolean isFrontCamera) {
|
||||
callCoordinator.updateFrontCamera(isFrontCamera);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Camera switch failed: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
@@ -36,8 +37,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
private final LiveData<String> displayName;
|
||||
private final LiveData<Icon> displayIcon;
|
||||
private final LiveData<Boolean> outgoingCallPlaced;
|
||||
private final LiveData<Boolean> answeredElsewhere;
|
||||
private final LiveData<CallEndpointCompat> currentAudioEndpoint;
|
||||
private final LiveData<List<CallEndpointCompat>> availableAudioEndpoints;
|
||||
private final LiveData<Boolean> isFrontCamera;
|
||||
|
||||
// Translated from coordinator's connectionState
|
||||
private final MediatorLiveData<CallState> callState;
|
||||
@@ -46,7 +49,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
private Observer<VideoTrack> answerCallObserver;
|
||||
private Observer<VideoTrack> startOutgoingCallObserver;
|
||||
|
||||
private boolean hasCallEnded = false;
|
||||
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
|
||||
|
||||
// User-facing call states
|
||||
public enum CallState {
|
||||
@@ -56,6 +59,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING,
|
||||
ANSWERED_ELSEWHERE,
|
||||
ENDED,
|
||||
ERROR
|
||||
}
|
||||
@@ -77,8 +81,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
this.displayName = callCoordinator.getDisplayName();
|
||||
this.displayIcon = callCoordinator.getDisplayIcon();
|
||||
this.outgoingCallPlaced = callCoordinator.getOutgoingCallPlaced();
|
||||
this.answeredElsewhere = callCoordinator.getAnsweredElsewhere();
|
||||
this.currentAudioEndpoint = callCoordinator.getCurrentAudioEndpoint();
|
||||
this.availableAudioEndpoints = callCoordinator.getAvailableAudioEndpoints();
|
||||
this.isFrontCamera = callCoordinator.getIsFrontCamera();
|
||||
|
||||
this.callState = new MediatorLiveData<>(CallState.INITIALIZING);
|
||||
|
||||
@@ -105,6 +111,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
callState.addSource(
|
||||
callCoordinator.getConnectionState(),
|
||||
state -> {
|
||||
if (callState.getValue() == CallState.ANSWERED_ELSEWHERE) {
|
||||
return;
|
||||
}
|
||||
|
||||
CallState newState = translateConnectionState(state);
|
||||
|
||||
if (callState.getValue() != newState) {
|
||||
@@ -113,9 +123,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
|
||||
if (state == PeerConnection.PeerConnectionState.FAILED
|
||||
|| state == PeerConnection.PeerConnectionState.CLOSED) {
|
||||
if (!hasCallEnded) {
|
||||
hasCallEnded = true;
|
||||
}
|
||||
hasCallEnded.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,6 +136,15 @@ public class CallViewModel extends AndroidViewModel {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
callState.addSource(
|
||||
answeredElsewhere,
|
||||
value -> {
|
||||
if (Boolean.TRUE.equals(value)) {
|
||||
hasCallEnded.set(true);
|
||||
callState.setValue(CallState.ANSWERED_ELSEWHERE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CallState translateConnectionState(PeerConnection.PeerConnectionState state) {
|
||||
@@ -257,11 +274,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void declineCall() {
|
||||
Log.d(TAG, "declineCall");
|
||||
|
||||
if (hasCallEnded) {
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
hasCallEnded = true;
|
||||
|
||||
callCoordinator.declineCall();
|
||||
}
|
||||
@@ -269,11 +285,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void hangUp() {
|
||||
Log.d(TAG, "hangUp");
|
||||
|
||||
if (hasCallEnded) {
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
hasCallEnded = true;
|
||||
|
||||
callCoordinator.hangUp();
|
||||
}
|
||||
@@ -354,8 +369,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void onCallDisconnected(DisconnectCause disconnectCause) {
|
||||
Log.d(TAG, "onCallDisconnected callback from CallControlScope, cause: " + disconnectCause);
|
||||
|
||||
if (!hasCallEnded) {
|
||||
hasCallEnded = true;
|
||||
if (hasCallEnded.compareAndSet(false, true)) {
|
||||
callState.postValue(CallState.ENDED);
|
||||
}
|
||||
}
|
||||
@@ -414,6 +428,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
return availableAudioEndpoints;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
// Notification Action Handlers
|
||||
|
||||
public void handleNotificationAnswer() {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import org.thoughtcrime.securesms.EglUtils;
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
@@ -31,6 +36,7 @@ public class MediaStreamManager {
|
||||
private VideoSource videoSource;
|
||||
private AudioSource audioSource;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private volatile boolean isFrontCamera = true;
|
||||
|
||||
public interface Callback {
|
||||
void onMediaStreamReady(MediaStream stream);
|
||||
@@ -38,6 +44,12 @@ public class MediaStreamManager {
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public interface CameraSwitchCallback {
|
||||
void onCameraSwitch(boolean isFrontCamera);
|
||||
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public MediaStreamManager(@NonNull Context context, PeerConnectionFactory peerConnectionFactory) {
|
||||
this.context = context.getApplicationContext();
|
||||
|
||||
@@ -45,6 +57,7 @@ public class MediaStreamManager {
|
||||
}
|
||||
|
||||
/** Create media stream with audio and optionally video */
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void createMediaStream(Callback callback) {
|
||||
try {
|
||||
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
|
||||
@@ -83,6 +96,12 @@ public class MediaStreamManager {
|
||||
|
||||
@Nullable
|
||||
private VideoCapturer createVideoCapturer() {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "Camera permission not granted");
|
||||
return null;
|
||||
}
|
||||
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
|
||||
// Try front camera first
|
||||
@@ -91,6 +110,7 @@ public class MediaStreamManager {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = true;
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +120,7 @@ public class MediaStreamManager {
|
||||
for (String deviceName : deviceNames) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = enumerator.isFrontFacing(deviceName);
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -107,12 +128,61 @@ public class MediaStreamManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
if (videoCapturer instanceof CameraVideoCapturer) {
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
cameraVideoCapturer.switchCamera(null);
|
||||
Log.d(TAG, "Camera switched");
|
||||
public void switchCamera(@Nullable CameraSwitchCallback callback) {
|
||||
if (!(videoCapturer instanceof CameraVideoCapturer)) {
|
||||
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
|
||||
return;
|
||||
}
|
||||
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
|
||||
// Find the opposite-facing camera
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
String targetCameraName = null;
|
||||
for (String deviceName : deviceNames) {
|
||||
boolean isTargetFront = !isFrontCamera;
|
||||
boolean deviceIsFront = enumerator.isFrontFacing(deviceName);
|
||||
|
||||
if (deviceIsFront == isTargetFront) {
|
||||
targetCameraName = deviceName;
|
||||
break; // Take the first match
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCameraName == null) {
|
||||
Log.e(TAG, "No camera found with opposite facing direction");
|
||||
if (callback != null) {
|
||||
callback.onError("No opposite camera available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalTargetCameraName = targetCameraName;
|
||||
Log.d(TAG, "Switching to camera: " + finalTargetCameraName);
|
||||
|
||||
// Call with explicit camera name
|
||||
cameraVideoCapturer.switchCamera(
|
||||
new CameraVideoCapturer.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFront) {
|
||||
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
|
||||
isFrontCamera = isFront;
|
||||
if (callback != null) callback.onCameraSwitch(isFront);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSwitchError(String errorDescription) {
|
||||
Log.e(TAG, "switchCamera FAILED: " + errorDescription);
|
||||
if (callback != null) callback.onError(errorDescription);
|
||||
}
|
||||
},
|
||||
finalTargetCameraName);
|
||||
}
|
||||
|
||||
public boolean isFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
/** Cleanup resources */
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.loader.app.LoaderManager;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class AttachmentTypeSelector extends PopupWindow {
|
||||
@@ -45,7 +46,7 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
private final @NonNull ImageView imageButton;
|
||||
private final @NonNull ImageView documentButton;
|
||||
private final @NonNull ImageView contactButton;
|
||||
// private final @NonNull ImageView cameraButton;
|
||||
// private final @NonNull ImageView cameraButton;
|
||||
private final @NonNull ImageView videoButton;
|
||||
private final @NonNull ImageView locationButton;
|
||||
private final @NonNull ImageView webxdcButton;
|
||||
@@ -87,11 +88,9 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
this.webxdcButton.setOnClickListener(new PropagatingClickListener(ADD_WEBXDC));
|
||||
this.recentRail.setListener(new RecentPhotoSelectedListener());
|
||||
|
||||
// disable location streaming button for now
|
||||
// if (!Prefs.isLocationStreamingEnabled(context)) {
|
||||
this.locationButton.setVisibility(View.GONE);
|
||||
ViewUtil.findById(layout, R.id.location_button_label).setVisibility(View.GONE);
|
||||
// }
|
||||
if (!Prefs.isLocationStreamingEnabled(context)) {
|
||||
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setLocationButtonImage(context);
|
||||
|
||||
|
||||
@@ -145,8 +145,6 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(91, context.getString(R.string.devicemsg_self_deleted));
|
||||
dcContext.setStockTranslation(97, context.getString(R.string.forwarded));
|
||||
dcContext.setStockTranslation(98, context.getString(R.string.devicemsg_storage_exceeding));
|
||||
dcContext.setStockTranslation(99, context.getString(R.string.n_bytes_message));
|
||||
dcContext.setStockTranslation(100, context.getString(R.string.download_max_available_until));
|
||||
dcContext.setStockTranslation(103, context.getString(R.string.incoming_messages));
|
||||
dcContext.setStockTranslation(104, context.getString(R.string.outgoing_messages));
|
||||
dcContext.setStockTranslation(107, context.getString(R.string.connectivity_connected));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.connect;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
@@ -11,6 +10,7 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
@@ -119,7 +119,7 @@ public class KeepAliveService extends Service {
|
||||
|
||||
private static boolean ch_created = false;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private static void createFgNotificationChannel(Context context) {
|
||||
if (!ch_created) {
|
||||
ch_created = true;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ActiveLocationChats {
|
||||
|
||||
private static final String PREFS_NAME = "location_streaming";
|
||||
private static final String KEY_ACTIVE = "active_chat_ids";
|
||||
|
||||
private ActiveLocationChats() {}
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a chat. Uses commit() to guarantee the write reaches disk before the process can die to
|
||||
* preserve the superset invariant.
|
||||
*/
|
||||
static void add(Context context, int chatId) {
|
||||
Set<String> current = new HashSet<>(getAll(context));
|
||||
current.add(String.valueOf(chatId));
|
||||
prefs(context).edit().putStringSet(KEY_ACTIVE, current).commit();
|
||||
}
|
||||
|
||||
public static void remove(Context context, int chatId) {
|
||||
Set<String> current = new HashSet<>(getAll(context));
|
||||
current.remove(String.valueOf(chatId));
|
||||
prefs(context).edit().putStringSet(KEY_ACTIVE, current).apply();
|
||||
}
|
||||
|
||||
static void clear(Context context) {
|
||||
prefs(context).edit().remove(KEY_ACTIVE).apply();
|
||||
}
|
||||
|
||||
static Set<Integer> getAllIds(Context context) {
|
||||
Set<Integer> ids = new HashSet<>();
|
||||
for (String s : getAll(context)) {
|
||||
try {
|
||||
ids.add(Integer.parseInt(s));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static Set<String> getAll(Context context) {
|
||||
return prefs(context).getStringSet(KEY_ACTIVE, new HashSet<>());
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
import java.util.Observable;
|
||||
|
||||
public class DcLocation extends Observable {
|
||||
private static final String TAG = DcLocation.class.getSimpleName();
|
||||
private Location lastLocation;
|
||||
private static DcLocation instance;
|
||||
private static final int TIMEOUT = 1000 * 15;
|
||||
private static final int EARTH_RADIUS = 6371;
|
||||
|
||||
private DcLocation() {
|
||||
lastLocation = getDefault();
|
||||
}
|
||||
|
||||
public static DcLocation getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new DcLocation();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Location getLastLocation() {
|
||||
return lastLocation;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return !"?".equals(lastLocation.getProvider());
|
||||
}
|
||||
|
||||
void updateLocation(Location location) {
|
||||
if (isBetterLocation(location, lastLocation)) {
|
||||
lastLocation = location;
|
||||
|
||||
instance.setChanged();
|
||||
instance.notifyObservers();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
updateLocation(getDefault());
|
||||
}
|
||||
|
||||
private Location getDefault() {
|
||||
return new Location("?");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.android.com/guide/topics/location/strategies Determines whether one Location
|
||||
* reading is better than the current Location fix
|
||||
*
|
||||
* @param location The new Location that you want to evaluate
|
||||
* @param currentBestLocation The current Location fix, to which you want to compare the new one
|
||||
*/
|
||||
private boolean isBetterLocation(Location location, Location currentBestLocation) {
|
||||
if (currentBestLocation == null) {
|
||||
// A new location is always better than no location
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check whether the new location fix is newer or older
|
||||
long timeDelta = location.getTime() - currentBestLocation.getTime();
|
||||
boolean isSignificantlyOlder = timeDelta < -TIMEOUT;
|
||||
|
||||
if (isSignificantlyOlder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whether the new location fix is more or less accurate
|
||||
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
||||
Log.d(TAG, "accuracyDelta: " + accuracyDelta);
|
||||
boolean isSignificantlyMoreAccurate = accuracyDelta > 50;
|
||||
boolean isSameProvider =
|
||||
isSameProvider(location.getProvider(), currentBestLocation.getProvider());
|
||||
|
||||
if (isSignificantlyMoreAccurate && isSameProvider) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isMoreAccurate = accuracyDelta > 0;
|
||||
double distance = distance(location, currentBestLocation);
|
||||
return hasLocationChanged(distance) && isMoreAccurate
|
||||
|| hasLocationSignificantlyChanged(distance);
|
||||
}
|
||||
|
||||
private boolean hasLocationSignificantlyChanged(double distance) {
|
||||
return distance > 30D;
|
||||
}
|
||||
|
||||
private boolean hasLocationChanged(double distance) {
|
||||
return distance > 10D;
|
||||
}
|
||||
|
||||
private double distance(Location location, Location currentBestLocation) {
|
||||
|
||||
double startLat = location.getLatitude();
|
||||
double startLong = location.getLongitude();
|
||||
double endLat = currentBestLocation.getLatitude();
|
||||
double endLong = currentBestLocation.getLongitude();
|
||||
|
||||
double dLat = Math.toRadians(endLat - startLat);
|
||||
double dLong = Math.toRadians(endLong - startLong);
|
||||
|
||||
startLat = Math.toRadians(startLat);
|
||||
endLat = Math.toRadians(endLat);
|
||||
|
||||
double a = haversin(dLat) + Math.cos(startLat) * Math.cos(endLat) * haversin(dLong);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
double distance = EARTH_RADIUS * c * 1000;
|
||||
Log.d(TAG, "Distance between location updates: " + distance);
|
||||
return distance;
|
||||
}
|
||||
|
||||
private double haversin(double val) {
|
||||
return Math.pow(Math.sin(val / 2), 2);
|
||||
}
|
||||
|
||||
/** Checks whether two providers are the same */
|
||||
private boolean isSameProvider(String provider1, String provider2) {
|
||||
if (provider1 == null) {
|
||||
return provider2 == null;
|
||||
}
|
||||
return provider1.equals(provider2);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import static android.content.Context.BIND_AUTO_CREATE;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.location.Location;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
public class DcLocationManager implements Observer {
|
||||
|
||||
private static final String TAG = DcLocationManager.class.getSimpleName();
|
||||
private LocationBackgroundService.LocationBackgroundServiceBinder serviceBinder;
|
||||
private final Context context;
|
||||
private DcLocation dcLocation = DcLocation.getInstance();
|
||||
private final LinkedList<Integer> pendingShareLastLocation = new LinkedList<>();
|
||||
private final ServiceConnection serviceConnection =
|
||||
new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.d(TAG, "background service connected");
|
||||
serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service;
|
||||
while (!pendingShareLastLocation.isEmpty()) {
|
||||
shareLastLocation(pendingShareLastLocation.pop());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "background service disconnected");
|
||||
serviceBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
public DcLocationManager(Context context, DcContext dcContext) {
|
||||
this.context = context.getApplicationContext();
|
||||
DcLocation.getInstance().addObserver(this);
|
||||
if (dcContext.isSendingLocationsToChat(0)) {
|
||||
startLocationEngine();
|
||||
}
|
||||
}
|
||||
|
||||
public void startLocationEngine() {
|
||||
if (serviceBinder == null) {
|
||||
Intent intent = new Intent(context.getApplicationContext(), LocationBackgroundService.class);
|
||||
context.bindService(intent, serviceConnection, BIND_AUTO_CREATE);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopLocationEngine() {
|
||||
if (serviceBinder == null) {
|
||||
return;
|
||||
}
|
||||
context.unbindService(serviceConnection);
|
||||
serviceBinder.stop();
|
||||
serviceBinder = null;
|
||||
}
|
||||
|
||||
public void stopSharingLocation(int chatId) {
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
|
||||
if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) {
|
||||
stopLocationEngine();
|
||||
}
|
||||
}
|
||||
|
||||
public void shareLocation(int duration, int chatId) {
|
||||
startLocationEngine();
|
||||
Log.d(TAG, String.format("Share location in chat %d for %d seconds", chatId, duration));
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, duration);
|
||||
if (dcLocation.isValid()) {
|
||||
writeDcLocationUpdateMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public void shareLastLocation(int chatId) {
|
||||
if (serviceBinder == null) {
|
||||
pendingShareLastLocation.push(chatId);
|
||||
startLocationEngine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dcLocation.isValid()) {
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 1);
|
||||
writeDcLocationUpdateMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Observable o, Object arg) {
|
||||
if (o instanceof DcLocation) {
|
||||
dcLocation = (DcLocation) o;
|
||||
if (dcLocation.isValid()) {
|
||||
writeDcLocationUpdateMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDcLocationUpdateMessage() {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Share location: "
|
||||
+ dcLocation.getLastLocation().getLatitude()
|
||||
+ ", "
|
||||
+ dcLocation.getLastLocation().getLongitude());
|
||||
Location lastLocation = dcLocation.getLastLocation();
|
||||
|
||||
boolean continueLocationStreaming =
|
||||
DcHelper.getContext(context)
|
||||
.setLocation(
|
||||
(float) lastLocation.getLatitude(),
|
||||
(float) lastLocation.getLongitude(),
|
||||
lastLocation.getAccuracy());
|
||||
if (!continueLocationStreaming) {
|
||||
stopLocationEngine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class LocationBackgroundService extends Service {
|
||||
|
||||
private static final int INITIAL_TIMEOUT = 1000 * 60 * 2;
|
||||
private static final String TAG = LocationBackgroundService.class.getSimpleName();
|
||||
private LocationManager locationManager = null;
|
||||
private static final int LOCATION_INTERVAL = 1000;
|
||||
private static final float LOCATION_DISTANCE = 25F;
|
||||
ServiceLocationListener locationListener;
|
||||
|
||||
private final IBinder mBinder = new LocationBackgroundServiceBinder();
|
||||
|
||||
@Override
|
||||
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
|
||||
return super.bindService(service, conn, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
locationManager =
|
||||
(LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationManager == null) {
|
||||
Log.e(TAG, "Unable to initialize location service");
|
||||
return;
|
||||
}
|
||||
|
||||
locationListener = new ServiceLocationListener();
|
||||
Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
if (lastLocation != null) {
|
||||
long locationAge = System.currentTimeMillis() - lastLocation.getTime();
|
||||
if (locationAge <= 600 * 1000) { // not older than 10 minutes
|
||||
DcLocation.getInstance().updateLocation(lastLocation);
|
||||
}
|
||||
}
|
||||
// requestLocationUpdate(LocationManager.NETWORK_PROVIDER);
|
||||
requestLocationUpdate(LocationManager.GPS_PROVIDER);
|
||||
initialLocationUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
super.onStartCommand(intent, flags, startId);
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (locationManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
} catch (Exception ex) {
|
||||
Log.i(TAG, "fail to remove location listeners, ignore", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestLocationUpdate(String provider) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
provider, LOCATION_INTERVAL, LOCATION_DISTANCE, locationListener);
|
||||
} catch (SecurityException | IllegalArgumentException ex) {
|
||||
Log.e(
|
||||
TAG,
|
||||
String.format("Unable to request %s provider based location updates.", provider),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initialLocationUpdate() {
|
||||
try {
|
||||
Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
if (gpsLocation != null
|
||||
&& System.currentTimeMillis() - gpsLocation.getTime() < INITIAL_TIMEOUT) {
|
||||
locationListener.onLocationChanged(gpsLocation);
|
||||
}
|
||||
|
||||
} catch (NullPointerException | SecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
class LocationBackgroundServiceBinder extends Binder {
|
||||
LocationBackgroundServiceBinder getService() {
|
||||
return LocationBackgroundServiceBinder.this;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
DcLocation.getInstance().reset();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private class ServiceLocationListener implements LocationListener {
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(@NonNull Location location) {
|
||||
Log.d(TAG, "onLocationChanged: " + location);
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
DcLocation.getInstance().updateLocation(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(@NonNull String provider) {
|
||||
Log.e(TAG, "onProviderDisabled: " + provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(@NonNull String provider) {
|
||||
Log.e(TAG, "onProviderEnabled: " + provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
Log.e(TAG, "onStatusChanged: " + provider + " status: " + status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.location.Location;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
/**
|
||||
* Process-wide holder for the current streamed location. Foreground service writes, UI observes.
|
||||
*/
|
||||
public final class LocationData {
|
||||
|
||||
private static final LocationData INSTANCE = new LocationData();
|
||||
|
||||
private final MutableLiveData<Location> liveLocation = new MutableLiveData<>();
|
||||
|
||||
private LocationData() {}
|
||||
|
||||
public static LocationData getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public LiveData<Location> observable() {
|
||||
return liveLocation;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Location current() {
|
||||
return liveLocation.getValue();
|
||||
}
|
||||
|
||||
void post(@NonNull Location location) {
|
||||
liveLocation.postValue(location);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
liveLocation.postValue(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/** Abstraction over platform LocationManager and GMS FusedLocationProviderClient. */
|
||||
public interface LocationSource {
|
||||
|
||||
void startUpdates(@NonNull Context context, @NonNull Callback callback);
|
||||
|
||||
void stopUpdates();
|
||||
|
||||
interface Callback {
|
||||
void onLocationUpdate(@NonNull Location location);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
public class LocationStreamingService extends Service {
|
||||
|
||||
private static final String TAG = LocationStreamingService.class.getSimpleName();
|
||||
private static final String ACTION_STOP = "org.thoughtcrime.securesms.geolocation.STOP_STREAMING";
|
||||
private static final int NOTIFICATION_ID = 8801;
|
||||
private static final String CHANNEL_ID = "location_streaming";
|
||||
|
||||
private static volatile boolean running = false;
|
||||
|
||||
private LocationSource source;
|
||||
private Location lastPublished;
|
||||
|
||||
// static API
|
||||
|
||||
/** Register a chat for location updates, then ensure the service is running. */
|
||||
public static void startSharing(Context context, int chatId, int durationSeconds) {
|
||||
ActiveLocationChats.add(context, chatId);
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, durationSeconds);
|
||||
ContextCompat.startForegroundService(
|
||||
context, new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
|
||||
/** Unregister a chat. If no chats remain, stop the service. */
|
||||
public static void stopSharing(Context context, int chatId) {
|
||||
ActiveLocationChats.remove(context, chatId);
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
|
||||
if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) {
|
||||
context.stopService(new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureRunning(Context context) {
|
||||
if (!hasLocationPermission(context)) {
|
||||
for (int chatId : ActiveLocationChats.getAllIds(context)) {
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
|
||||
}
|
||||
ActiveLocationChats.clear(context);
|
||||
return;
|
||||
}
|
||||
ContextCompat.startForegroundService(
|
||||
context, new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if (!hasLocationPermission(this)) {
|
||||
Log.w(TAG, "Location permission not granted, stopping");
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
promoteToForeground();
|
||||
beginLocationUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||
stopAllSharing();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// If the service is already running, and we already have a fix,
|
||||
// push it immediately.
|
||||
if (lastPublished != null) {
|
||||
publishAndWrite(lastPublished);
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void stopAllSharing() {
|
||||
for (int chatId : ActiveLocationChats.getAllIds(this)) {
|
||||
DcHelper.getContext(this).sendLocationsToChat(chatId, 0);
|
||||
}
|
||||
ActiveLocationChats.clear(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
running = false;
|
||||
if (source != null) {
|
||||
source.stopUpdates();
|
||||
source = null;
|
||||
}
|
||||
LocationData.getInstance().clear();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(int startId) {
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(int startId, int fgsType) {
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
// location
|
||||
|
||||
private void beginLocationUpdates() {
|
||||
source = LocationSourceFactory.create(this);
|
||||
source.startUpdates(this, this::onNewLocation);
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.d(TAG, "onNewLocation raw: " + location);
|
||||
publishAndWrite(location);
|
||||
lastPublished = location;
|
||||
}
|
||||
|
||||
private void publishAndWrite(Location location) {
|
||||
LocationData.getInstance().post(location);
|
||||
|
||||
boolean keepGoing =
|
||||
DcHelper.getContext(this)
|
||||
.setLocation(
|
||||
(float) location.getLatitude(),
|
||||
(float) location.getLongitude(),
|
||||
location.getAccuracy());
|
||||
Log.d(TAG, "keepGoing: " + keepGoing);
|
||||
|
||||
if (!keepGoing) {
|
||||
stopAllSharing();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
// foreground / notification
|
||||
|
||||
private void promoteToForeground() {
|
||||
ensureNotificationChannel();
|
||||
Notification notification = buildNotification();
|
||||
try {
|
||||
int type =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
: 0;
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, type);
|
||||
} catch (Exception e) {
|
||||
// SecurityException on API 34+ if permission missing,
|
||||
// ForegroundServiceStartNotAllowedException on API 31+ if in background.
|
||||
Log.e(TAG, "Cannot promote to foreground", e);
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.location_streaming_notification_title),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(getString(R.string.location_streaming_channel_desc));
|
||||
channel.setShowBadge(false);
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm != null) nm.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
Intent tapIntent = new Intent(this, ConversationListActivity.class);
|
||||
PendingIntent contentPendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this, 0, tapIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent stopIntent = new Intent(this, LocationStreamingService.class);
|
||||
stopIntent.setAction(ACTION_STOP);
|
||||
PendingIntent stopPendingIntent =
|
||||
PendingIntent.getService(this, 1, stopIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.location_streaming_notification_title))
|
||||
.setContentText(getString(R.string.location_streaming_notification_text))
|
||||
.setSmallIcon(R.drawable.ic_location_on_white_24dp)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.addAction(
|
||||
R.drawable.ic_stop_circle, getString(R.string.stop_sharing_location), stopPendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean hasLocationPermission(Context context) {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context, android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(
|
||||
context, android.Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.location.LocationListenerCompat;
|
||||
import androidx.core.location.LocationManagerCompat;
|
||||
import androidx.core.location.LocationRequestCompat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class PlatformLocationSource implements LocationSource {
|
||||
|
||||
private static final String TAG = PlatformLocationSource.class.getSimpleName();
|
||||
private static final long UPDATE_INTERVAL_MS = 0;
|
||||
|
||||
private LocationManager locationManager;
|
||||
private final List<LocationListenerCompat> activeListeners = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void startUpdates(@NonNull Context context, @NonNull Callback callback) {
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationManager == null) {
|
||||
Log.e(TAG, "LocationManager unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean registered = false;
|
||||
|
||||
// API 31+: try the platform fused provider first
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
registered = requestProvider(context, LocationManager.FUSED_PROVIDER, callback);
|
||||
}
|
||||
|
||||
// Fall back (or complement) with individual providers
|
||||
if (!registered) {
|
||||
requestProvider(context, LocationManager.GPS_PROVIDER, callback);
|
||||
requestProvider(context, LocationManager.NETWORK_PROVIDER, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean requestProvider(Context context, String provider, Callback callback) {
|
||||
if (locationManager == null) return false;
|
||||
|
||||
if (!locationManager.isProviderEnabled(provider)) {
|
||||
Log.d(TAG, "Provider " + provider + " not enabled, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LocationRequestCompat request =
|
||||
new LocationRequestCompat.Builder(UPDATE_INTERVAL_MS)
|
||||
.setMinUpdateDistanceMeters(0)
|
||||
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
|
||||
.build();
|
||||
|
||||
LocationListenerCompat listener = callback::onLocationUpdate;
|
||||
Executor mainExecutor = ContextCompat.getMainExecutor(context);
|
||||
|
||||
LocationManagerCompat.requestLocationUpdates(
|
||||
locationManager, provider, request, mainExecutor, listener);
|
||||
activeListeners.add(listener);
|
||||
Log.d(TAG, "Registered on provider: " + provider);
|
||||
return true;
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.e(TAG, "Cannot request " + provider + " updates", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopUpdates() {
|
||||
if (locationManager != null) {
|
||||
for (LocationListenerCompat listener : activeListeners) {
|
||||
try {
|
||||
locationManager.removeUpdates(listener);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error removing listener", e);
|
||||
}
|
||||
}
|
||||
activeListeners.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareLocationDialog;
|
||||
@@ -64,7 +63,8 @@ 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;
|
||||
import org.thoughtcrime.securesms.geolocation.ActiveLocationChats;
|
||||
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
@@ -490,47 +490,36 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public static void selectLocation(Activity activity, int chatId) {
|
||||
ApplicationContext applicationContext = ApplicationContext.getInstance(activity);
|
||||
DcLocationManager dcLocationManager = applicationContext.getLocationManager();
|
||||
Context appContext = activity.getApplicationContext();
|
||||
|
||||
if (DcHelper.getContext(applicationContext).isSendingLocationsToChat(chatId)) {
|
||||
dcLocationManager.stopSharingLocation(chatId);
|
||||
return;
|
||||
if (DcHelper.getContext(appContext).isSendingLocationsToChat(chatId)) {
|
||||
if (LocationStreamingService.isRunning()) {
|
||||
LocationStreamingService.stopSharing(appContext, chatId);
|
||||
return;
|
||||
}
|
||||
// Stale — service is dead but chat layer still thinks it's sharing.
|
||||
// Clean up this chat and fall through to the fresh start flow.
|
||||
ActiveLocationChats.remove(appContext, chatId);
|
||||
DcHelper.getContext(appContext).sendLocationsToChat(chatId, 0);
|
||||
}
|
||||
|
||||
// see
|
||||
// https://support.google.com/googleplay/android-developer/answer/9799150#zippy=%2Cstep-provide-prominent-in-app-disclosure
|
||||
// for rationale dialog requirements
|
||||
Permissions.PermissionsBuilder permissionsBuilder =
|
||||
Permissions.with(activity)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(
|
||||
"To share your live location with chat members, allow ArcaneChat to use your location data.\n\nTo make live location work gaplessly, location data is used even when the app is closed or not in use.",
|
||||
R.drawable.ic_location_on_white_24dp)
|
||||
.withPermanentDenialDialog(
|
||||
activity.getString(R.string.perm_explain_access_to_location_denied))
|
||||
.onAllGranted(
|
||||
() -> {
|
||||
ShareLocationDialog.show(
|
||||
activity,
|
||||
durationInSeconds -> {
|
||||
if (durationInSeconds == 1) {
|
||||
dcLocationManager.shareLastLocation(chatId);
|
||||
} else {
|
||||
dcLocationManager.shareLocation(durationInSeconds, chatId);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
permissionsBuilder.request(
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
} else {
|
||||
permissionsBuilder.request(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
}
|
||||
permissionsBuilder.execute();
|
||||
Permissions.with(activity)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(
|
||||
activity.getString(R.string.location_rationale), R.drawable.ic_location_on_white_24dp)
|
||||
.withPermanentDenialDialog(
|
||||
activity.getString(R.string.perm_explain_access_to_location_denied))
|
||||
.onAllGranted(
|
||||
() -> {
|
||||
ShareLocationDialog.show(
|
||||
activity,
|
||||
durationInSeconds ->
|
||||
LocationStreamingService.startSharing(appContext, chatId, durationInSeconds));
|
||||
})
|
||||
.request(
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
android.Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private @Nullable Uri getSlideUri() {
|
||||
|
||||
@@ -208,7 +208,6 @@ public class PersistentBlobProvider {
|
||||
return getFile(context, ContentUris.parseId(uri)).delete();
|
||||
}
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (isExternalBlobUri(context, uri)) {
|
||||
return new File(uri.getPath()).delete();
|
||||
}
|
||||
@@ -287,6 +286,20 @@ public class PersistentBlobProvider {
|
||||
|
||||
private static @NonNull File getExternalDir(Context context) throws IOException {
|
||||
File externalDir = context.getExternalCacheDir();
|
||||
|
||||
if (externalDir != null) {
|
||||
try {
|
||||
FileProviderUtil.getUriFor(context, new File(externalDir, "test"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"External cache dir not resolvable by FileProvider, "
|
||||
+ "falling back to internal cache",
|
||||
e);
|
||||
externalDir = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (externalDir == null) {
|
||||
externalDir = context.getCacheDir();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.relay;
|
||||
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.getContext;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
@@ -24,7 +23,6 @@ import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
@@ -44,7 +42,6 @@ import org.thoughtcrime.securesms.WelcomeActivity;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.proxy.ProxySettingsActivity;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -79,8 +76,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
Spinner smtpSecurity;
|
||||
Spinner certCheck;
|
||||
|
||||
private SwitchCompat proxySwitch;
|
||||
|
||||
Rpc rpc;
|
||||
int accId;
|
||||
|
||||
@@ -116,13 +111,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
smtpSecurity = findViewById(R.id.smtp_security);
|
||||
certCheck = findViewById(R.id.cert_check);
|
||||
|
||||
proxySwitch = findViewById(R.id.proxy_settings);
|
||||
proxySwitch.setOnClickListener(
|
||||
l -> {
|
||||
proxySwitch.setChecked(!proxySwitch.isChecked()); // revert toggle
|
||||
startActivity(new Intent(this, ProxySettingsActivity.class));
|
||||
});
|
||||
|
||||
String addr = getIntent().getStringExtra(EXTRA_ADDR);
|
||||
EnteredLoginParam config = null;
|
||||
try {
|
||||
@@ -185,10 +173,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
boolean expandAdvanced = false;
|
||||
int intVal;
|
||||
|
||||
intVal = DcHelper.getInt(this, CONFIG_PROXY_ENABLED);
|
||||
proxySwitch.setChecked(intVal == 1);
|
||||
expandAdvanced = expandAdvanced || intVal == 1;
|
||||
|
||||
if (config != null) { // configured
|
||||
emailInput.setText(config.addr);
|
||||
if (!TextUtils.isEmpty(config.addr)) {
|
||||
@@ -246,7 +230,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1);
|
||||
}
|
||||
|
||||
private void showLog() {
|
||||
|
||||
@@ -198,10 +198,6 @@ public class Prefs {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isCallsEnabled(Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ringtone
|
||||
|
||||
public static @NonNull Uri getNotificationRingtone(Context context) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -57,34 +58,34 @@ public class WebRTCClient {
|
||||
private final Context context;
|
||||
private final Handler mainHandler;
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
private List<PeerConnection.IceServer> iceServers;
|
||||
private volatile PeerConnection peerConnection;
|
||||
private volatile List<PeerConnection.IceServer> iceServers;
|
||||
|
||||
private DataChannel iceTricklingDataChannel;
|
||||
private DataChannel mutedStateDataChannel;
|
||||
|
||||
// ICE candidate
|
||||
private final List<IceCandidate> iceCandidateBuffer;
|
||||
private boolean iceTricklingChannelOpen;
|
||||
private boolean mutedStateChannelOpen;
|
||||
private boolean enableIceTrickling;
|
||||
private boolean isEnded = false;
|
||||
private volatile boolean iceTricklingChannelOpen;
|
||||
private volatile boolean mutedStateChannelOpen;
|
||||
private volatile boolean enableIceTrickling;
|
||||
private final AtomicBoolean isEnded = new AtomicBoolean(false);
|
||||
|
||||
// ICE gathering
|
||||
private volatile boolean isIceGatheringComplete;
|
||||
private volatile boolean hasRelayCandidate;
|
||||
private volatile boolean hasSrflxCandidate;
|
||||
private volatile boolean hasHostCandidate;
|
||||
private CountDownLatch iceGatheringLatch;
|
||||
private CountDownLatch relayCandidateLatch;
|
||||
private CountDownLatch srflxCandidateLatch;
|
||||
private volatile CountDownLatch iceGatheringLatch;
|
||||
private volatile CountDownLatch relayCandidateLatch;
|
||||
private volatile CountDownLatch srflxCandidateLatch;
|
||||
|
||||
// Media
|
||||
private MediaStream localStream;
|
||||
private VideoTrack localVideoTrack;
|
||||
private AudioTrack localAudioTrack;
|
||||
private VideoTrack remoteVideoTrack;
|
||||
private AudioTrack remoteAudioTrack;
|
||||
private volatile VideoTrack localVideoTrack;
|
||||
private volatile AudioTrack localAudioTrack;
|
||||
private volatile VideoTrack remoteVideoTrack;
|
||||
private volatile AudioTrack remoteAudioTrack;
|
||||
|
||||
// Callbacks to ViewModel
|
||||
private final Callbacks callbacks;
|
||||
@@ -644,7 +645,7 @@ public class WebRTCClient {
|
||||
boolean gotIce = waitForEnoughIce();
|
||||
|
||||
synchronized (WebRTCClient.this) {
|
||||
if (isEnded || peerConnection == null) {
|
||||
if (isEnded.get() || peerConnection == null) {
|
||||
Log.d(TAG, "Call ended during ICE gathering, aborting");
|
||||
return;
|
||||
}
|
||||
@@ -803,7 +804,7 @@ public class WebRTCClient {
|
||||
boolean gotIce = waitForEnoughIce();
|
||||
|
||||
synchronized (WebRTCClient.this) {
|
||||
if (isEnded || peerConnection == null) {
|
||||
if (isEnded.get() || peerConnection == null) {
|
||||
Log.d(TAG, "Call ended during ICE gathering, aborting");
|
||||
return;
|
||||
}
|
||||
@@ -939,12 +940,10 @@ public class WebRTCClient {
|
||||
// Cleanup
|
||||
|
||||
public synchronized void endCall() {
|
||||
if (isEnded) {
|
||||
if (!isEnded.compareAndSet(false, true)) {
|
||||
Log.d(TAG, "endCall() already called, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isEnded = true;
|
||||
Log.d(TAG, "Ending call");
|
||||
|
||||
// Unblock any thread waiting in waitForEnoughIce()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M320,640L640,640L640,320L320,320L320,640ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
|
||||
</vector>
|
||||
@@ -129,7 +129,7 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="inbox, imap_login, imap_server, imap_port, imap_security_label, imap_security, outbox_view_spacer_top,
|
||||
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button, proxy_settings" />
|
||||
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/advanced_icon"
|
||||
@@ -157,17 +157,6 @@
|
||||
app:layout_constraintStart_toEndOf="@id/advanced_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/no_servers_hint" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/proxy_settings"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/proxy_use_proxy"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/inbox"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -176,7 +165,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/login_inbox"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/proxy_settings" />
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_text" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/imap_login"
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
android:id="@+id/menu_marknoticed_selected"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:title="@string/mark_as_unread"
|
||||
android:id="@+id/menu_markfresh_selected"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:title="@string/add_to_home_screen"
|
||||
android:id="@+id/menu_add_to_home_screen"
|
||||
|
||||
@@ -615,10 +615,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">До %1$s, повечето от които изображения с по-лошо качество</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">До %1$s, повечето от които изображения с балансирано качество</string>
|
||||
<string name="download_failed">Неуспешно сваляне</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s съобщение</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Сваляне на максимално наличното до %1$s</string>
|
||||
<string name="profile_image_select">Избор на изображение за профила</string>
|
||||
<string name="select_your_new_profile_image">Изберете новото изображение за профила си</string>
|
||||
<string name="profile_image_delete">Изтриване на изображението на профила</string>
|
||||
|
||||
@@ -397,8 +397,6 @@
|
||||
<string name="pref_show_emails_all">پوی</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB -->
|
||||
<string name="up_to_x">تا سقف %1$s</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">پیوم %1$s</string>
|
||||
<string name="profile_image_select">پسند شؽوات پوروفایل</string>
|
||||
<string name="profile_image_delete">پاک کردن شؽوات پوروفایل</string>
|
||||
<string name="pref_edit_profile">آلشت پوروفایل</string>
|
||||
|
||||
@@ -814,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Fins a %1$s, les imatges de pitjor qualitat</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Fins a %1$s, imatges de qualitat la majoria de vegades.</string>
|
||||
<string name="download_failed">La baixada ha fallat</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s missatge</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Baixa el màxim disponible fins a %1$s</string>
|
||||
<string name="profile_image_select">Trieu la imatge de perfil</string>
|
||||
<string name="select_your_new_profile_image">Trieu la imatge nova de perfil</string>
|
||||
<string name="profile_image_delete">Esborra la imatge del perfil</string>
|
||||
|
||||
@@ -788,10 +788,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Do %1$s, většina obrázků horší kvality</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Do %1$s, většina obrázků vyvážené kvality</string>
|
||||
<string name="download_failed">Stahování selhalo</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s zpráva</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Stahovat dostupné maximum do %1$s</string>
|
||||
<string name="profile_image_select">Vybrat profilový obrázek</string>
|
||||
<string name="select_your_new_profile_image">Vyberte si svůj nový profilový obrázek</string>
|
||||
<string name="profile_image_delete">Smazat profilový obrázek</string>
|
||||
|
||||
@@ -377,7 +377,6 @@
|
||||
<string name="share_location_for_one_hour">für 1 Stunde</string>
|
||||
<string name="share_location_for_two_hours">für 2 Stunden</string>
|
||||
<string name="share_location_for_six_hours">für 6 Stunden</string>
|
||||
<string name="share_location_for_12_hours">für 12 Stunden</string>
|
||||
|
||||
<plurals name="ask_send_following_n_files_to">
|
||||
<item quantity="one">Die folgende Datei an %s senden?</item>
|
||||
@@ -815,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Bis zu %1$s, die meisten Bilder schlechter Qualität</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Bis zu %1$s, die meisten Bilder ausgewogener Qualität</string>
|
||||
<string name="download_failed">Download gescheitert</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s Nachricht</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Download max. verfügbar bis %1$s</string>
|
||||
<string name="profile_image_select">Profilbild auswählen</string>
|
||||
<string name="select_your_new_profile_image">Wählen dein Profilbild aus</string>
|
||||
<string name="profile_image_delete">Profilbild löschen</string>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="or_separator">oder</string>
|
||||
<string name="online">online</string>
|
||||
<string name="pink">Rosa</string>
|
||||
<string name="gray">Grau</string>
|
||||
<string name="share_location_for_24_hours">für 24 Stunden</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<string name="device_welcome_message">🔮 Willkommen bei ArcaneChat✨\n\nUm mit Freunden in Kontakt zu treten:\n\n🤳🏻Tippe auf dem Startbildschirm beider Geräte auf „QR-Code“. Wähle auf einem Gerät „QR-Code scannen“ und richte die Kamera auf das andere.\n\n🔗 Falls ihr euch nicht im selben Raum befindet, teilt euren Einladungslink über „Freunde einladen“ im Menü.\n\n📢Tritt dem [Offiziellen ArcaneChat-Kanal](%1$s) (auf Englisch) bei, um über die App-Entwicklung auf dem Laufenden zu bleiben.</string>
|
||||
|
||||
</resources>
|
||||
@@ -526,10 +526,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Έως και %1$s, τις περισσότερες εικόνες χειρότερης ποιότητας</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Έως %1$s, εικόνες πιο ισορροπημένης ποιότητας</string>
|
||||
<string name="download_failed">Αποτυχία Λήψης</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s μήνυμα</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Μέγιστη διαθέσιμη λήψη μέχρι %1$s</string>
|
||||
<string name="profile_image_select">Επιλογή Εικόνας Προφίλ</string>
|
||||
<string name="select_your_new_profile_image">Επιλογή Νέας Εικόνας Προφίλ</string>
|
||||
<string name="profile_image_delete">Διαγραφή Εικόνας Προφίλ</string>
|
||||
|
||||
@@ -627,7 +627,7 @@
|
||||
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
|
||||
<string name="onboarding_alternative_logins">Ya tengo un perfil</string>
|
||||
<!-- This is a button and a title, allowing to use existing, classic email, setting ports, passwords and so on -->
|
||||
<string name="manual_account_setup_option">Usar correo-e clásico como Retransmisor</string>
|
||||
<string name="manual_account_setup_option">Usar correo electrónico como Retransmisor</string>
|
||||
<!-- Instant onboarding title (there is not more to do than to set name and avatar) -->
|
||||
<string name="instant_onboarding_title">Tu perfil</string>
|
||||
<!-- The placeholder will be replaced by the default onboarding server -->
|
||||
@@ -831,10 +831,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Hasta %1$s, la mayoría de imágenes de peor calidad</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Hasta %1$s, la mayoría de imágenes de calidad balanceada</string>
|
||||
<string name="download_failed">Descarga fallida</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Mensaje de %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Descarga disponible hasta %1$s</string>
|
||||
<string name="profile_image_select">Seleccionar imagen de perfil</string>
|
||||
<string name="select_your_new_profile_image">Seleccionar tu nueva imagen de perfil</string>
|
||||
<string name="profile_image_delete">Borrar imagen de perfil</string>
|
||||
|
||||
@@ -2,20 +2,14 @@
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="description">Descripción</string>
|
||||
<string name="or_separator">o</string>
|
||||
<string name="online">en línea</string>
|
||||
<string name="pink">Rosa</string>
|
||||
<string name="gray">Gris</string>
|
||||
<string name="share_location_for_12_hours">Por 12 horas</string>
|
||||
<string name="share_location_for_24_hours">Por 24 horas</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<string name="device_welcome_message">🔮 Bienvenido a ArcaneChat✨\n\nPara ponerte en contacto con amigos:\n\n🤳🏻
|
||||
Toca \"Código QR\" en la pantalla principal de ambos dispositivos. Elije \"Escanear código
|
||||
QR\" en un dispositivo y apunta la cámara hacia el otro\n\n🔗 Si no estás en la misma
|
||||
habitación, comparte tu enlace de invitación usando \"Invitar amigos\" en el menú\n\n📢
|
||||
Únete al [Canal Oficial de ArcaneChat](%1$s) (en inglés) para noticias sobre el desarrollo
|
||||
de la aplicación.</string>
|
||||
<string name="device_welcome_message">🔮 Bienvenido a ArcaneChat✨\n\nPara ponerte en contacto con amigos:\n\n🤳🏻 Toca \"Código QR\" en la pantalla principal de ambos dispositivos. Elije \"Escanear código QR\" en un dispositivo y apunta la cámara hacia el otro\n\n🔗 Si no estás en la misma habitación, comparte tu enlace de invitación usando \"Invitar amigos\" en el menú\n\n📢 Únete al [Canal Oficial de ArcaneChat](%1$s) (en inglés) para noticias sobre el desarrollo de la aplicación.</string>
|
||||
|
||||
<string name="update_2_46ac">🔮 ¿Qué hay de nuevo?\n\n✦ Llamadas de audio/video experimentales mejoradas con una mejor integración con el sistema\n\n✦ Se agregó un selector de stickers básico\n\n✦ Se mejoró el selector de archivos adjuntos\n\n✦ Se minimizó aún más los metadatos en los mensajes\n\n✦ Mayor resiliencia con el uso de múltiples relays: si un relay falla, los mensajes se siguen recibiendo en los demás\n\n✦ Mejor sincronización del estado de las notificaciones en varios dispositivos\n\n✦ Enlace de invitación corregido para unirse al 📢 [Canal oficial de ArcaneChat](%2$s) (en inglés)\n\n✦ Varias correcciones de errores y traducciones actualizadas\n\n\n💜 Por favor, dona para ayudarnos a mantenernos independientes y continuar brindando mejoras: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -669,6 +669,8 @@
|
||||
<string name="used_for_sending">Kasutatud saatmiseks</string>
|
||||
<string name="hide_from_contacts">Peida kontaktidest</string>
|
||||
<string name="hidden_from_contacts">Peidetud kontaktidest</string>
|
||||
<!-- Hint for the list of relays -->
|
||||
<string name="transport_list_hint">Sõnumid saavad kõik alusühendused (edastusserverid).\n\n⚠️ Kui muudad siin midagi, siis palun kontrolli, et kõikides sinu seadmetes on kasutusel vähemalt versioon 2.47.0. Vastasel juhul võivad vanema tarkvaraga seadmetes jääta sõnumid saabumata.</string>
|
||||
<!-- shown if a QR code was scanned that can be used as a relay -->
|
||||
<string name="confirm_add_transport">Kas lisad selle alusühenduse (edastusserveri)?</string>
|
||||
<string name="invalid_transport_qr">Skaneeritud QR-koodis pole korrektset alusühendust (edastusserverid)</string>
|
||||
@@ -812,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Suurusega kuni %1$s, üldjuhul kehva kvaliteediga pildid</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Suurusega kuni %1$s, üldjuhul mõistliku kvaliteediga pildid</string>
|
||||
<string name="download_failed">Allalaadimine ei õnnestunud</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Sõnum suurusega %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Allalaadimise ülempiir on saadaval kuni %1$s</string>
|
||||
<string name="profile_image_select">Vali profiilipilt</string>
|
||||
<string name="select_your_new_profile_image">Vali oma uus profiilipilt</string>
|
||||
<string name="profile_image_delete">Kustuta profiilipilt</string>
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<string name="last_week">Azken astea</string>
|
||||
<string name="last_month">Azken hilabetea</string>
|
||||
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by date or time, resulting in "Last seen at 12:13 AM" or "Last seen Nov 12" -->
|
||||
<string name="last_seen_at">%1$s-n ikusi da azken</string>
|
||||
<string name="last_seen_at">Azken aldiz begiratua: %1$s</string>
|
||||
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by a relative point in time as "3 minutes ago" (see https://momentjs.com for more examples and languages) -->
|
||||
<string name="last_seen_relative">Azken aldiz begiratua %1$s</string>
|
||||
<string name="last_seen_unknown">Azken aldiz begiratzea: ezezaguna</string>
|
||||
@@ -669,6 +669,8 @@
|
||||
<string name="used_for_sending">Bidaltzeko erabilita</string>
|
||||
<string name="hide_from_contacts">Ezkutatu kontaktuetatik</string>
|
||||
<string name="hidden_from_contacts">Kontaktuetatik ezkutatuta</string>
|
||||
<!-- Hint for the list of relays -->
|
||||
<string name="transport_list_hint">Mezuak errele guztietan jasotzen dira.\n\n⚠️ Hemen zerbait aldatzen baduzu, ziurtatu zure gailu guztiek gutxienez 2.47.0 bertsioa dutela. Bestela, gailu zaharragoek mezuak gal ditzakete.</string>
|
||||
<!-- shown if a QR code was scanned that can be used as a relay -->
|
||||
<string name="confirm_add_transport">Gehitu errelea?</string>
|
||||
<string name="invalid_transport_qr">Eskaneatutako QR kodeak ez dauka baliozko errelerik.</string>
|
||||
@@ -812,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">%1$s arte: kalitate txarreneko irudiak</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">%1$s arte: kalitate orekatueneko irudiak</string>
|
||||
<string name="download_failed">Deskargak huts egin du</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s mezu</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Deskargatu eskuragarri dagoen gehienezkoa %1$s arte</string>
|
||||
<string name="profile_image_select">Aukeratu abatarra</string>
|
||||
<string name="select_your_new_profile_image">Aukeratu zure abatar berria</string>
|
||||
<string name="profile_image_delete">Ezabatu abatarra</string>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="description">Deskribapena</string>
|
||||
<string name="could_not_open_file">Ezin izan da fitxategia ireki</string>
|
||||
<string name="menu_export_image">Esportatu irudia</string>
|
||||
<!-- the word "or" to separate blocks in the user interface that are mutually exclusive -->
|
||||
<string name="or_separator">edo</string>
|
||||
<string name="online">online</string>
|
||||
<string name="pink">Arrosa</string>
|
||||
<string name="gray">Grisa</string>
|
||||
<string name="share_location_for_12_hours">12 orduz</string>
|
||||
<string name="share_location_for_24_hours">24 orduz</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<!-- when translating the sentence about the ArcaneChat channel, clarify that the channel is in English -->
|
||||
|
||||
@@ -770,10 +770,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">تا سقف %1$s، بدترین کیفیت تصویر</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">تا سقف %1$s، متعادلترین کیفیت تصویر</string>
|
||||
<string name="download_failed">بارگیری ناموفق</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">پیام %1$s </string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">سقف دانلود تا %1$s فعال خواهد بود</string>
|
||||
<string name="profile_image_select">انتخاب تصویر نمایه</string>
|
||||
<string name="select_your_new_profile_image">تصویر نمایهٔ جدیدتان را برگزینید</string>
|
||||
<string name="profile_image_delete">حذف تصویر نمایه</string>
|
||||
|
||||
@@ -619,10 +619,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Korkeintaan %1$s, suurin osa alhaisen laadun kuvista</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Korkeintaan %1$s, suurin osa tasapainoisen laadun kuvista</string>
|
||||
<string name="download_failed">Lataus epäonnistui</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s viesti</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Latauksen yläraja saatavilla %1$s asti</string>
|
||||
<string name="profile_image_select">Valitse profiilikuva</string>
|
||||
<string name="select_your_new_profile_image">Valitse uusi profiilikuvasi</string>
|
||||
<string name="profile_image_delete">Poista profiilikuva</string>
|
||||
|
||||
@@ -388,7 +388,6 @@
|
||||
<string name="share_location_for_one_hour">pour 1 heure</string>
|
||||
<string name="share_location_for_two_hours">pour 2 heures</string>
|
||||
<string name="share_location_for_six_hours">pour 6 heures</string>
|
||||
<string name="share_location_for_12_hours">pour 12 heures</string>
|
||||
|
||||
<plurals name="ask_send_following_n_files_to">
|
||||
<item quantity="one">Envoyer le fichier suivant à %s ?</item>
|
||||
@@ -830,10 +829,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Jusqu\'à %1$s, plus basse qualité d\'image</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Jusqu\'à %1$s, qualité d\'image la plus équilibrée </string>
|
||||
<string name="download_failed">Échec du téléchargement</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s message</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Télécharger le plus possible jusqu\'à %1$s</string>
|
||||
<string name="profile_image_select">Sélectionnez l\'image de profil</string>
|
||||
<string name="select_your_new_profile_image">Sélectionnez votre nouvelle image de profil</string>
|
||||
<string name="profile_image_delete">Supprimer l\'image de profil</string>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="share_location_for_24_hours">pour 24 heures</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<string name="device_welcome_message">🔮 Bienvenue sur ArcaneChat ✨\n\nPour vous connecter avec vos amis:\n\n🤳🏻Appuyez sur «Code QR» sur l’écran d’accueil des deux appareils. Sélectionnez «Scanner le code QR» sur un appareil et pointez l’appareil photo vers l’autre.\n\n🔗 Si vous n’êtes pas dans la même pièce, partagez votre lien d’invitation via «Inviter des amis» dans le menu.\n\n📢Rejoignez le [Chaîne officielle ArcaneChat](%1$s) (en anglais) pour suivre l’actualité du développement de l’application.</string>
|
||||
|
||||
</resources>
|
||||
@@ -713,10 +713,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Ata %1$s, a maioría de imaxes da menor calidade</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Ata %1$s, a maioría de imaxes de calidade media</string>
|
||||
<string name="download_failed">Fallou a descarga</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s mensaxe</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Descarga o máximo dispoñible ata %1$s</string>
|
||||
<string name="profile_image_select">Elixe imaxe de perfil</string>
|
||||
<string name="select_your_new_profile_image">Elixe a túa nova imaxe de perfil</string>
|
||||
<string name="profile_image_delete">Elimina a imaxe de perfil</string>
|
||||
|
||||
@@ -744,10 +744,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Legfeljebb %1$s, leginkább rosszabb minőségű kép</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Legfeljebb %1$s, leginkább kiegyensúlyozatabb minőségű kép</string>
|
||||
<string name="download_failed">Sikertelen letöltés</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s üzenet</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Letöltés maximum ennyi ideig visszamenőleg: %1$s </string>
|
||||
<string name="profile_image_select">Profilkép kiválasztása</string>
|
||||
<string name="select_your_new_profile_image">Válassza ki az új profilképet</string>
|
||||
<string name="profile_image_delete">Profilkép törlése</string>
|
||||
|
||||
@@ -388,7 +388,6 @@
|
||||
<string name="share_location_for_one_hour">Per 1 ora</string>
|
||||
<string name="share_location_for_two_hours">Per 2 ore</string>
|
||||
<string name="share_location_for_six_hours">Per 6 ore</string>
|
||||
<string name="share_location_for_12_hours">Per 12 ore</string>
|
||||
|
||||
<plurals name="ask_send_following_n_files_to">
|
||||
<item quantity="one">Invia il seguente file a %s?</item>
|
||||
@@ -832,10 +831,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Fino a %1$s, immagini di qualità peggiore</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Fino a %1$s, immagini di qualità più equilibrata</string>
|
||||
<string name="download_failed">Scaricamento fallito</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s messaggio</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Scaricamento disponibile massimo fino a %1$s</string>
|
||||
<string name="profile_image_select">Seleziona Immagine Profilo</string>
|
||||
<string name="select_your_new_profile_image">Seleziona la tua nuova immagine profilo</string>
|
||||
<string name="profile_image_delete">Elimina Immagine Profilo</string>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="share_location_for_24_hours">Per 24 ore</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<string name="device_welcome_message">🔮 Benvenuti su ArcaneChat✨\n\nPer connettervi con gli amici:\n\n🤳🏻 Tocca "Codice QR" sulla schermata iniziale di entrambi i dispositivi. Seleziona \"Scansiona codice QR\" su un dispositivo e punta la fotocamera verso l\'altro.\n\n🔗 Se non siete nella stessa stanza, condividi il tuo link di invito utilizzando \"Invita amici\" nel menu.\n\n📢 Unisciti al [Canale ufficiale di ArcaneChat](%1$s) (in inglese) per gli aggiornamenti sullo sviluppo dell\'app.</string>
|
||||
|
||||
</resources>
|
||||
@@ -810,10 +810,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Arma d %1$s, tugniwin n tɣara n diri maḍi</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Arma d %1$s, tugniwin n tɣara yemneknayen</string>
|
||||
<string name="download_failed">Asader ur yeddi ara</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Izen %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Sader-d afellay i yellan alamma d %1$s</string>
|
||||
<string name="profile_image_select">Fren tugna n umaɣnu</string>
|
||||
<string name="select_your_new_profile_image">Fren tugna-ik·im tamaynut n umaɣnu</string>
|
||||
<string name="profile_image_delete">Kkes tugna n umaɣnu</string>
|
||||
|
||||
@@ -502,10 +502,6 @@
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB -->
|
||||
<string name="up_to_x">%1$s까지 </string>
|
||||
<string name="download_failed">다운로드 실패</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s 메시지</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">%1$s까지 최대 다운로드 가능</string>
|
||||
<string name="profile_image_select">프로필 이미지 선택</string>
|
||||
<string name="select_your_new_profile_image">새로운 프로필 이미지 선택</string>
|
||||
<string name="profile_image_delete">프로필 이미지 삭제</string>
|
||||
|
||||
@@ -720,8 +720,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Iki %1$s, iš esmės, blogos kokybės paveikslus</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Iki %1$s, iš esmės, subalansuotos kokybės paveikslus</string>
|
||||
<string name="download_failed">Atsiuntimas nepavyko</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s žinutė</string>
|
||||
<string name="profile_image_select">Pasirinkti profilio nuotrauką</string>
|
||||
<string name="select_your_new_profile_image">Pasirinkite naują profilio nuotrauką</string>
|
||||
<string name="profile_image_delete">Ištrinti profilio nuotrauką</string>
|
||||
|
||||
@@ -814,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Tot %1$s - lage afbeeldingskwaliteit</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Tot %1$s - redelijke afbeeldingskwaliteit</string>
|
||||
<string name="download_failed">Het downloaden is mislukt</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s bericht</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Maximaal downloaden tot en met een aantal van %1$s</string>
|
||||
<string name="profile_image_select">Profielfoto kiezen</string>
|
||||
<string name="select_your_new_profile_image">Kies een nieuwe profielfoto</string>
|
||||
<string name="profile_image_delete">Profielfoto verwijderen</string>
|
||||
|
||||
@@ -800,10 +800,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Do %1$s, większ. obrazów gorszej jakości</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Do %1$s, obrazy zrównoważonej jakości</string>
|
||||
<string name="download_failed">Pobieranie nie udane</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Wiadomość %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Maksymalna ilość do pobrania dostępna do %1$s</string>
|
||||
<string name="profile_image_select">Wybierz zdjęcie profilowe</string>
|
||||
<string name="select_your_new_profile_image">Wybierz nowe zdjęcie profilowe</string>
|
||||
<string name="profile_image_delete">Usuń zdjęcie profilowe</string>
|
||||
|
||||
@@ -585,10 +585,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Até %1$s, imagens de pior qualidade</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Até %1$s, imagens de qualidade equilibrada</string>
|
||||
<string name="download_failed">Falha no Download</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Mensagem de %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Descarregamento máximo disponível até %1$s</string>
|
||||
<string name="profile_image_select">Selecione a imagem do perfil</string>
|
||||
<string name="select_your_new_profile_image">Selecione sua nova imagem do perfil</string>
|
||||
<string name="profile_image_delete">Excluir imagem do perfil</string>
|
||||
|
||||
@@ -600,13 +600,13 @@
|
||||
<string name="multidevice_this_creates_a_qr_code">Это позволяет создать QR-код, который можно отсканировать на втором устройстве, чтобы скопировать профиль.\n\nУбедитесь, что посторонние лица или камеры не могут видеть следующий экран.</string>
|
||||
<string name="multidevice_install_dc_on_other_device">Установите Delta Chat на другом устройстве (https://get.delta.chat)</string>
|
||||
<!-- "I Already Have a Profile / Add as Second Device” should be the same text as defined by the keys onboarding_alternative_logins and multidevice_receiver_title -->
|
||||
<string name="multidevice_tap_scan_on_other_device">Запустите Delta Chat, выберите \"У меня уже есть профиль / Добавить как второе устройство\" и отсканируйте код, показанный здесь</string>
|
||||
<string name="multidevice_tap_scan_on_other_device">Запустите Delta Chat, нажмите \"У меня уже есть профиль / Добавить как второе устройство\" и отсканируйте показанный здесь код</string>
|
||||
<!-- Shown inside a "QR code card" with very limited space; please formulate the text as short as possible therefore. The placeholder will be replaced by the profile name eg. "Scan to set up second device for Alice" -->
|
||||
<string name="multidevice_qr_subtitle">Отсканируйте, чтобы настроить второе устройство для %1$s</string>
|
||||
<string name="multidevice_receiver_title">Добавить как второе устройство</string>
|
||||
<string name="multidevice_open_settings_on_other_device">На первом устройстве запустите Delta Chat, откройте \"Настройки / Добавить второе устройство\" и отсканируйте показанный там код</string>
|
||||
<string name="multidevice_open_settings_on_other_device">На первом устройстве запустите Delta Chat, перейдите в \"Настройки / Добавить второе устройство\" и отсканируйте показанный там код</string>
|
||||
<string name="multidevice_receiver_scanning_ask">Скопировать профиль с другого устройства на это устройство?</string>
|
||||
<string name="multidevice_receiver_needs_update">Профиль, который вы хотите импортировать, создан в более новой версии Delta Chat. Чтобы продолжить настройку второго устройства, пожалуйста, обновите это устройство до последней версии Delta Chat.</string>
|
||||
<string name="multidevice_receiver_needs_update">Профиль, который вы хотите импортировать, создан в более новой версии Delta Chat.\n\nЧтобы продолжить настройку второго устройства, пожалуйста, обновите это устройство до последней версии Delta Chat.</string>
|
||||
<string name="multidevice_abort">Прервать настройку второго устройства?</string>
|
||||
<string name="multidevice_abort_will_invalidate_copied_qr">Это сделает недействительным QR-код, скопированный в буфер обмена.</string>
|
||||
<string name="multidevice_transfer_done_devicemsg">ℹ️ Профиль перенесен на ваше второе устройство.</string>
|
||||
@@ -668,7 +668,7 @@
|
||||
<string name="welcome_chat_over_email">Безопасный децентрализованный чат</string>
|
||||
<string name="scan_invitation_code">Сканировать код приглашения</string>
|
||||
<string name="login_title">Войти</string>
|
||||
<string name="login_advanced_hint">Эта авторизация предназначена для опытных пользователей:\n\n• Не используйте адрес, который вы уже используете в другом приложении.\n\n• Обычный почтовый сервер позволяет вести чаты без сквозного шифрования, что отмечено значком почты.</string>
|
||||
<string name="login_advanced_hint">Этот способ входа предназначен для опытных пользователей:\n\n• Не используйте адрес, который вы уже используете в другом приложении.\n\n• Обычный почтовый сервер позволяет вести чаты без сквозного шифрования, (такие чаты помечаются значком конверта).</string>
|
||||
<string name="login_inbox">Входящие</string>
|
||||
<string name="login_imap_login">Имя пользователя IMAP</string>
|
||||
<string name="login_imap_server">Сервер IMAP</string>
|
||||
@@ -704,7 +704,7 @@
|
||||
<string name="hide_from_contacts">Скрыть из контактов</string>
|
||||
<string name="hidden_from_contacts">Скрыт из контактов</string>
|
||||
<!-- Hint for the list of relays -->
|
||||
<string name="transport_list_hint">Сообщения поступают на все релеи.\n\n⚠️ Если вы что-либо измените здесь, убедитесь, что все ваши устройства работают как минимум на версии 2.47.0. В противном случае старые устройства могут пропускать сообщения.</string>
|
||||
<string name="transport_list_hint">Сообщения поступают на все релеи.\n\n⚠️ Если вы измените какие-либо настройки в этом разделе, убедитесь, что на всех ваших устройствах установлена версия Delta Chat не ниже 2.47.0. В противном случае старые версии могут не получать сообщения.</string>
|
||||
<!-- shown if a QR code was scanned that can be used as a relay -->
|
||||
<string name="confirm_add_transport">Добавить этот релей?</string>
|
||||
<string name="invalid_transport_qr">Отсканированный QR-код не содержит корректный релей.</string>
|
||||
@@ -746,7 +746,7 @@
|
||||
<!-- first placeholder is replaced by the number of files (always 2 or more); second placeholder is replaced by a chat name -->
|
||||
<string name="ask_send_files_to_chat">Отправить %1$d файла(ов) в \"%2$s\"?</string>
|
||||
<string name="ask_send_files_to_selected_chats">Отправить %1$d файл(ов) в %2$d чата(ов)?</string>
|
||||
<string name="videos_sent_without_recoding">(Отправка нескольких видео приведет к их отправке в исходном виде, что может потребовать значительного объема данных. Чтобы уменьшить размер файлов и сэкономить трафик, отправляйте каждое видео в отдельном сообщении.)</string>
|
||||
<string name="videos_sent_without_recoding">(Отправка нескольких видео приведет к их отправке в исходном виде, что может потребовать значительного объема данных. Чтобы уменьшить размер файлов и сэкономить трафик, отправляйте каждое видео в отдельном сообщении)</string>
|
||||
<string name="share_text_multiple_chats">Отправить текст в %1$d чата(ов)?\n\n\"%2$s\"</string>
|
||||
<string name="share_abort">Общий доступ прерван из-за отсутствия необходимых разрешений.</string>
|
||||
|
||||
@@ -764,7 +764,7 @@
|
||||
<string name="pref_enter_sends">Отправка по Enter</string>
|
||||
<string name="pref_enter_sends_explain">Нажатие клавиши Enter отправит текстовые сообщения</string>
|
||||
<string name="pref_outgoing_media_quality">Качество отправляемых медиафайлов</string>
|
||||
<string name="pref_outgoing_media_quality_hint">Чтобы отправить файлы в оригинальном качестве, прикрепляйте их как \"Файл\". Это потребует больше трафика для вас и ваших получателей.</string>
|
||||
<string name="pref_outgoing_media_quality_hint">Чтобы отправить файлы в исходном качестве, прикрепляйте их как \"Файл\". Это потребует больше трафика для вас и ваших получателей.</string>
|
||||
<string name="pref_outgoing_balanced">Сбалансированное</string>
|
||||
<string name="pref_outgoing_worse">Низкое качество, малый размер</string>
|
||||
<string name="pref_vibrate">Вибрация</string>
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="pref_other">Другое</string>
|
||||
<string name="pref_backup">Резервное копирование</string>
|
||||
<string name="pref_backup_explain">Резервное копирование чатов на внешний носитель</string>
|
||||
<string name="pref_backup_export_explain">Резервная копия поможет вам восстановить данные на этом или на другом устройстве.\n\nРезервная копия содержит все сообщения, контакты и чаты, а также параметры сквозного шифрования Autocrypt. Храните файл резервной копии в безопасном месте или удалите его как можно скорее.</string>
|
||||
<string name="pref_backup_export_explain">Резервная копия поможет вам восстановить данные на этом или на другом устройстве.\n\nРезервная копия содержит все сообщения, контакты и чаты, а также параметры сквозного шифрования Autocrypt. Храните файл резервной копии в безопасном месте или удалите его при первой возможности.</string>
|
||||
<string name="pref_backup_export_this">Экспортировать профиль</string>
|
||||
<!-- the placeholder will be replaced by the number of profiles to export; the number is always larger than 1 -->
|
||||
<string name="pref_backup_export_all">Экспорт всех %1$d профилей</string>
|
||||
@@ -826,8 +826,8 @@
|
||||
<string name="pref_imap_folder_warn_disable_defaults">Если вы измените этот параметр, убедитесь, что ваш сервер и другие клиенты настроены соответствующим образом.\n\nВ противном случае, всё может перестать работать.</string>
|
||||
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
|
||||
<string name="pref_multidevice">Режим нескольких устройств</string>
|
||||
<string name="pref_multidevice_explain">Синхронизируйте свои сообщения с другими устройствами. Автоматически включается при добавлении второго устройства</string>
|
||||
<string name="pref_multidevice_change_warn">Режим нескольких устройств должен быть включен при использовании одного и того же профиля на нескольких устройствах. Отключайте эту настройку, только если вы удалили этот профиль со всех остальных устройств.\n\nОтключение режима нескольких устройств при использовании профиля на нескольких устройствах приведет к пропуску сообщений и другим проблемам.</string>
|
||||
<string name="pref_multidevice_explain">Синхронизируйте свои сообщения с другими устройствами. Функция включается автоматически при добавлении второго устройства</string>
|
||||
<string name="pref_multidevice_change_warn">Режим нескольких устройств должен быть включен, если вы используете один и тот же профиль на нескольких устройствах. Отключайте эту настройку, только если вы удалили этот профиль со всех остальных устройств.\n\nОтключение режима нескольких устройств при использовании профиля на нескольких устройствах приведет к потере сообщений и возникновению других проблем.</string>
|
||||
<string name="pref_auto_folder_moves">Автоперемещение в папку DeltaChat</string>
|
||||
<string name="pref_only_fetch_mvbox_title">Загружать только из папки DeltaChat</string>
|
||||
<string name="pref_show_emails">Показывать обычную почту</string>
|
||||
@@ -848,10 +848,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">До %1$s, изображения плохого качества</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">До %1$s, изображения среднего качества</string>
|
||||
<string name="download_failed">Загрузка не удалась</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s сообщение</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Максимальная загрузка доступна до %1$s</string>
|
||||
<string name="profile_image_select">Выбрать изображение профиля</string>
|
||||
<string name="select_your_new_profile_image">Выбрать новое изображение профиля</string>
|
||||
<string name="profile_image_delete">Удалить изображение профиля</string>
|
||||
@@ -860,7 +856,7 @@
|
||||
<string name="send_stats_to_devs">Отправить статистику разработчикам Delta Chat</string>
|
||||
<string name="stats_device_message">Хотите помочь улучшить Delta Chat и поддержать исследования, отправляя еженедельную анонимную статистику использования?\n\n👉 Нажмите здесь...👈</string>
|
||||
<string name="stats_confirmation_dialog">Хотите помочь улучшить Delta Chat и поддержать исследования, отправляя еженедельную анонимную статистику использования?</string>
|
||||
<string name="stats_thanks">Спасибо! Вы всегда можете отключить отправку в разделе \"Настройки -> Дополнительно\"\n\nУ вас есть 5 минут, чтобы принять участие в научном исследовании по безопасности Delta Chat?</string>
|
||||
<string name="stats_thanks">Спасибо! Вы всегда можете отключить отправку в разделе \"Настройки -> Дополнительно\"\n\nУ вас есть 5 минут, чтобы принять участие в исследовании безопасности Delta Chat?</string>
|
||||
<string name="stats_disable_dialog">Отправка статистики уже включена.\n\nВы хотите отключить её?</string>
|
||||
<string name="disable">Отключить</string>
|
||||
<string name="stats_keep_sending">Продолжить отправку</string>
|
||||
@@ -893,7 +889,7 @@
|
||||
<string name="autodel_device_title">Удалять сообщения с устройства</string>
|
||||
<string name="autodel_server_title">Удалять сообщения с сервера</string>
|
||||
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
|
||||
<string name="autodel_device_ask">Вы хотите удалить %1$d сообщений сейчас и все новые сообщения \"%2$s\" в дальнейшем?\n\n• Это относится ко всем медиафайлам\n\n• Сообщения будут удалены, независимо от того, были ли они просмотрены или нет.\n\n• \"Сохранённые сообщения\" будут пропущены при локальном удалении</string>
|
||||
<string name="autodel_device_ask">Вы хотите удалить %1$d сообщений сейчас и все новые сообщения \"%2$s\" в дальнейшем?\n\n• Это действие затронет все медиафайлы\n\n• Сообщения будут удалены, независимо от того, были ли они прочитаны или нет.\n\n• \"Сохранённые сообщения\" не будут удалены с устройства</string>
|
||||
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
|
||||
<string name="autodel_server_ask">Вы хотите удалить %1$d сообщений сейчас и все новые сообщения \"%2$s\" в дальнейшем?\n\n⚠️ Это относится ко всем электронным письмам, медиафайлам и \"Сохранённым сообщениям\" во всех папках на сервере.\n\n⚠️ Не используйте эту функцию, если хотите сохранить данные на сервере или если вы используете обычные клиенты электронной почты</string>
|
||||
<!-- shown below enabled autodel_server-option, should be a summary of autodel_server_ask and remind about the impact -->
|
||||
@@ -1015,9 +1011,9 @@
|
||||
<string name="qrscan_contains_text">Текст из QR-кода:\n\n%1$s</string>
|
||||
<string name="qrscan_contains_url">URL из QR-кода:\n\n%1$s</string>
|
||||
<string name="qrscan_fingerprint_label">Отпечаток</string>
|
||||
<string name="withdraw_verifycontact_explain">Этот QR-код можно отсканировать, чтобы связаться с вами.\n\nВы можете сбросить его и тогда существующий QR-код или ссылка-приглашение перестанут работать</string>
|
||||
<string name="withdraw_verifygroup_explain">Этот QR-код можно отсканировать, чтобы присоединиться к группе \"%1$s\".\n\nВы можете сбросить его и тогда существующий QR-код или ссылка-приглашение перестанут работать</string>
|
||||
<string name="withdraw_joinbroadcast_explain">Этот QR-код могут сканировать другие пользователи для присоединения к каналу \"%1$s\".\n\nВы можете сбросить его, чтобы существующий QR-код или пригласительная ссылка перестали работать.</string>
|
||||
<string name="withdraw_verifycontact_explain">Этот QR-код можно отсканировать, чтобы связаться с вами.\n\nВы можете сбросить его и тогда существующий QR-код или ссылка-приглашение перестанут работать.</string>
|
||||
<string name="withdraw_verifygroup_explain">Этот QR-код можно отсканировать, чтобы присоединиться к группе \"%1$s\".\n\nВы можете сбросить его и тогда существующий QR-код или ссылка-приглашение перестанут работать.</string>
|
||||
<string name="withdraw_joinbroadcast_explain">Этот QR-код можно отсканировать, чтобы присоединиться к каналу \"%1$s\".\n\nВы можете сбросить его и тогда существующий QR-код или ссылка-приглашение перестанут работать.</string>
|
||||
<string name="withdraw_qr_code">Сбросить QR-код</string>
|
||||
<string name="revive_verifycontact_explain">Этот QR-код был сброшен и больше не работает.</string>
|
||||
<string name="revive_qr_code">Активировать QR-код</string>
|
||||
@@ -1034,7 +1030,7 @@
|
||||
<string name="qrshow_join_contact_hint">Отсканируйте, чтобы начать чат с %1$s</string>
|
||||
<string name="qrshow_join_contact_no_connection_toast">Нет соединения с интернетом, невозможно выполнить настройку QR-кода.</string>
|
||||
<string name="qraccount_ask_create_and_login">Создать новый профиль на \"%1$s\" и авторизоваться?</string>
|
||||
<string name="set_name_and_avatar_explain">Укажите имя, которое будут видеть ваши контакты. Также можно установить изображение профиля.</string>
|
||||
<string name="set_name_and_avatar_explain">Укажите имя, которое будут видеть ваши контакты. Вы также можете установить изображение профиля.</string>
|
||||
<string name="please_enter_name">Введите имя.</string>
|
||||
<string name="qraccount_qr_code_cannot_be_used">Отсканированный QR-код не может быть использован для создания нового профиля.</string>
|
||||
<!-- the placeholder will be replaced by the address of the profile -->
|
||||
@@ -1076,12 +1072,12 @@
|
||||
<!-- permissions -->
|
||||
<string name="perm_required_title">Требуется разрешение</string>
|
||||
<string name="perm_continue">Продолжить</string>
|
||||
<string name="perm_explain_access_to_camera_denied">Чтобы делать фотографии, снимать видео, откройте настройки приложения, выберите \"Разрешения\" и включите \"Камера\".</string>
|
||||
<string name="perm_explain_access_to_camera_denied">Чтобы делать фотографии, снимать видео, откройте настройки приложения, выберите \"Разрешения\" и включите доступ к \"Камере\".</string>
|
||||
<!-- give the user an idea where to find the "Microphone" option. the hint is only shown if access was initially denied by the user. pick up wordings really used on the systems, but do not be overly precise: things shift around often and there are too many system we support to track every detail and click path -->
|
||||
<string name="perm_explain_access_to_mic_denied">Чтобы отправлять аудиосообщения, откройте настройки приложения, выберите \"Разрешения\" или \"Конфиденциальность и безопасность\" и включите \"Микрофон\".</string>
|
||||
<string name="perm_explain_access_to_storage_denied">Чтобы получать или отправлять файлы, откройте настройки приложения, выберите \"Разрешения\" и включите \"Хранилище\".</string>
|
||||
<string name="perm_explain_access_to_location_denied">Чтобы прикрепить местоположение, откройте настройки приложения, выберите \"Разрешения\" и включите \"Местоположение\".</string>
|
||||
<string name="perm_explain_access_to_notifications_denied">Для получения уведомлений, перейдите в \"Настройки системы / Приложения / Delta Chat\" и включите \"Уведомления\".</string>
|
||||
<string name="perm_explain_access_to_mic_denied">Чтобы отправлять аудиосообщения, откройте настройки приложения, выберите \"Разрешения\" или \"Конфиденциальность и безопасность\" и включите доступ к \"Микрофону\".</string>
|
||||
<string name="perm_explain_access_to_storage_denied">Чтобы получать или отправлять файлы, откройте настройки приложения, выберите \"Разрешения\" и включите доступ к \"Хранилищу\".</string>
|
||||
<string name="perm_explain_access_to_location_denied">Чтобы прикрепить местоположение, откройте настройки приложения, выберите \"Разрешения\" и включите доступ к \"Местоположению\".</string>
|
||||
<string name="perm_explain_access_to_notifications_denied">Для получения уведомлений, перейдите в \"Настройки системы / Приложения / Delta Chat\" и включите доступ к \"Уведомлениям\".</string>
|
||||
|
||||
<!-- ImageEditorHud -->
|
||||
<string name="ImageEditorHud_draw_anywhere_to_blur">Нарисуйте где угодно, чтобы размыть</string>
|
||||
@@ -1166,11 +1162,11 @@
|
||||
<string name="share_account_not_configured">Профиль не настроен.</string>
|
||||
<string name="cannot_play_audio_file">Аудиофайл не может быть воспроизведён.</string>
|
||||
<!-- iOS camera permission alert -->
|
||||
<string name="perm_ios_explain_access_to_camera_denied">Чтобы делать фотографии, снимать видео и использовать сканер QR-кодов, откройте системные настройки и включите \"Камера\".</string>
|
||||
<string name="perm_ios_explain_access_to_camera_denied">Чтобы делать фотографии, снимать видео и использовать сканер QR-кодов, откройте системные настройки и включите доступ к \"Камере\".</string>
|
||||
<string name="open_settings">Открыть настройки</string>
|
||||
<!-- iOS device contact alert -->
|
||||
<string name="import_device_contacts">Импорт контактов устройства</string>
|
||||
<string name="import_device_contacts_hint">Чтобы переписываться с контактами из вашей адресной книги, откройте \"Настройки\" и включите \"Контакты\".</string>
|
||||
<string name="import_device_contacts_hint">Чтобы переписываться с контактами из вашей адресной книги, откройте \"Настройки\" и включите доступ к \"Контактам\".</string>
|
||||
<!-- iOS notification settings -->
|
||||
<string name="disabled_in_dc">Отключено в Delta Chat.</string>
|
||||
<string name="disabled_in_system_settings">Отключено в системных настройках</string>
|
||||
@@ -1212,5 +1208,5 @@
|
||||
<string name="perm_enable_bg_already_done">Вы уже разрешили Delta Chat получать сообщения в фоновом режиме.\n\nЕсли сообщения все равно не приходят в фоновом режиме, также проверьте настройки вашей системы.</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<string name="update_2_0">Что нового?\n\n💯 Сквозное шифрование стало постоянной и надёжной функцией. 🔒 Замков больше нет!\n\n✉️ Обычные письма без сквозного шифрования помечены значком конверта\n\n😻 Обновлённый экран профиля для всех контактов\n\n🔲 Новая кнопка быстрого доступа к приложениям, используемым в чате\n\n❤️ Поддержите нас финансово, чтобы мы сохранили независимость и продолжали вносить улучшения: %1$s</string>
|
||||
<string name="update_2_0">Что нового?\n\n💯 Сквозное шифрование стало постоянной и надёжной функцией. Значки замков 🔒 убраны!\n\n✉️ Обычные письма без сквозного шифрования помечены значком конверта\n\n😻 Обновлённый экран профиля для всех контактов\n\n🔲 Новая кнопка быстрого доступа к приложениям, используемым в чате\n\n❤️ Пожалуйста, сделайте пожертвование, чтобы помочь нам сохранить независимость и продолжать работу над улучшениями: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -705,10 +705,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Až do %1$s, väčšina obrázkov v horšej kvalite</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Až do %1$s, väčšina obrázkov vo vyváženej kvalite</string>
|
||||
<string name="download_failed">Sťahovanie zlyhalo</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s správa</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Maximálny počet stiahnutí k dispozícii do %1$s</string>
|
||||
<string name="profile_image_select">Vyberte profilový obrázok</string>
|
||||
<string name="select_your_new_profile_image">Vyberte svoj nový profilový obrázok</string>
|
||||
<string name="profile_image_delete">Odstrániť profilový obrázok</string>
|
||||
|
||||
@@ -809,10 +809,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Deri në %1$s, figura me cilësinë më të dobët</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Deri në %1$s, figurat me cilësinë më të baraspeshuar</string>
|
||||
<string name="download_failed">Shkarkimi dështoi</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Mesazh %1$s</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Shkarko maksimumin e mundshëm deri më %1$s</string>
|
||||
<string name="profile_image_select">Përzgjidhni Figurë Profili</string>
|
||||
<string name="select_your_new_profile_image">Përzgjidhni figurën tuaj të re të profilit</string>
|
||||
<string name="profile_image_delete">Fshini Figurën e Profilit</string>
|
||||
|
||||
@@ -582,10 +582,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Повећано на %1$s, најлошији квалитет слика</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Повећано на %1$s, уравнотежен квалитет слика</string>
|
||||
<string name="download_failed">Преузимање није успело</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s поруке</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Преузмите максимално доступно до %1$s</string>
|
||||
<string name="profile_image_select">Изабери слику за профил</string>
|
||||
<string name="select_your_new_profile_image">Изаберите нову слику за ваш профил</string>
|
||||
<string name="profile_image_delete">Избриши слику профила</string>
|
||||
|
||||
@@ -796,10 +796,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Upp till %1$s, sämst bildkvalitet</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Upp till %1$s, balanserad bildkvalitet</string>
|
||||
<string name="download_failed">Nerladdning misslyckades</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s meddelande</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Ladda ner max tillgängligt till %1$s</string>
|
||||
<string name="profile_image_select">Välj profilbild</string>
|
||||
<string name="select_your_new_profile_image">Välj din nya profilbild</string>
|
||||
<string name="profile_image_delete">Ta bort profilbild</string>
|
||||
|
||||
@@ -814,10 +814,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">%1$s kadar, en kötü kaliteli görseller</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">%1$s kadar, en dengeli kaliteli görseller</string>
|
||||
<string name="download_failed">İndirme başarısız</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s ileti</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">İndirme en fazla %1$s zamanına kadar kullanılabilir</string>
|
||||
<string name="profile_image_select">Profil Görseli Seç</string>
|
||||
<string name="select_your_new_profile_image">Yeni profil görselinizi seçin</string>
|
||||
<string name="profile_image_delete">Profil Görselini Sil</string>
|
||||
|
||||
@@ -801,10 +801,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">До %1$s, зображення найгіршої якості</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">До %1$s, найбільш збалансована якість зображень</string>
|
||||
<string name="download_failed">Не вдалося завантажити</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">Повідомлення розміром %1$s\'</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Доступний для завантаження максимум до %1$s</string>
|
||||
<string name="profile_image_select">Вибрати зображення профілю</string>
|
||||
<string name="select_your_new_profile_image">Вибрати ваше нове зображення профілю</string>
|
||||
<string name="profile_image_delete">Видалити зображення профілю</string>
|
||||
|
||||
@@ -573,10 +573,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Lên đến %1$s, hầu hết các hình ảnh có chất lượng kém hơn</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Lên đến %1$s, hình ảnh có chất lượng cân bằng nhất</string>
|
||||
<string name="download_failed">Tải xuống không thành công</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s tin nhắn</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Tải xuống tối đa có sẵn cho đến %1$s</string>
|
||||
<string name="profile_image_select">Chọn ảnh hồ sơ</string>
|
||||
<string name="select_your_new_profile_image">Chọn hình ảnh hồ sơ mới của bạn</string>
|
||||
<string name="profile_image_delete">Xóa ảnh hồ sơ</string>
|
||||
|
||||
@@ -652,6 +652,8 @@
|
||||
<string name="used_for_sending">用于发送</string>
|
||||
<string name="hide_from_contacts">对联系人隐藏</string>
|
||||
<string name="hidden_from_contacts">已对联系人隐藏</string>
|
||||
<!-- Hint for the list of relays -->
|
||||
<string name="transport_list_hint">在所有中继上均已收到消息。\n\n⚠️ 如果您在此处进行任何更改,请确保所有设备至少运行 2.47.0 版本。否则,较旧的设备可能会错过消息。</string>
|
||||
<!-- shown if a QR code was scanned that can be used as a relay -->
|
||||
<string name="confirm_add_transport">添加此中继?</string>
|
||||
<string name="invalid_transport_qr">扫描的二维码不包含有效的中继。</string>
|
||||
@@ -795,10 +797,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">%1$s 以下,最差的图像质量</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">%1$s 以下,均衡的图像质量</string>
|
||||
<string name="download_failed">下载失败</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s 消息</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">下载最晚于 %1$s 前可用</string>
|
||||
<string name="profile_image_select">选择个人资料图片</string>
|
||||
<string name="select_your_new_profile_image">选择新的个人资料图片</string>
|
||||
<string name="profile_image_delete">删除个人资料图片</string>
|
||||
|
||||
@@ -791,10 +791,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">%1$s以下,最差品質的圖像</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">%1$s以下,品質平衡的圖像</string>
|
||||
<string name="download_failed">下載失敗</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s 訊息</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">下載有效期至%1$s</string>
|
||||
<string name="profile_image_select">選擇大頭貼</string>
|
||||
<string name="select_your_new_profile_image">選擇新的大頭貼</string>
|
||||
<string name="profile_image_delete">刪除大頭貼</string>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<item>@string/share_location_for_one_hour</item>
|
||||
<item>@string/share_location_for_two_hours</item>
|
||||
<item>@string/share_location_for_six_hours</item>
|
||||
<item>@string/share_location_for_12_hours</item>
|
||||
<item>@string/share_location_for_24_hours</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="recipient_vibrate_entries">
|
||||
|
||||
@@ -66,10 +66,13 @@
|
||||
<string name="show_full_message">Show Full Message…</string>
|
||||
<!-- Stay short here, say ~16 characters. The source string could also be "All Read", maybe that hint can make translations easier :) -->
|
||||
<string name="mark_all_as_read">Mark All as Read</string>
|
||||
<!-- Used as an menu entry or button -->
|
||||
<string name="mark_as_read">Mark as Read</string>
|
||||
<!-- Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
|
||||
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
|
||||
<string name="mark_as_read_short">Mark Read</string>
|
||||
<!-- Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
|
||||
<!-- Used as an menu entry or button -->
|
||||
<string name="mark_as_unread">Mark as Unread</string>
|
||||
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
|
||||
<string name="mark_as_unread_short">Unread</string>
|
||||
<!-- Placeholder text when something is loading -->
|
||||
<string name="loading">Loading…</string>
|
||||
@@ -175,7 +178,9 @@
|
||||
<!-- "Stickers" as known from other messengers; in some languages, the English "Sticker" is fine. -->
|
||||
<string name="sticker">Sticker</string>
|
||||
<string name="add_to_sticker_collection">Add to Sticker Collection</string>
|
||||
<!-- deprecated, use sticker_picker_empty_hint instead -->
|
||||
<string name="add_stickers_instructions">To add stickers, tap "Open Sticker Folder", create a subfolder for your sticker pack, and drag image and sticker files there</string>
|
||||
<!-- deprecated -->
|
||||
<string name="open_sticker_folder">Open Sticker Folder</string>
|
||||
<string name="ask_add_sticker_to_collection">Add this sticker to your collection?</string>
|
||||
<string name="ask_delete_sticker">Delete this sticker?</string>
|
||||
@@ -416,6 +421,8 @@
|
||||
<string name="canceled_call">Canceled call</string>
|
||||
<string name="missed_call">Missed call</string>
|
||||
<string name="already_in_call">Already in a call</string>
|
||||
<string name="call_answered_elsewhere">Call answered on another device</string>
|
||||
<string name="call_requires_mic_permission">Microphone permission is required for calls</string>
|
||||
|
||||
<!-- get confirmations -->
|
||||
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
|
||||
@@ -814,10 +821,6 @@
|
||||
<string name="up_to_x_most_worse_quality_images">Up to %1$s, most worse quality images</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">Up to %1$s, most balanced quality images</string>
|
||||
<string name="download_failed">Download failed</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s message</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Download maximum available until %1$s</string>
|
||||
<string name="profile_image_select">Select Profile Image</string>
|
||||
<string name="select_your_new_profile_image">Select your new profile image</string>
|
||||
<string name="profile_image_delete">Delete Profile Image</string>
|
||||
@@ -1039,6 +1042,12 @@
|
||||
<string name="new_messages_body">You have new messages</string>
|
||||
<string name="n_messages_in_m_chats">%1$d messages in %2$d chats</string>
|
||||
|
||||
<!-- location streaming -->
|
||||
<string name="location_streaming_channel_desc">Channel for On-demand Location Streaming</string>
|
||||
<string name="location_streaming_notification_title">Location Streaming</string>
|
||||
<string name="location_streaming_notification_text">You are sharing your location</string>
|
||||
<string name="location_rationale">To share your live location with chat members, allow Delta Chat to use your location data.\n\nTo make live location work gaplessly, location data is used even when the app is closed or not in use.</string>
|
||||
|
||||
<!-- permissions -->
|
||||
<string name="perm_required_title">Permission required</string>
|
||||
<string name="perm_continue">Continue</string>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingTranslation">
|
||||
<string name="description">Description</string>
|
||||
<string name="could_not_open_file">Could not open file</string>
|
||||
<string name="menu_export_image">Export Image</string>
|
||||
<!-- the word "or" to separate blocks in the user interface that are mutually exclusive -->
|
||||
<string name="or_separator">or</string>
|
||||
<string name="online">online</string>
|
||||
<string name="pink">Pink</string>
|
||||
<string name="gray">Gray</string>
|
||||
<string name="share_location_for_12_hours">For 12 hours</string>
|
||||
<string name="share_location_for_24_hours">For 24 hours</string>
|
||||
|
||||
<!-- device messages for updates -->
|
||||
<!-- when translating the sentence about the ArcaneChat channel, clarify that the channel is in English -->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user