mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
236 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0e95a59d1 | |||
| d44ad7de0c | |||
| 3e0a283633 | |||
| ac01395482 | |||
| fc1adc4863 | |||
| d68c32ff26 | |||
| b67e9d795e | |||
| ae7be8a841 | |||
| 1894425ad2 | |||
| 3460e7e405 | |||
| ca40ae7869 | |||
| cb05abb85f | |||
| 8a76f99701 | |||
| fd5a3b9f8e | |||
| 6cab1d1cf4 | |||
| be8009c950 | |||
| f6c1fa2f5c | |||
| 6949be6fd8 | |||
| f95a6e8db7 | |||
| b81757d579 | |||
| 30c0026840 | |||
| efb4bf0536 | |||
| cd2fc0df0f | |||
| 51380cf906 | |||
| ded0c44ce3 | |||
| 61f5064929 | |||
| f316ebea20 | |||
| dc1a029e4a | |||
| 6bbacae54d | |||
| 972d372bd4 | |||
| fe7fb19adf | |||
| 5112389dae | |||
| c190a7b50a | |||
| 7ce8ccff78 | |||
| 7eb0a99fc7 | |||
| cbdd8f19ed | |||
| 5fdf319cda | |||
| 837e2313dd | |||
| e45fd2545e | |||
| 15537ea4b2 | |||
| df97f4069a | |||
| dea8b2bed9 | |||
| 0d130a4218 | |||
| b4ee89c1e8 | |||
| b2e7b8fdb6 | |||
| a494feb272 | |||
| 5efce4345a | |||
| b7703ce71f | |||
| c0ed32b645 | |||
| 7e4f408059 | |||
| a1993426de | |||
| bbb6ce8def | |||
| c601595477 | |||
| f909174119 | |||
| b0762a8b46 | |||
| 545f987f31 | |||
| b625c57d2d | |||
| 022d24bc5f | |||
| c5c09008a6 | |||
| 7679cc8bba | |||
| 4807d05861 | |||
| 34eedf5f07 | |||
| 941c186142 | |||
| b54ac50ea5 | |||
| b99426b7c6 | |||
| de1eedc63f | |||
| c3dac043cb | |||
| 00b52d4fe6 | |||
| b66bf595e2 | |||
| 7c24e836fe | |||
| 694f3bf013 | |||
| f6f42991e8 | |||
| 23f6803be9 | |||
| cdcb7fab4b | |||
| 5ee3607312 | |||
| 4b9642f51f | |||
| 4abbf3091f | |||
| ec4db3e58f | |||
| 0d2175b641 | |||
| a821ee363a | |||
| f43083acef | |||
| 7aed78a935 | |||
| 26c30fe3dc | |||
| 5550b8b1f5 | |||
| c94512fb24 | |||
| d1db6e5a8a | |||
| d352583237 | |||
| c8469be136 | |||
| 97620b8a3c | |||
| 24317b38bf | |||
| 6b28ed15e3 | |||
| b8b203e517 | |||
| e5c1e477f0 | |||
| 64752d3bae | |||
| 6145e0d2df | |||
| b365284743 | |||
| af62041c14 | |||
| 910dbf56fd | |||
| 4eca0dea4a | |||
| a76c17fd45 | |||
| b00aeec03a | |||
| ffe147fde8 | |||
| 02a5bb06a9 | |||
| a8997738bf | |||
| 202690d02e | |||
| 6d14bbdbc7 | |||
| b0699ab9be | |||
| 03a0b53eee | |||
| a044181a75 | |||
| e9ae9dc5bf | |||
| cbfa3b7e58 | |||
| 499f4aafc8 | |||
| bf57d3bd73 | |||
| 6200f354ca | |||
| c8dfb08dcb | |||
| 543e9d91e4 | |||
| dec5879919 | |||
| a1d21d8562 | |||
| 253ed877e5 | |||
| b809d24ab1 | |||
| 78aee0a487 | |||
| d16254c146 | |||
| 7ab3ef8453 | |||
| 4b695a3293 | |||
| da4c382c9a | |||
| 80973960f5 | |||
| 7be75f7008 | |||
| 6fa114c6c0 | |||
| 767b5f2bae | |||
| ed3a21b992 | |||
| 899c2b5647 | |||
| 61e616a53b | |||
| 3bf4504de9 | |||
| cd0740d895 | |||
| 9f99edc159 | |||
| 60568f23f5 | |||
| 6dc8bd7ba8 | |||
| ff4ecb3bda | |||
| c71e71359a | |||
| d5a09fa25e | |||
| f52c2be2c2 | |||
| 2233b93108 | |||
| d76eb3239a | |||
| a982bd2bbb | |||
| 9ef3edcc0e | |||
| de7c54b886 | |||
| 9c9f966597 | |||
| 12e77789d3 | |||
| bd640072ab | |||
| 0a9c46fbfc | |||
| 1cc4f11484 | |||
| 9386f2f9cb | |||
| 45d73d4604 | |||
| a5892330dd | |||
| 8a0e2d72a4 | |||
| 2629c65564 | |||
| 10694a6809 | |||
| aa3d177243 | |||
| 4d35d4edeb | |||
| bbaba3cd33 | |||
| d1f002a132 | |||
| 9ce9a91c95 | |||
| 7844e146b1 | |||
| eb997eca00 | |||
| 30124de2a8 | |||
| 1dcf7e4860 | |||
| 9ffc904ae5 | |||
| 2704749b44 | |||
| 1c174b5b70 | |||
| 8b5dd70d75 | |||
| f022316ad7 | |||
| 3386f5c5f7 | |||
| 57eead3a34 | |||
| 2868b51835 | |||
| 82118db71b | |||
| 14f55ca6b1 | |||
| cebfa12142 | |||
| 96c8c21b78 | |||
| 9ab1b1f3a7 | |||
| 94a0e426f8 | |||
| 006f8ae826 | |||
| 2889266522 | |||
| 5e6fccf143 | |||
| 3847e20d18 | |||
| fc69212a51 | |||
| 8999f54ba2 | |||
| f470e92300 | |||
| 3ac49e3e58 | |||
| 8dd9cfec5b | |||
| 0588214ee7 | |||
| 4ec49a031c | |||
| a31d7d6d3e | |||
| 64bbe9866d | |||
| 5d7ab84efc | |||
| 11f73a88e8 | |||
| dd0e847976 | |||
| 71ed333468 | |||
| 41d94ae3ee | |||
| b66bc1f863 | |||
| df7d80319c | |||
| 134145d166 | |||
| 688a103c10 | |||
| e2efa1f913 | |||
| aebd5c66f7 | |||
| 15c60c6b12 | |||
| f319ba2b83 | |||
| 93f12e7367 | |||
| 10acb07f82 | |||
| ee9a8dd53a | |||
| 0a55023bdb | |||
| fec9f8b4d3 | |||
| 13374df709 | |||
| 7fa04dc3c0 | |||
| 9d4e0e4e21 | |||
| 473c28ab07 | |||
| de47feac40 | |||
| 84894ff538 | |||
| f254c35749 | |||
| 1ff4e069ea | |||
| d574d33596 | |||
| f6756fc34b | |||
| 7c27eb47fc | |||
| 8d5a55c24c | |||
| 2e9aa79b02 | |||
| 1f9264225b | |||
| 18e145faaf | |||
| 47cf70120a | |||
| 34be7aab17 | |||
| 59139ed242 | |||
| 11f3964bdc | |||
| 7683408d18 | |||
| b74e793654 | |||
| 4bd74324d2 | |||
| 9a121b3039 | |||
| a9832c9c53 | |||
| 1e4c8bc291 |
@@ -8,7 +8,7 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
|
||||
- **Language:** Java (Java 8 compatibility)
|
||||
- **Build System:** Gradle with Android Gradle Plugin 8.11.1
|
||||
- **Min SDK:** 21 (Android 5.0)
|
||||
- **Target SDK:** 35 (Android 15)
|
||||
- **Target SDK:** 36 (Android 16)
|
||||
- **NDK Version:** 27.0.12077973
|
||||
- **Native Components:** Rust (deltachat-core-rust submodule)
|
||||
- **UI Framework:** Android SDK, Material Design Components
|
||||
@@ -17,13 +17,23 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
|
||||
## Repository Structure
|
||||
|
||||
- `src/main/` - Main application source code
|
||||
- `src/main/java/org/thoughtcrime/securesms/` - Main UI components
|
||||
- `src/main/java/com/b44t/messenger/` - Delta Chat core integration
|
||||
- `src/main/java/chat/delta/rpc/` - JSON-RPC bindings (generated, don't edit manually)
|
||||
- `src/main/res/` - Android resources (layouts, strings, drawables)
|
||||
- `src/androidTest/` - Instrumented tests (UI tests, benchmarks)
|
||||
- `src/androidTest/java/com/b44t/messenger/uitests/` - UI tests
|
||||
- `src/androidTest/java/com/b44t/messenger/uibenchmarks/` - Performance benchmarks
|
||||
- `src/gplay/` - Google Play flavor-specific code
|
||||
- `src/foss/` - F-Droid/FOSS flavor-specific code
|
||||
- `jni/deltachat-core-rust/` - Native Rust core library (submodule)
|
||||
- `jni/deltachat-core-rust/` - Native Rust core library (submodule, **don't edit directly**)
|
||||
- `scripts/` - Build and helper scripts
|
||||
- `scripts/ndk-make.sh` - Build native libraries
|
||||
- `scripts/install-toolchains.sh` - Install Rust cross-compilation toolchains
|
||||
- `scripts/generate-rpc-bindings.sh` - Generate JSON-RPC bindings
|
||||
- `docs/` - Documentation
|
||||
- `fastlane/` - App store metadata and screenshots
|
||||
- `.github/workflows/` - CI/CD workflows (GitHub Actions)
|
||||
|
||||
## Build Instructions
|
||||
|
||||
@@ -33,17 +43,36 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
This MUST be done first before any build attempts.
|
||||
|
||||
2. **Build native libraries:**
|
||||
2. **Set up environment variables:**
|
||||
```bash
|
||||
export ANDROID_NDK_ROOT=/path/to/ndk/27.0.12077973
|
||||
export PATH=${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/:${ANDROID_NDK_ROOT}
|
||||
```
|
||||
Note: Path format varies by OS (linux-x86_64, darwin-x86_64, etc.)
|
||||
|
||||
3. **Install Rust toolchains:**
|
||||
```bash
|
||||
scripts/install-toolchains.sh
|
||||
```
|
||||
Required for building the native Rust components.
|
||||
|
||||
4. **Build native libraries:**
|
||||
```bash
|
||||
scripts/ndk-make.sh
|
||||
```
|
||||
Note: First run may take significant time as it builds for all architectures (armeabi-v7a, arm64-v8a, x86, x86_64)
|
||||
**IMPORTANT:** First run takes 30-60 minutes as it builds for all architectures (armeabi-v7a, arm64-v8a, x86, x86_64).
|
||||
For faster development builds, build for a single architecture:
|
||||
```bash
|
||||
scripts/ndk-make.sh armeabi-v7a
|
||||
```
|
||||
|
||||
3. **Build APK:**
|
||||
5. **Build APK:**
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
Build time: ~2-5 minutes after native libraries are built.
|
||||
|
||||
### Build Flavors
|
||||
|
||||
@@ -55,6 +84,24 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
|
||||
- Debug APKs: `build/outputs/apk/gplay/debug/` and `build/outputs/apk/fat/debug/`
|
||||
- Release APKs require signing configuration in `~/.gradle/gradle.properties`
|
||||
|
||||
### Common Build Issues
|
||||
|
||||
1. **Missing NDK or incorrect version:**
|
||||
- Error: `ANDROID_NDK_ROOT not set` or native library missing
|
||||
- Solution: Install NDK 27.0.12077973 and set ANDROID_NDK_ROOT environment variable
|
||||
|
||||
2. **Submodules not initialized:**
|
||||
- Error: Missing deltachat-core-rust files
|
||||
- Solution: Run `git submodule update --init --recursive`
|
||||
|
||||
3. **Gradle wrapper validation:**
|
||||
- Always validate gradle wrapper before building: `./gradlew wrapper --gradle-version=current`
|
||||
- Wrapper is validated in CI via `gradle/actions/wrapper-validation@v4`
|
||||
|
||||
4. **Clean build issues:**
|
||||
- If build fails, try: `./gradlew clean && scripts/ndk-make.sh && ./gradlew assembleDebug`
|
||||
- Remove `build/` directory if clean doesn't work
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Unit Tests
|
||||
@@ -62,16 +109,19 @@ ArcaneChat is a Delta Chat Android client built on top of the official Delta Cha
|
||||
```bash
|
||||
./gradlew test
|
||||
```
|
||||
Expected duration: 1-3 minutes
|
||||
|
||||
### Running Instrumented Tests
|
||||
|
||||
1. **Disable animations** on your device/emulator:
|
||||
- Developer Options → Set "Window animation scale", "Transition animation scale", and "Animator duration scale" to 0x
|
||||
- **CRITICAL:** Tests will fail if animations are enabled
|
||||
|
||||
2. **Run tests:**
|
||||
```bash
|
||||
./gradlew connectedAndroidTest
|
||||
```
|
||||
Expected duration: 10-30 minutes depending on device/emulator
|
||||
|
||||
### Online Tests
|
||||
|
||||
@@ -129,6 +179,14 @@ TEST_MAIL_PW=yourpassword
|
||||
- Java bindings are in `src/main/java/com/b44t/messenger/Dc*.java`
|
||||
- JSON-RPC bindings in `chat.delta.rpc.*` package (generated via dcrpcgen)
|
||||
|
||||
### Generating JSON-RPC Bindings
|
||||
|
||||
To regenerate JSON-RPC bindings after core changes:
|
||||
```bash
|
||||
./scripts/generate-rpc-bindings.sh
|
||||
```
|
||||
**Note:** Requires Rust tooling and [dcrpcgen tool](https://github.com/chatmail/dcrpcgen) installed
|
||||
|
||||
### Working with Translations
|
||||
|
||||
- Translations managed via Transifex (not in repository)
|
||||
@@ -142,6 +200,43 @@ Decode crash symbols:
|
||||
$ANDROID_NDK_ROOT/ndk-stack --sym obj/local/armeabi-v7a --dump crash.txt > decoded.txt
|
||||
```
|
||||
|
||||
## Validation and Quality Checks
|
||||
|
||||
### Pre-commit Checks
|
||||
|
||||
Before committing changes, always run:
|
||||
|
||||
1. **Gradle wrapper validation:**
|
||||
```bash
|
||||
./gradlew wrapper --gradle-version=current
|
||||
```
|
||||
|
||||
2. **Build verification:**
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
3. **Unit tests:**
|
||||
```bash
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
4. **Code style:** Match existing code style in modified files (no automatic formatter configured)
|
||||
|
||||
### When to Rebuild Native Libraries
|
||||
|
||||
Rebuild native libraries (`scripts/ndk-make.sh`) when:
|
||||
- Updating deltachat-core-rust submodule
|
||||
- Modifying anything in `jni/` directory
|
||||
- Changing NDK version
|
||||
- After `git clean -fdx` or fresh clone
|
||||
|
||||
**DO NOT** rebuild native libraries for:
|
||||
- Pure Java/Kotlin code changes
|
||||
- Resource file changes
|
||||
- Gradle configuration changes (unless changing native library linking)
|
||||
- Documentation updates
|
||||
|
||||
## WebXDC Support
|
||||
|
||||
ArcaneChat has extended WebXDC support:
|
||||
@@ -152,12 +247,24 @@ ArcaneChat has extended WebXDC support:
|
||||
|
||||
## Important Files
|
||||
|
||||
- `build.gradle` - Main build configuration
|
||||
- `build.gradle` - Main build configuration (Android Gradle Plugin 8.11.1, Java 8 compatibility)
|
||||
- `CONTRIBUTING.md` - Contribution guidelines
|
||||
- `BUILDING.md` - Detailed build setup
|
||||
- `BUILDING.md` - Detailed build setup instructions
|
||||
- `RELEASE.md` - Release process
|
||||
- `proguard-rules.pro` - ProGuard configuration
|
||||
- `google-services.json` - Firebase configuration (gplay flavor)
|
||||
- `proguard-rules.pro` - ProGuard configuration (enabled for both debug and release)
|
||||
- `google-services.json` - Firebase configuration (gplay flavor only)
|
||||
- `settings.gradle` - Gradle settings
|
||||
- `.github/workflows/` - CI/CD configuration
|
||||
|
||||
## Dependencies and Constraints
|
||||
|
||||
- **Java Version:** Java 8 compatibility (do not use Java 9+ features)
|
||||
- **Gradle:** Use wrapper (`./gradlew`) to ensure correct Gradle version
|
||||
- **NDK:** Must use version 27.0.12077973 (specified in build.gradle)
|
||||
- **Min SDK:** 21 (Android 5.0) - code must be compatible
|
||||
- **Target SDK:** 36 (Android 16) - test on this API level when possible
|
||||
- **ProGuard:** Always enabled - ensure ProGuard rules are correct for new dependencies
|
||||
- **Multi-dex:** Enabled - app exceeds 65k method limit
|
||||
|
||||
## Package Structure
|
||||
|
||||
@@ -173,3 +280,35 @@ ArcaneChat has extended WebXDC support:
|
||||
- Native library must be rebuilt after core changes
|
||||
- ProGuard is enabled in both debug and release builds
|
||||
- Multi-dex is enabled due to app size
|
||||
|
||||
## CI/CD Workflows
|
||||
|
||||
### Preview APK Workflow (.github/workflows/preview-apk.yml)
|
||||
|
||||
Runs on every pull request to build and upload a preview APK:
|
||||
|
||||
1. **Setup steps:**
|
||||
- Checks out repository with submodules
|
||||
- Validates Fastlane metadata
|
||||
- Sets up Rust cache (working-directory: jni/deltachat-core-rust)
|
||||
- Sets up Java 17 (Temurin distribution)
|
||||
- Sets up Android SDK
|
||||
- Caches Gradle dependencies
|
||||
- Sets up NDK r27
|
||||
|
||||
2. **Build process:**
|
||||
```bash
|
||||
scripts/install-toolchains.sh && scripts/ndk-make.sh armeabi-v7a
|
||||
./gradlew --no-daemon -PABI_FILTER=armeabi-v7a assembleFossDebug
|
||||
```
|
||||
Note: Builds only armeabi-v7a for faster CI builds
|
||||
|
||||
3. **Output:** Uploads APK artifact to GitHub Actions
|
||||
|
||||
### Important CI Considerations
|
||||
|
||||
- Always validate Gradle wrapper before committing changes
|
||||
- Fastlane metadata must be valid (validated in CI)
|
||||
- Use `--no-daemon` flag for Gradle in CI environments
|
||||
- CI builds use FOSS flavor to avoid Google Services dependencies
|
||||
- Expected CI build time: 15-25 minutes for full workflow
|
||||
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/"
|
||||
scripts/install-toolchains.sh && scripts/ndk-make.sh armeabi-v7a
|
||||
scripts/install-toolchains.sh && scripts/ndk-make.sh arm64-v8a
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew --no-daemon -PABI_FILTER=armeabi-v7a assembleFossDebug
|
||||
run: ./gradlew --no-daemon -PABI_FILTER=arm64-v8a assembleFossDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+25
-1
@@ -2,6 +2,19 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Allow to set chat description
|
||||
* Unified date display in call bubbles
|
||||
* Explain at "Settings / Chats / Outgoing Media Quality" how to send original quality
|
||||
* Add a basic sticker picker
|
||||
* Leave groups and channels before deletion
|
||||
* Fix: keep original sent timestamp for resent messages
|
||||
* Fix: make clicking on broadcast member-added messages work always
|
||||
* Fix: remove notification when a message is deleted by sender
|
||||
* Update to core 2.44.0
|
||||
|
||||
## v2.43.0
|
||||
2026-02
|
||||
|
||||
* Improve switch speed when changing profiles
|
||||
* Allow to switch profile when sharing or forwarding
|
||||
* Display message views count for channel owners
|
||||
@@ -12,13 +25,24 @@
|
||||
* Mark external links with " ↗" to make them clear
|
||||
* Make QR code larger on "Add Second Device" screen
|
||||
* Add indication for blocked contacts in user profile
|
||||
* Allow to start calls with video disabled
|
||||
* Show hint for empty contact search results
|
||||
* Add background playing for voice messages and other audio files
|
||||
* Allow scanning Invitation Code when creating a new profile
|
||||
* Add context menu in long-pressing relays items instead of showing buttons
|
||||
* Enhanced video player UI
|
||||
* Fix: Show dialog if pasted QR codes are invalid
|
||||
* Fix: Refresh chat list when returning from conversation if selected profile changed
|
||||
* Fix: Update menu when using "select all" in contact selection
|
||||
* Fix: Avoid empty profiles after using "add as second device" from welcome screen
|
||||
* Fix: Remove from group deselected members in the contact selection list
|
||||
* Fix multi-device seen messages synchronization when using multiple relays
|
||||
* Update to core 2.39.0
|
||||
* Fix mailto handling
|
||||
* Fix layout problems inside in-chat apps
|
||||
* Fix real-time for in-chat apps that need it
|
||||
* Avoid crash when the app is minimized with profile switcher or reactions dialogs open
|
||||
* Remove "trash icon" option from contact selection list when adding members to group
|
||||
* Update to core 2.43.0
|
||||
|
||||
## v2.35.0
|
||||
2026-01
|
||||
|
||||
+8
-3
@@ -33,8 +33,8 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000736
|
||||
versionName "2.36.0"
|
||||
versionCode 30000737
|
||||
versionName "2.43.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
@@ -211,6 +211,8 @@ dependencies {
|
||||
implementation "io.noties.markwon:inline-parser:$markwon_version"
|
||||
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
|
||||
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
|
||||
implementation 'androidx.sharetarget:sharetarget:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
@@ -231,8 +233,11 @@ dependencies {
|
||||
implementation 'androidx.work:work-runtime:2.9.1'
|
||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
|
||||
implementation 'com.google.guava:guava:31.1-android'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$media3_version"
|
||||
implementation "androidx.media3:media3-session:$media3_version"
|
||||
implementation "androidx.media3:media3-ui:$media3_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
|
||||
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
|
||||
|
||||
+38
-2
@@ -153,6 +153,42 @@ static uint32_t* jintArray2uint32Pointer(JNIEnv* env, jintArray ja, int* ret_icn
|
||||
}
|
||||
|
||||
|
||||
/************************************************************
|
||||
* DcEventChannel
|
||||
************************************************************/
|
||||
|
||||
static dc_event_channel_t* get_dc_event_channel(JNIEnv *env, jobject obj)
|
||||
{
|
||||
static jfieldID fid = 0;
|
||||
if (fid==0) {
|
||||
jclass cls = (*env)->GetObjectClass(env, obj);
|
||||
fid = (*env)->GetFieldID(env, cls, "eventChannelCPtr", "J" /*Signature, J=long*/);
|
||||
}
|
||||
if (fid) {
|
||||
return (dc_event_channel_t*)(*env)->GetLongField(env, obj, fid);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_createEventChannelCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return (jlong)dc_event_channel_new();
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcEventChannel_unrefEventChannelCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
dc_event_channel_unref(get_dc_event_channel(env, obj));
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_getEventEmitterCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return (jlong)dc_event_channel_get_event_emitter(get_dc_event_channel(env, obj));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* DcAccounts
|
||||
******************************************************************************/
|
||||
@@ -172,11 +208,11 @@ static dc_accounts_t* get_dc_accounts(JNIEnv *env, jobject obj)
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir)
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir, jobject chanObj)
|
||||
{
|
||||
CHAR_REF(dir);
|
||||
int writable = 1;
|
||||
jlong accountsCPtr = (jlong)dc_accounts_new(dirPtr, writable);
|
||||
jlong accountsCPtr = (jlong)dc_accounts_new_with_event_channel(dirPtr, writable, get_dc_event_channel(env, chanObj));
|
||||
CHAR_UNREF(dir);
|
||||
return accountsCPtr;
|
||||
}
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: 8b2e88f45a...27dbe0bb6c
@@ -11,7 +11,6 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.espresso.NoMatchingViewException;
|
||||
import androidx.test.espresso.UiController;
|
||||
@@ -19,7 +18,6 @@ import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.util.TreeIterables;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -60,10 +58,12 @@ public class TestUtils {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ActivityScenarioRule<ConversationListActivity> getOfflineActivityRule(boolean useExistingChats) {
|
||||
public static ActivityScenarioRule<ConversationListActivity> getOfflineActivityRule(
|
||||
boolean useExistingChats) {
|
||||
Intent intent =
|
||||
Intent.makeMainActivity(
|
||||
new ComponentName(getInstrumentation().getTargetContext(), ConversationListActivity.class));
|
||||
Intent.makeMainActivity(
|
||||
new ComponentName(
|
||||
getInstrumentation().getTargetContext(), ConversationListActivity.class));
|
||||
if (!useExistingChats) {
|
||||
createOfflineAccount();
|
||||
}
|
||||
@@ -72,18 +72,22 @@ public class TestUtils {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T extends Activity> ActivityScenarioRule<T> getOnlineActivityRule(Class<T> activityClass) {
|
||||
public static <T extends Activity> ActivityScenarioRule<T> getOnlineActivityRule(
|
||||
Class<T> activityClass) {
|
||||
Context context = getInstrumentation().getTargetContext();
|
||||
AccountManager.getInstance().beginAccountCreation(context);
|
||||
prepare();
|
||||
return new ActivityScenarioRule<>(new Intent(getInstrumentation().getTargetContext(), activityClass));
|
||||
return new ActivityScenarioRule<>(
|
||||
new Intent(getInstrumentation().getTargetContext(), activityClass));
|
||||
}
|
||||
|
||||
private static void prepare() {
|
||||
Prefs.setBooleanPreference(getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true);
|
||||
Prefs.setBooleanPreference(
|
||||
getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true);
|
||||
if (!AccessibilityUtil.areAnimationsDisabled(getInstrumentation().getTargetContext())) {
|
||||
throw new RuntimeException("To run the tests, disable animations at Developer options' " +
|
||||
"-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'");
|
||||
throw new RuntimeException(
|
||||
"To run the tests, disable animations at Developer options' "
|
||||
+ "-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,26 +120,22 @@ public class TestUtils {
|
||||
}
|
||||
|
||||
throw new NoMatchingViewException.Builder()
|
||||
.withRootView(view)
|
||||
.withViewMatcher(matcher)
|
||||
.build();
|
||||
.withRootView(view)
|
||||
.withViewMatcher(matcher)
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform action of implicitly waiting for a certain view.
|
||||
* This differs from EspressoExtensions.searchFor in that,
|
||||
* upon failure to locate an element, it will fetch a new root view
|
||||
* in which to traverse searching for our @param match
|
||||
* Perform action of implicitly waiting for a certain view. This differs from
|
||||
* EspressoExtensions.searchFor in that, upon failure to locate an element, it will fetch a new
|
||||
* root view in which to traverse searching for our @param match
|
||||
*
|
||||
* @param viewMatcher ViewMatcher used to find our view
|
||||
*/
|
||||
public static ViewInteraction waitForView(
|
||||
Matcher<View> viewMatcher,
|
||||
int waitMillis,
|
||||
int waitMillisPerTry
|
||||
) {
|
||||
Matcher<View> viewMatcher, int waitMillis, int waitMillisPerTry) {
|
||||
|
||||
// Derive the max tries
|
||||
int maxTries = (int) (waitMillis / waitMillisPerTry);
|
||||
@@ -164,12 +164,11 @@ public class TestUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normally, you would do
|
||||
* onView(withId(R.id.send_button)).perform(click());
|
||||
* to send the draft message. However, in order to change the send button to the attach button
|
||||
* while there is no draft, the send button is made invisible and the attach button is made
|
||||
* visible instead. This confuses the test framework.<br/><br/>
|
||||
*
|
||||
* Normally, you would do onView(withId(R.id.send_button)).perform(click()); to send the draft
|
||||
* message. However, in order to change the send button to the attach button while there is no
|
||||
* draft, the send button is made invisible and the attach button is made visible instead. This
|
||||
* confuses the test framework.<br>
|
||||
* <br>
|
||||
* So, this is a workaround for pressing the send button.
|
||||
*/
|
||||
public static void pressSend() {
|
||||
|
||||
@@ -10,14 +10,11 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.b44t.messenger.TestUtils;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
@@ -34,18 +31,19 @@ public class EnterChatsBenchmark {
|
||||
// ==============================================================================================
|
||||
// Set this to true if you already have at least 10 chats on your existing DeltaChat installation
|
||||
// and want to traverse through them instead of 10 newly created chats
|
||||
private final static boolean USE_EXISTING_CHATS = false;
|
||||
private static final boolean USE_EXISTING_CHATS = false;
|
||||
// ==============================================================================================
|
||||
private final static int GO_THROUGH_ALL_CHATS_N_TIMES = 8;
|
||||
private static final int GO_THROUGH_ALL_CHATS_N_TIMES = 8;
|
||||
|
||||
// ==============================================================================================
|
||||
// PLEASE BACKUP YOUR ACCOUNT BEFORE RUNNING THIS!
|
||||
// ==============================================================================================
|
||||
|
||||
private final static String TAG = EnterChatsBenchmark.class.getSimpleName();
|
||||
private static final String TAG = EnterChatsBenchmark.class.getSimpleName();
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<ConversationListActivity> activityRule = TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS);
|
||||
public ActivityScenarioRule<ConversationListActivity> activityRule =
|
||||
TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS);
|
||||
|
||||
@Test
|
||||
public void createAndEnter10FilledChats() {
|
||||
@@ -55,7 +53,9 @@ public class EnterChatsBenchmark {
|
||||
for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) {
|
||||
times[i] = "" + timeGoToNChats(10); // 10 group chats were created
|
||||
}
|
||||
Log.i(TAG, "MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times));
|
||||
Log.i(
|
||||
TAG,
|
||||
"MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,13 +66,17 @@ public class EnterChatsBenchmark {
|
||||
for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) {
|
||||
times[i] = "" + timeGoToNChats(1);
|
||||
}
|
||||
Log.i(TAG, "MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: " + String.join(",", times));
|
||||
Log.i(
|
||||
TAG,
|
||||
"MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: "
|
||||
+ String.join(",", times));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enterFilledChat() {
|
||||
if (!USE_EXISTING_CHATS) {
|
||||
createChatAndGoBack("Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help");
|
||||
createChatAndGoBack(
|
||||
"Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help");
|
||||
}
|
||||
|
||||
String[] times = new String[50];
|
||||
@@ -82,7 +86,18 @@ public class EnterChatsBenchmark {
|
||||
long end = System.currentTimeMillis();
|
||||
long diff = end - start;
|
||||
pressBack();
|
||||
Log.i(TAG, "Measured (Benchmark) " + (i+1) + "/" + times.length + ": Entering 1 filled chat took " + diff + "ms " + "(going back took " + (System.currentTimeMillis() - end) + "ms)");
|
||||
Log.i(
|
||||
TAG,
|
||||
"Measured (Benchmark) "
|
||||
+ (i + 1)
|
||||
+ "/"
|
||||
+ times.length
|
||||
+ ": Entering 1 filled chat took "
|
||||
+ diff
|
||||
+ "ms "
|
||||
+ "(going back took "
|
||||
+ (System.currentTimeMillis() - end)
|
||||
+ "ms)");
|
||||
|
||||
times[i] = "" + diff;
|
||||
}
|
||||
@@ -91,16 +106,37 @@ public class EnterChatsBenchmark {
|
||||
|
||||
private void create10Chats(boolean fillWithMsgs) {
|
||||
if (!USE_EXISTING_CHATS) {
|
||||
createChatAndGoBack("Group #1", fillWithMsgs, "Hello!", "Some links: https://testrun.org", "And a command: /help");
|
||||
createChatAndGoBack("Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb");
|
||||
createChatAndGoBack("Group #3", fillWithMsgs, repeat("Some string ", 600), repeat("Another string", 200), "Hi!!!");
|
||||
createChatAndGoBack(
|
||||
"Group #1",
|
||||
fillWithMsgs,
|
||||
"Hello!",
|
||||
"Some links: https://testrun.org",
|
||||
"And a command: /help");
|
||||
createChatAndGoBack(
|
||||
"Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb");
|
||||
createChatAndGoBack(
|
||||
"Group #3",
|
||||
fillWithMsgs,
|
||||
repeat("Some string ", 600),
|
||||
repeat("Another string", 200),
|
||||
"Hi!!!");
|
||||
createChatAndGoBack("Group #4", fillWithMsgs, "xyzabc", "Hi!!!!", "Let's meet!");
|
||||
createChatAndGoBack("Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc");
|
||||
createChatAndGoBack("Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb");
|
||||
createChatAndGoBack("Group #7", fillWithMsgs, repeat("abcdefg ", 500), repeat("xxxxx", 100), "yrrrrrrrrrrrrr");
|
||||
createChatAndGoBack("Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!");
|
||||
createChatAndGoBack("Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc");
|
||||
createChatAndGoBack("Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd");
|
||||
createChatAndGoBack(
|
||||
"Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc");
|
||||
createChatAndGoBack(
|
||||
"Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb");
|
||||
createChatAndGoBack(
|
||||
"Group #7",
|
||||
fillWithMsgs,
|
||||
repeat("abcdefg ", 500),
|
||||
repeat("xxxxx", 100),
|
||||
"yrrrrrrrrrrrrr");
|
||||
createChatAndGoBack(
|
||||
"Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!");
|
||||
createChatAndGoBack(
|
||||
"Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc");
|
||||
createChatAndGoBack(
|
||||
"Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,10 +166,10 @@ public class EnterChatsBenchmark {
|
||||
onView(withContentDescription(R.string.group_create_button)).perform(click());
|
||||
|
||||
if (fillWithMsgs) {
|
||||
for (String t: texts) {
|
||||
for (String t : texts) {
|
||||
sendText(t);
|
||||
}
|
||||
for (String t: texts) {
|
||||
for (String t : texts) {
|
||||
sendText(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.TestUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
@@ -29,8 +28,6 @@ import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ForwardingTest {
|
||||
@@ -43,7 +40,8 @@ public class ForwardingTest {
|
||||
}
|
||||
|
||||
@Rule
|
||||
public final ActivityScenarioRule<ConversationListActivity> activityRule = TestUtils.getOfflineActivityRule(false);
|
||||
public final ActivityScenarioRule<ConversationListActivity> activityRule =
|
||||
TestUtils.getOfflineActivityRule(false);
|
||||
|
||||
@Before
|
||||
public void createChats() {
|
||||
@@ -51,10 +49,13 @@ public class ForwardingTest {
|
||||
dcContext.createChatByContactId(DcContact.DC_CONTACT_ID_SELF);
|
||||
// Disable bcc_self so that DC doesn't try to send messages to the server.
|
||||
// If we didn't do this, messages would stay in DC_STATE_OUT_PENDING forever.
|
||||
// The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't work
|
||||
// The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't
|
||||
// work
|
||||
// with animations, and the tests would hang and never finish.
|
||||
dcContext.setConfig("bcc_self", "0");
|
||||
activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group"));
|
||||
activityRule
|
||||
.getScenario()
|
||||
.onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group"));
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -68,7 +69,8 @@ public class ForwardingTest {
|
||||
// The group is at position 0, self chat is at position 1, device talk is at position 2
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
onView(withId(R.id.title)).check(matches(withText(R.string.device_talk)));
|
||||
onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(android.R.id.list))
|
||||
.perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.menu_context_forward)).perform(click());
|
||||
// Send it to self chat (which is sorted to the top because we're forwarding)
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
|
||||
@@ -77,11 +79,13 @@ public class ForwardingTest {
|
||||
|
||||
pressBack();
|
||||
|
||||
onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected)));
|
||||
onView(withId(R.id.toolbar_title))
|
||||
.check(matches(withText(R.string.connectivity_not_connected)));
|
||||
// Self chat moved up because we sent a message there
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
|
||||
onView(withId(R.id.title)).check(matches(withText(R.string.saved_messages)));
|
||||
onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(android.R.id.list))
|
||||
.perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.menu_context_forward)).perform(click());
|
||||
// Send it to the group
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
@@ -89,6 +93,7 @@ public class ForwardingTest {
|
||||
onView(withId(R.id.title)).check(matches(withText("group")));
|
||||
|
||||
pressBack();
|
||||
onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected)));
|
||||
onView(withId(R.id.toolbar_title))
|
||||
.check(matches(withText(R.string.connectivity_not_connected)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.TestUtils;
|
||||
|
||||
import java.io.File;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -34,9 +32,6 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class SharingTest {
|
||||
@@ -48,26 +43,34 @@ public class SharingTest {
|
||||
private static int createdSingleChatId;
|
||||
|
||||
@Rule
|
||||
public final ActivityScenarioRule<ConversationListActivity> activityRule = TestUtils.getOfflineActivityRule(false);
|
||||
public final ActivityScenarioRule<ConversationListActivity> activityRule =
|
||||
TestUtils.getOfflineActivityRule(false);
|
||||
|
||||
@Before
|
||||
public void createGroup() {
|
||||
activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group"));
|
||||
activityRule
|
||||
.getScenario()
|
||||
.onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group"));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void createSingleChat() {
|
||||
activityRule.getScenario().onActivity(a -> {
|
||||
int contactId = DcHelper.getContext(a).createContact("", "abc@example.org");
|
||||
createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId);
|
||||
});
|
||||
activityRule
|
||||
.getScenario()
|
||||
.onActivity(
|
||||
a -> {
|
||||
int contactId = DcHelper.getContext(a).createContact("", "abc@example.org");
|
||||
createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalSharing() {
|
||||
Intent i = new Intent(Intent.ACTION_SEND);
|
||||
i.putExtra(Intent.EXTRA_TEXT, "Hello!");
|
||||
i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
i.setComponent(
|
||||
new ComponentName(
|
||||
getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
activityRule.getScenario().onActivity(a -> a.startActivity(i));
|
||||
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
|
||||
@@ -77,9 +80,9 @@ public class SharingTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test direct sharing from a screenshot.
|
||||
* Also, this is the regression test for https://github.com/deltachat/deltachat-android/issues/2040
|
||||
* where network changes during sharing lead to a bug
|
||||
* Test direct sharing from a screenshot. Also, this is the regression test for
|
||||
* https://github.com/deltachat/deltachat-android/issues/2040 where network changes during sharing
|
||||
* lead to a bug
|
||||
*/
|
||||
@Test
|
||||
public void testShareFromScreenshot() {
|
||||
@@ -92,20 +95,25 @@ public class SharingTest {
|
||||
pngImage = file;
|
||||
}
|
||||
}
|
||||
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage));
|
||||
Uri uri =
|
||||
Uri.parse(
|
||||
"content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage));
|
||||
DcHelper.sharedFiles.put(pngImage, "image/png");
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_SEND);
|
||||
i.setType("image/png");
|
||||
i.putExtra(Intent.EXTRA_SUBJECT, "Screenshot (Sep 27, 2021 00:00:00");
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
|
||||
i.setFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
|
||||
| Intent.FLAG_ACTIVITY_FORWARD_RESULT
|
||||
| Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
|
||||
| Intent.FLAG_RECEIVER_FOREGROUND
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
i.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
i.putExtra(ShareActivity.EXTRA_CHAT_ID, createdGroupId);
|
||||
i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
i.setComponent(
|
||||
new ComponentName(
|
||||
getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
activityRule.getScenario().onActivity(a -> a.startActivity(i));
|
||||
|
||||
TestUtils.waitForView(withId(R.id.send_button), 10000, 50);
|
||||
@@ -121,16 +129,32 @@ public class SharingTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links:
|
||||
* Tests
|
||||
* https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links:
|
||||
*
|
||||
* <ul dir="auto">
|
||||
* <li><a href="mailto:abc@example.org">Just an email address</a> - should open a chat with <code>abc@example.org</code> (and maybe ask whether a chat should be created if it does not exist already)</li>
|
||||
* <li><a href="mailto:abc@example.org?subject=testing%20mailto%20uris">email address with subject</a> - should open a chat with <code>abc@example.org</code> and fill <code>testing mailto uris</code>; as we created the chat in the previous step, it should not ask <code>Chat with …</code> but directly open the chat</li>
|
||||
* <li><a href="mailto:abc@example.org?body=this%20is%20a%20test">email address with body</a> - should open a chat with <code>abc@example.org</code>, draft <code>this is a test</code></li>
|
||||
* <li><a href="mailto:abc@example.org?subject=testing%20mailto%20uris&body=this%20is%20a%20test">email address with subject and body</a> - should open a chat with <code>abc@example.org</code>, draft <code>testing mailto uris</code> <newline> <code>this is a test</code></li>
|
||||
* <li><a href="mailto:%20info@example.org">HTML encoding</a> - should open a chat with <code>info@example.org</code></li>
|
||||
* <li><a href="mailto:simplebot@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it">more HTML encoding</a> - should open a chat with <code>simplebot@example.org</code>, draft <code>!web https://duckduckgo.com/lite?q=duck%20it</code></li>
|
||||
* <li><a href="mailto:?subject=bla&body=blub">no email, just subject&body</a> - this should let you choose a chat and create a draft <code>bla</code> <newline> <code>blub</code> there</li>
|
||||
* <li><a href="mailto:abc@example.org">Just an email address</a> - should open a chat with
|
||||
* <code>abc@example.org</code> (and maybe ask whether a chat should be created if it does
|
||||
* not exist already)
|
||||
* <li><a href="mailto:abc@example.org?subject=testing%20mailto%20uris">email address with
|
||||
* subject</a> - should open a chat with <code>abc@example.org</code> and fill <code>
|
||||
* testing mailto uris</code>; as we created the chat in the previous step, it should not
|
||||
* ask <code>Chat with …</code> but directly open the chat
|
||||
* <li><a href="mailto:abc@example.org?body=this%20is%20a%20test">email address with body</a> -
|
||||
* should open a chat with <code>abc@example.org</code>, draft <code>this is a test</code>
|
||||
* <li><a
|
||||
* href="mailto:abc@example.org?subject=testing%20mailto%20uris&body=this%20is%20a%20test">email
|
||||
* address with subject and body</a> - should open a chat with <code>abc@example.org</code>,
|
||||
* draft <code>testing mailto uris</code> <newline> <code>this is a test</code>
|
||||
* <li><a href="mailto:%20info@example.org">HTML encoding</a> - should open a chat with <code>
|
||||
* info@example.org</code>
|
||||
* <li><a
|
||||
* href="mailto:simplebot@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it">more
|
||||
* HTML encoding</a> - should open a chat with <code>simplebot@example.org</code>, draft
|
||||
* <code>!web https://duckduckgo.com/lite?q=duck%20it</code>
|
||||
* <li><a href="mailto:?subject=bla&body=blub">no email, just subject&body</a> - this
|
||||
* should let you choose a chat and create a draft <code>bla</code> <newline> <code>
|
||||
* blub</code> there
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
@@ -140,7 +164,8 @@ public class SharingTest {
|
||||
|
||||
openLink("mailto:abc@example.org?subject=testing%20mailto%20uris");
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris")));
|
||||
onView(withHint(R.string.chat_input_placeholder))
|
||||
.check(matches(withText("testing mailto uris")));
|
||||
|
||||
openLink("mailto:abc@example.org?body=this%20is%20a%20test");
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
@@ -148,17 +173,22 @@ public class SharingTest {
|
||||
|
||||
openLink("mailto:abc@example.org?subject=testing%20mailto%20uris&body=this%20is%20a%20test");
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris\nthis is a test")));
|
||||
onView(withHint(R.string.chat_input_placeholder))
|
||||
.check(matches(withText("testing mailto uris\nthis is a test")));
|
||||
|
||||
openLink("mailto:%20abc@example.org");
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
|
||||
openLink("mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it");
|
||||
openLink(
|
||||
"mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it");
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it")));
|
||||
onView(withHint(R.string.chat_input_placeholder))
|
||||
.check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it")));
|
||||
|
||||
openLink("mailto:?subject=bla&body=blub");
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
onView(withId(R.id.list))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org")));
|
||||
onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("bla\nblub")));
|
||||
}
|
||||
@@ -170,49 +200,61 @@ public class SharingTest {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* <ul dir="auto">
|
||||
* <li>Open Saved Messages chat (could be any other chat too)</li>
|
||||
* <li>Go to another app and share some text to DC</li>
|
||||
* <li>In DC select Saved Messages. Edit the shared text if you like. <em>Don't</em> hit the Send button.</li>
|
||||
* <li>Leave DC</li>
|
||||
* <li>Open DC again from the "Recent apps"</li>
|
||||
* <li>Check that your draft is still there</li>
|
||||
* <li>Open Saved Messages chat (could be any other chat too)
|
||||
* <li>Go to another app and share some text to DC
|
||||
* <li>In DC select Saved Messages. Edit the shared text if you like. <em>Don't</em> hit the
|
||||
* Send button.
|
||||
* <li>Leave DC
|
||||
* <li>Open DC again from the "Recent apps"
|
||||
* <li>Check that your draft is still there
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
public void testOpenAgainFromRecents() {
|
||||
// Open a chat
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
onView(withId(R.id.list))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
|
||||
// Share some text to DC
|
||||
Intent i = new Intent(Intent.ACTION_SEND);
|
||||
i.putExtra(Intent.EXTRA_TEXT, "Veeery important draft");
|
||||
i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
i.setComponent(
|
||||
new ComponentName(
|
||||
getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class));
|
||||
activityRule.getScenario().onActivity(a -> a.startActivity(i));
|
||||
|
||||
// In DC, select the same chat you opened before
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
onView(withId(R.id.list))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
|
||||
// Leave DC and go back to the previous activity
|
||||
pressBack();
|
||||
|
||||
// Here, we can't exactly replicate the "steps to reproduce". Previously, the other activity
|
||||
// stayed open in the background, but since it doesn't anymore, we need to open it again:
|
||||
onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
onView(withId(R.id.list))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click()));
|
||||
|
||||
// Check that the draft is still there
|
||||
// Util.sleep(2000); // Uncomment for debugging
|
||||
onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("Veeery important draft")));
|
||||
onView(withHint(R.string.chat_input_placeholder))
|
||||
.check(matches(withText("Veeery important draft")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test:
|
||||
*
|
||||
* If you save your contacts's emails in the contacts app of the phone, there are buttons to call
|
||||
* them and also to write an email to them.
|
||||
* <p>If you save your contacts's emails in the contacts app of the phone, there are buttons to
|
||||
* call them and also to write an email to them.
|
||||
*
|
||||
* If you click the email button, ArcaneChat opened but instead of opening a chat with that contact,
|
||||
* the chat list was show and "share with" was displayed at the top
|
||||
* <p>If you click the email button, Delta Chat opened but instead of opening a chat with that
|
||||
* contact, the chat list was show and "share with" was displayed at the top
|
||||
*/
|
||||
@Test
|
||||
public void testOpenChatFromContacts() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.b44t.messenger.uitests.online;
|
||||
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
@@ -11,13 +10,10 @@ import static androidx.test.espresso.matcher.ViewMatchers.withHint;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.b44t.messenger.TestUtils;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -30,14 +26,16 @@ import org.thoughtcrime.securesms.WelcomeActivity;
|
||||
@LargeTest
|
||||
public class OnboardingTest {
|
||||
@Rule
|
||||
public ActivityScenarioRule<WelcomeActivity> activityRule = TestUtils.getOnlineActivityRule(WelcomeActivity.class);
|
||||
public ActivityScenarioRule<WelcomeActivity> activityRule =
|
||||
TestUtils.getOnlineActivityRule(WelcomeActivity.class);
|
||||
|
||||
@Test
|
||||
public void testAccountCreation() {
|
||||
if (TextUtils.isEmpty(BuildConfig.TEST_ADDR) || TextUtils.isEmpty(BuildConfig.TEST_MAIL_PW)) {
|
||||
throw new RuntimeException("You need to set TEST_ADDR and TEST_MAIL_PW; " +
|
||||
"either in gradle.properties or via an environment variable. " +
|
||||
"See README.md for more details.");
|
||||
throw new RuntimeException(
|
||||
"You need to set TEST_ADDR and TEST_MAIL_PW; "
|
||||
+ "either in gradle.properties or via an environment variable. "
|
||||
+ "See README.md for more details.");
|
||||
}
|
||||
onView(withText(R.string.scan_invitation_code)).check(matches(isClickable()));
|
||||
onView(withText(R.string.import_backup_title)).check(matches(isClickable()));
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="228"
|
||||
android:viewportHeight="280">
|
||||
<group
|
||||
android:scaleX="0.35014287"
|
||||
android:scaleY="0.43"
|
||||
android:translateX="74.08372"
|
||||
android:translateY="79.8">
|
||||
<path
|
||||
android:pathData="m10.03,234.14c0.3,-0.01 0.6,-0.02 0.9,-0.04 -0.07,-0.49 -0.14,-0.97 -0.2,-1.46 -0.15,-1.34 -0.26,-2.69 -0.25,-4.04 -0.02,-0.86 -0.05,-1.71 -0.07,-2.57 -0.09,-0.06 -0.18,-0.13 -0.27,-0.19 -0.02,-0.02 -0.04,-0.03 -0.07,-0.05zM44.87,232.95c11.71,-8.35 26.86,-14.79 46.21,-15.9 0,0 39.93,-0.27 47.91,-3.53 7.98,-3.26 68.68,-14.69 82.94,-98.43 14.26,-83.74 -1.06,-115.09 -1.06,-115.09 0,0 -21.14,55.68 -81.02,59.81 0,0 -14.5,1.03 -38.82,1.42 -24.32,0.39 -75.77,20.65 -90.55,85.62l-0.22,43.44c2.5,4.22 5.49,8.12 8.91,11.66 3.99,4.11 8.11,8.12 12.79,11.45 2.26,1.65 4.65,3.2 6.51,5.33 1.94,2.34 3.33,5 4.2,7.93 0.71,2.1 1.45,4.2 2.2,6.28z"
|
||||
android:fillColor="@color/ic_launcher_background" />
|
||||
<path
|
||||
android:pathData="m217.97,45.86c-0.3,0.01 -0.6,0.02 -0.9,0.04 0.07,0.49 0.14,0.97 0.2,1.46 0.15,1.34 0.26,2.69 0.25,4.04 0.02,0.86 0.05,1.71 0.07,2.57 0.09,0.06 0.18,0.13 0.27,0.19 0.02,0.02 0.04,0.03 0.07,0.05zM183.13,47.05c-11.71,8.35 -26.86,14.79 -46.21,15.9 0,0 -39.93,0.27 -47.91,3.53 -7.98,3.26 -68.68,14.69 -82.94,98.43 -14.26,83.74 1.06,115.09 1.06,115.09 0,0 21.14,-55.68 81.02,-59.81 0,0 14.5,-1.03 38.82,-1.42 24.32,-0.39 75.77,-20.65 90.55,-85.62l0.22,-43.44c-2.5,-4.22 -5.49,-8.12 -8.91,-11.66 -3.99,-4.11 -8.11,-8.12 -12.79,-11.45 -2.26,-1.65 -4.65,-3.2 -6.51,-5.33 -1.94,-2.34 -3.33,-5 -4.2,-7.93 -0.71,-2.1 -1.45,-4.2 -2.2,-6.28z"
|
||||
android:fillColor="@color/ic_launcher_background" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/white" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,14 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".connect.KeepAliveService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true" />
|
||||
</application>
|
||||
<application>
|
||||
<service
|
||||
android:name=".connect.KeepAliveService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.notifications;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/*
|
||||
@@ -10,6 +9,11 @@ import androidx.annotation.Nullable;
|
||||
*/
|
||||
public class FcmReceiveService {
|
||||
public static void register(Context context) {}
|
||||
|
||||
public static void waitForRegisterFinished() {}
|
||||
@Nullable public static String getToken() { return null; }
|
||||
|
||||
@Nullable
|
||||
public static String getToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- force compiling emojipicker on sdk<21 and firebase on sdk<19; runtime checks are required then -->
|
||||
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker, com.google.firebase.messaging, com.google.android.gms.cloudmessaging"/>
|
||||
<!-- force compiling emojipicker on sdk<21 and firebase on sdk<19; runtime checks are required then -->
|
||||
<uses-sdk
|
||||
tools:overrideLibrary="androidx.emoji2.emojipicker, com.google.firebase.messaging, com.google.android.gms.cloudmessaging" />
|
||||
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".connect.KeepAliveService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:enabled="true" />
|
||||
<application>
|
||||
<service
|
||||
android:name=".connect.KeepAliveService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:enabled="true" />
|
||||
|
||||
<service
|
||||
android:name=".notifications.FcmReceiveService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".notifications.FcmReceiveService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
</application>
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package org.thoughtcrime.securesms.notifications;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.android.gms.tasks.Tasks;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.service.FetchForegroundService;
|
||||
@@ -35,36 +31,38 @@ public class FcmReceiveService extends FirebaseMessagingService {
|
||||
return;
|
||||
}
|
||||
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
final String rawToken;
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
final String rawToken;
|
||||
|
||||
try {
|
||||
synchronized (INIT_LOCK) {
|
||||
if (!initialized) {
|
||||
// manual init: read tokens from `./google-services.json`;
|
||||
// automatic init disabled in AndroidManifest.xml to skip FCM code completely.
|
||||
FirebaseApp.initializeApp(context);
|
||||
try {
|
||||
synchronized (INIT_LOCK) {
|
||||
if (!initialized) {
|
||||
// manual init: read tokens from `./google-services.json`;
|
||||
// automatic init disabled in AndroidManifest.xml to skip FCM code completely.
|
||||
FirebaseApp.initializeApp(context);
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
rawToken = Tasks.await(FirebaseMessaging.getInstance().getToken());
|
||||
} catch (Exception e) {
|
||||
// we're here usually when FCM is not available and initializeApp() or getToken()
|
||||
// failed.
|
||||
Log.w(TAG, "cannot get FCM token for " + BuildConfig.APPLICATION_ID + ": " + e);
|
||||
triedRegistering = true;
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(rawToken)) {
|
||||
Log.w(TAG, "got empty FCM token for " + BuildConfig.APPLICATION_ID);
|
||||
triedRegistering = true;
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
rawToken = Tasks.await(FirebaseMessaging.getInstance().getToken());
|
||||
} catch (Exception e) {
|
||||
// we're here usually when FCM is not available and initializeApp() or getToken() failed.
|
||||
Log.w(TAG, "cannot get FCM token for " + BuildConfig.APPLICATION_ID + ": " + e);
|
||||
triedRegistering = true;
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(rawToken)) {
|
||||
Log.w(TAG, "got empty FCM token for " + BuildConfig.APPLICATION_ID);
|
||||
triedRegistering = true;
|
||||
return;
|
||||
}
|
||||
|
||||
prefixedToken = addPrefix(rawToken);
|
||||
Log.i(TAG, "FCM token: " + prefixedToken);
|
||||
ApplicationContext.getDcAccounts().setPushDeviceToken(prefixedToken);
|
||||
triedRegistering = true;
|
||||
});
|
||||
prefixedToken = addPrefix(rawToken);
|
||||
Log.i(TAG, "FCM token: " + prefixedToken);
|
||||
ApplicationContext.getDcAccounts().setPushDeviceToken(prefixedToken);
|
||||
triedRegistering = true;
|
||||
});
|
||||
}
|
||||
|
||||
// wait a until FCM registration got a token or not.
|
||||
@@ -98,7 +96,8 @@ public class FcmReceiveService extends FirebaseMessagingService {
|
||||
// ForegroundServiceStartNotAllowedException.
|
||||
// So, it's recommended to check the result of RemoteMessage.getPriority() and
|
||||
// confirm it's PRIORITY_HIGH() before attempting to start a foreground service.
|
||||
// source: https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start
|
||||
// source:
|
||||
// https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start
|
||||
if (remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH) {
|
||||
FetchForegroundService.start(this);
|
||||
} else {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
|
||||
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
|
||||
@@ -92,6 +93,7 @@
|
||||
<intent-filter>
|
||||
<data android:scheme="mailto"/>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
@@ -390,6 +392,15 @@
|
||||
android:name=".service.FetchForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.AudioPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1045,31 +1045,31 @@ für die <a href="https://chatmail.at/clients">Chatmail-Clients</a> umfasst, von
|
||||
|
||||
</h3>
|
||||
|
||||
<p>We would like to improve Delta Chat with your help,
|
||||
which is why Delta Chat for Android asks whether you want
|
||||
to send anonymous usage statistics.</p>
|
||||
<p>Wir möchten Delta Chat mit deiner Hilfe verbessern.
|
||||
Deshalb fragt Delta Chat für Android, ob du
|
||||
anonyme Nutzungsstatistiken senden möchtest.</p>
|
||||
|
||||
<p>You can turn it on and off at
|
||||
<strong>Settings → Advanced → Send statistics to Delta Chat’s developers</strong>.</p>
|
||||
<p>Du kannst dies unter
|
||||
<strong>Einstellungen → Erweitert → Statistik an Delta Chat Entwickler senden</strong> ein- und ausschalten.</p>
|
||||
|
||||
<p>When you turn it on,
|
||||
weekly statistics will be automatically sent to a bot.</p>
|
||||
<p>Wenn eingeschaltet,
|
||||
werden wöchentlich Statistiken automatisch an einen Bot gesendet.</p>
|
||||
|
||||
<p>We are interested e.g. in statistics like:</p>
|
||||
<p>Wir sind beispielsweise an folgenden Statistiken interessiert:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>How many contacts are introduced by personally scanning a QR code?</p>
|
||||
<p>Wie viele Kontakte werden durch das persönliche Scannen eines QR-Codes hergestellt?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Which versions of Delta Chat are being used?</p>
|
||||
<p>Welche Versionen von Delta Chat werden verwendet?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>What errors occur for users?</p>
|
||||
<p>Welche Fehler treten bei Benutzern auf?</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>We will <em>not</em> collect any personally identifiable information about you.</p>
|
||||
<p>Wir werden <em>keinerlei</em> personenbezogene Daten über dich sammeln.</p>
|
||||
|
||||
<h3 id="ich-bin-an-technischen-details-interessiert-gibt-es-hierzu-weitere-infos">
|
||||
|
||||
@@ -1107,6 +1107,10 @@ zum Austausch von Verschlüsselungsinformationen durch Scannen von QR-Codes oder
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> wird verwendet, um automatisch eine Ende-zu-Ende-Verschlüsselung zwischen Kontakten und allen Mitgliedern einer Gruppe herzustellen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wir post-quantum-resistente Verschlüsselung und Forward Secrecy einführen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Teilen eines Kontakts im Chat</a>
|
||||
ermöglicht es den Empfängern, eine Ende-zu-Ende-Verschlüsselung mit dem Kontakt zu verwenden.</p>
|
||||
@@ -1280,15 +1284,13 @@ selbst wenn die Nachricht nicht Ende-zu-Ende-verschlüsselt war.</p>
|
||||
speichern Delta-Chat-Apps keine Metadaten über Kontakte oder Gruppen auf Servern. Auch nicht in verschlüsselter Form.
|
||||
Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und ausschließlich auf den Endgeräten der Nutzer gespeichert.</p>
|
||||
|
||||
<p>Servers can therefore only see:</p>
|
||||
<p>Server können daher nur das folgende sehen:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Absender- und Empfängeradressen, standardmäßig zufällig generiert</li>
|
||||
<li>Größe der Nachricht</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>Alle anderen Metadaten zu Nachrichten, Kontakten und Gruppen befinden sich im Ende-zu-Ende-verschlüsselten Teil der Nachrichten.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1299,15 +1301,15 @@ Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und aussch
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Both for protecting against metadata-collecting servers
|
||||
as well as against the threat of device seizure
|
||||
we recommend to use a <a href="https://chatmail.at/relays">chatmail relay</a>
|
||||
to create chat profiles using random addresses for transport.
|
||||
Note that Delta Chat apps on all platforms support multiple profiles
|
||||
so you can easily use situation-specific profiles next to your “main” profile
|
||||
with the knowledge that all their data, along with all metadata, will be deleted.
|
||||
Moreover, if a device is seized then chat contacts using short-lived profiles
|
||||
can not be identified easily.</p>
|
||||
<p>Sowohl zum Schutz vor Servern, die Metadaten sammeln,
|
||||
als auch als Schutz bei Beschlagnahmung von Geräten
|
||||
empfehlen wir die Verwendung eines <a href="https://chatmail.at/relays">Chatmail-Relays</a>,
|
||||
um Chat-Profile mit zufälligen Adressen für den Transport zu erstellen.
|
||||
Beachte, dass Delta-Chat-Apps mehrere Profile unterstützen,
|
||||
sodass du neben deinem „Hauptprofil” ganz einfach situationsspezifische Profile verwenden kannst,
|
||||
mit der Gewissheit, dass alle Daten sowie alle Metadaten gelöscht werden.
|
||||
Darüber hinaus können Chat-Kontakte, die kurzlebige Profile verwenden,
|
||||
im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert werden.</p>
|
||||
|
||||
<h3 id="wer-sieht-meine-ip-adresse">
|
||||
|
||||
@@ -1350,11 +1352,10 @@ um seine Serverinfrastruktur darüber im Unklaren zu lassen, wer eine Nachricht
|
||||
Dies ist besonders wichtig, weil der Signal-Server die Handynummer jedes Kontos kennt,
|
||||
die in der Regel mit einer Passidentität verbunden ist.</p>
|
||||
|
||||
<p>Even if <a href="https://chatmail.at/relays">chatmail relays</a>
|
||||
do not ask for any private data (including no phone numbers),
|
||||
it might still be worthwhile to protect relational metadata between addresses.
|
||||
We don’t foresee bigger problems in using random throw-away addresses for sealed sending
|
||||
but an implementation has not been agreed as a priority yet.</p>
|
||||
<p>Auch wenn <a href="https://chatmail.at/relays">Chatmail-Relays</a>
|
||||
keine privaten Daten (einschließlich Telefonnummern) abfragen,
|
||||
könnte es dennoch sinnvoll sein, Metadaten zwischen Adressen zu schützen.
|
||||
Wir sehen keine größeren Probleme bei der Verwendung von zufälligen Wegwerfadressen für aber eine Umsetzung wurde noch nicht als priorisiert.</p>
|
||||
|
||||
<h3 id="pfs">
|
||||
|
||||
@@ -1364,23 +1365,20 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nein, noch nicht.</p>
|
||||
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
and someone has collected your prior in-transit messages,
|
||||
they will be able to decrypt and read them using the leaked decryption key.
|
||||
Note that Forward Secrecy only increases security if you delete messages.
|
||||
Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
<p>Delta Chat unterstützt derzeit keine Perfect Forward Secrecy (PFS).
|
||||
Das bedeutet, dass, wenn Ihr privater Schlüssel offengelegt wird
|
||||
und jemand Ihre früheren Nachrichten während der Übertragung gesammelt hat,
|
||||
diese mit dem offengelegten Schlüssel entschlüsselt und gelesen werden können.
|
||||
Beachten Sie, dass Forward Secrecy die Sicherheit nur erhöht, wenn du Nachrichten löschst.
|
||||
Andernfalls kann jemand, der deinen Schlüssel erhält,
|
||||
in der Regel auch alle deine nicht gelöschten Nachrichten abrufen
|
||||
und muss zuvor gesammelte Nachrichten nicht einmal entschlüsseln.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wird durch automatische Schlüsselrotation eine zuverlässige Löschung (Forward Secrecy) gewährleisten.
|
||||
Dieser Ansatz ist im Entwurf <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1390,11 +1388,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nein, noch nicht.</p>
|
||||
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat verwendet die Rust OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP Entwurf</a> unterstützt.
|
||||
Wir beabsichtigen, PQC-Unterstützung zum <a href="https://github.com/chatmail/core">chatmail core</a> hinzuzufügen, sobald der Entwurf bei der IETF in Zusammenarbeit mit anderen OpenPGP-Implementierern fertiggestellt ist.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
|
||||
wird eine post-quantum-resistente Verschlüsselung zum Schutz vor Angriffen durch Quantencomputer bieten.
|
||||
Delta Chat verwendet die Rust-OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>,
|
||||
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Kryptografie OpenPGP Entfurf</a> unterstützt.
|
||||
Die Implementierung ist in <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
|
||||
|
||||
<h3 id="wie-kann-ich-die-verschlüsselung-manuell-überprüfen">
|
||||
|
||||
@@ -1420,12 +1420,11 @@ ist die Verbindung sicher.</p>
|
||||
|
||||
<p>Nein.</p>
|
||||
|
||||
<p>Delta Chat generates secure OpenPGP keys according to the Autocrypt specification 1.1.
|
||||
We do not recommend or offer users to perform manual key management.
|
||||
We want to ensure that security audits can focus on a few proven cryptographic algorithms
|
||||
instead of the full breadth of possible algorithms allowed with OpenPGP.
|
||||
If you want to extract your OpenPGP key, there only is an expert method:
|
||||
you need to look it up in the “keypairs” SQLite table of a profile backup tar-file.</p>
|
||||
<p>Delta Chat generiert sichere OpenPGP-Schlüssel gemäß der Autocrypt-Spezifikation 1.1.
|
||||
Wir bieten Benutzern keine manuelle Schlüsselverwaltung an, noch empfehlen diese.
|
||||
Wir wollen sicherstellen, dass sich Sicherheitsüberprüfungen auf einige wenige bewährte kryptografische Algorithmen konzentrieren können,
|
||||
anstatt auf die gesamte Bandbreite der mit OpenPGP zulässigen Algorithmen.
|
||||
Wenn Sie Ihren OpenPGP-Schlüssel extrahieren möchten, gibt es nur eine Methode für Experten: Sie müssen ihn in der SQLite-Tabelle „keypairs” des Backups nachschlagen.</p>
|
||||
|
||||
<h3 id="security-audits">
|
||||
|
||||
@@ -1535,31 +1534,24 @@ Wir nutzen vielmehr öffentliche Finanzierungsquellen, die bisher aus der EU und
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>2023 und 2024 wurden wir in das Next-Generation-Internet-Programm (NGI)
|
||||
für unsere Arbeit an <a href="https://nlnet.nl/project/WebXDC-Push/">Webxdc-PUSH</a> aufgenommen,
|
||||
zusammen mit Kooperationspartnern, die an
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">Webxdc-Evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">Webxdc-XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> und
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Alle diese Projekte sind teilweise abgeschlossen oder sollen Anfang 2025 abgeschlossen werden.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im Jahr 2021 erhielten wir weitere EU-Mittel für zwei “Next-Generation-Internet”-Anträge, nämlich für <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - E-Mail-Provider-Portabilitätsverzeichnis</a> (~97K EUR) und <a href="https://nlnet.nl/project/EmailPorting/">AEAP - E-Mail-Adressportierung</a> (~90K EUR). Ziel sind bessere Unterstützung von Mehrfachkonten, verbesserten QR-Code-Kontakt- und -Gruppen-Setups sowie Netzwerkverbesserungen auf allen Plattformen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Die <a href="https://nlnet.nl/">NLnet-Stiftung</a> bewilligte 2019/2020 46K EUR für die Fertigstellung von Rust-/Python-Bindungs und die Einrichtung eines Chat-Bot-Ökosystems.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>Der <a href="https://opentechfund.org">Open Technology Fund</a> hat Delta Chat erstmals 2018/2019 bezuschusst; mit dieser Förderung (~$200K) wurden hauptsächlich die Android-App verbessert sowie das Release der Desktop-App in einer Betaversion ermöglicht. Basierend auf Nutzererfahrungen im Menschenrechtskontext wurden zudem verschiedene Funktionen entwickelt, siehe unseren Bericht <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Die zweite Förderung 2019/2020 (~$300K) half uns bei der Erstellung der iOS-Version, unsere Kernbibliothek in die Programmiersprache “Rust” zu konvertieren und neue Funktionen für alle Plattformen bereitzustellen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Das EU-Projekt <a href="https://nextleap.eu">NEXTLEAP</a> finanzierte 2017 und 2018 die Entwicklung und Implementierung von “Verifizierten Gruppen” und “Setup Kontakt” und half auch bei der Integration der Ende-zu-Ende-Verschlüsselung durch <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1184,6 +1184,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1365,12 +1369,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1446,7 +1448,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1457,12 +1459,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1472,12 +1471,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1175,6 +1175,9 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, qui est prévu pour 2026, amènera un chiffrement avec résistance post-quantique ainsi que la confidentialité persistante (“forward secrecy”).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
chat</a>
|
||||
@@ -1358,12 +1361,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1418,7 +1419,7 @@ For example, tapping a link exposes IP Addresses to unknown parties and is the b
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Non, pas encore.</p>
|
||||
|
||||
<p>The Signal messenger introduced <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” in 2018</a>
|
||||
to keep their server infrastructure ignorant of who is sending a message to a set of recipients.
|
||||
@@ -1439,7 +1440,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1450,12 +1451,8 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, permettra la suppression fiable (forward secrecy) grâce à une rotation automatique des clefs.
|
||||
Cette approche est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1465,12 +1462,11 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, amènera un chiffrement avec résistance post-quantique pour protéger contre les attaques effectuées par des ordinateurs quantiques.
|
||||
Delta Chat utilise la librairie Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> qui supporte les dernières <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
L’implémentation est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1173,6 +1173,10 @@ per scambiare informazioni sulla configurazione della crittografia tramite la sc
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> viene utilizzato per stabilire
|
||||
automaticamente la crittografia end-to-end tra i contatti e tutti i membri di una chat di gruppo.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
introdurrà una crittografia post-quantistica resistente e una segretezza avanzata.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Condivisione di un contatto con una
|
||||
@@ -1436,7 +1440,7 @@ ma un’implementazione non è stata ancora concordata come priorità.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, non ancora.</p>
|
||||
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat al momento non supporta la tecnologia Perfect Forward Secrecy (PFS).
|
||||
Ciò significa che se la tua chiave di decrittazione privata viene divulgata
|
||||
@@ -1447,12 +1451,9 @@ In caso contrario, chi ottiene le tue chiavi di decrittazione
|
||||
in genere è in grado di ottenere anche tutti i tuoi messaggi non eliminati
|
||||
e non ha nemmeno bisogno di decifrare i messaggi raccolti in precedenza.</p>
|
||||
|
||||
<p>Abbiamo progettato un approccio Forward Secrecy che ha superato
|
||||
l’esame iniziale di alcuni crittografi ed esperti di implementazione
|
||||
ma è in attesa di una stesura più formale
|
||||
per accertarne l’affidabilità nella messaggistica federata e nell’utilizzo su più dispositivi,
|
||||
prima di poter essere implementato in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">clients di chatmail</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
garantirà un’eliminazione affidabile (segretezza in avanti) tramite rotazione automatica delle chiavi.
|
||||
Questo approccio è specificato nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificati OpenPGP di Autocrypt v2</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1462,12 +1463,13 @@ che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">c
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, non ancora.</p>
|
||||
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
|
||||
offrirà una crittografia post-quantistica resistente per proteggere dagli attacchi ai computer quantistici.
|
||||
Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
che supporta l’ultima <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">bozza IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Il nostro obiettivo è aggiungere il supporto PQC nel <a href="https://github.com/chatmail/core">core di chatmail</a> dopo che la bozza sarà stata finalizzata dall’IETF
|
||||
in collaborazione con altri implementatori di OpenPGP.</p>
|
||||
L’implementazione è specificata nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Certificati OpenPGP Autocrypt v2</a>.</p>
|
||||
|
||||
<h3 id="come-posso-controllare-manualmente-le-informazioni-di-crittografia">
|
||||
|
||||
@@ -1647,31 +1649,31 @@ ordinate cronologicamente:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>Nel 2023 e nel 2024 siamo stati accettati nel programma Next Generation Internet (NGI)
|
||||
per il nostro lavoro in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
insieme ai partner di collaborazione che lavorano su
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> e
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Tutti questi progetti sono parzialmente completati o saranno completati all’inizio del 2025.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Nel 2021 abbiamo ricevuto ulteriori finanziamenti dall’UE per due proposte di Next-Generation-Internet, ovvero per <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - directory di portabilità dei provider di posta elettronica</a> (~97.000 EUR) e <a href="https://nlnet.nl/project/EmailPorting/">AEAP - portabilità degli indirizzi email</a> (~90.000 EUR), che hanno portato a un migliore supporto multi-profilo, a un miglioramento delle impostazioni di contatto e di gruppo tramite codice QR e a numerosi miglioramenti di rete su tutte le piattaforme.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>La <a href="https://nlnet.nl/">fondazione NLnet</a> ha concesso nel 2019/2020 46.000 EUR per
|
||||
completando i collegamenti Rust/Python e avviando un ecosistema Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>L’<a href="https://opentechfund.org">Open Technology Fund</a> ci ha dato una
|
||||
prima sovvenzione 2018/2019 (~$200K) durante la quale abbiamo notevolmente migliorato l’app Android
|
||||
e ha rilasciato una prima versione beta dell’app desktop, e che inoltre
|
||||
ancorato i nostri sviluppi delle funzionalità nella ricerca sulla UX nei contesti dei diritti umani,
|
||||
vedete il nostro <a href="https://delta.chat/en/2019-07-19-uxreport">Rapporto Needfinding e UX</a> conclusivo.
|
||||
La seconda sovvenzione 2019/2020 (~$300K) ci ha aiutato a farlo
|
||||
rilasciare nelle versioni Delta/iOS, per convertire la nostra libreria principale in Rust, e
|
||||
per fornire nuove funzionalità per tutte le piattaforme.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Il progetto UE <a href="https://nextleap.eu">NEXTLEAP</a> ha finanziato la ricerca
|
||||
|
||||
@@ -1182,6 +1182,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1366,12 +1370,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1447,7 +1449,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1458,12 +1460,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1473,12 +1472,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1102,6 +1102,10 @@ weekly statistics will be automatically sent to a bot.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> służy do automatycznego ustanawiania szyfrowania typu end-to-end między kontaktami a wszystkimi członkami czatu grupowego.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Udostępnienie kontaktu na czacie</a> umożliwia odbiorcom korzystanie z szyfrowania typu end-to-end z tym kontaktem.</p>
|
||||
</li>
|
||||
@@ -1236,12 +1240,10 @@ even if the message was not end-to-end encrypted.</p>
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>Wszystkie pozostałe metadane dotyczące wiadomości, kontaktów i grup znajdują się w zaszyfrowanej metodą end-to-end części wiadomości.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1314,11 +1316,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nie, jeszcze nie.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat obecnie nie obsługuje mechanizmu Perfect Forward Secrecy (PFS). Oznacza to, że jeśli twój prywatny klucz deszyfrujący zostanie ujawniony, a ktoś zdobędzie twoje wcześniejsze wiadomości w trakcie transmisji, będzie mógł je odszyfrować i odczytać za pomocą ujawnionego klucza deszyfrującego. Należy pamiętać, że mechanizm Forward Secrecy zwiększa bezpieczeństwo tylko w przypadku usuwania wiadomości. W przeciwnym razie osoba, która uzyska twoje klucze deszyfrujące, zazwyczaj będzie mogła uzyskać dostęp do wszystkich nieusuniętych wiadomości i nie będzie musiała odszyfrowywać żadnych wcześniej zebranych wiadomości.</p>
|
||||
|
||||
<p>Opracowaliśmy metodę Forward Secrecy, która przeszła wstępną analizę niektórych kryptografów i ekspertów ds. wdrożeń, ale oczekuje na bardziej formalne opracowanie, które potwierdzi jej niezawodne działanie w federacyjnym przesyłaniu wiadomości i w przypadku korzystania z wielu urządzeń, zanim zostanie zaimplementowana w <a href="https://github.com/chatmail/core">rdzeniu chatmail</a>, co uczyniłoby ją dostępną we wszystkich <a href="https://chatmail.at/clients">klientach chatmail</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1328,9 +1332,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Nie, jeszcze nie.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat korzysta z biblioteki Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>, która obsługuje najnowszy <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">projekt OpenPGP IETF Post-Quantum-Cryptography</a>. Planujemy dodać obsługę PQC do <a href="https://github.com/chatmail/core">rdzenia chatmail</a> po sfinalizowaniu projektu w IETF we współpracy z innymi implementatorami OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="jak-mogę-ręcznie-sprawdzić-informacje-o-szyfrowaniu">
|
||||
|
||||
@@ -1463,32 +1471,17 @@ Raczej korzystamy z publicznych źródeł finansowania, jak dotąd pochodzących
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>W latach 2023 i 2024 zostaliśmy przyjęci do programu Next Generation Internet (NGI) za naszą pracę w <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, wraz z partnerami współpracującymi pracującymi nad <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> i <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Wszystkie te projekty są częściowo ukończone lub zostaną ukończone na początku 2025 r.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>W 2021 r. otrzymaliśmy kolejne dofinansowanie z UE na dwie propozycje dotyczące Internetu nowej generacji, a mianowicie na <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD – katalog przenośności dostawcy poczty e-mail</a> ( ~97 tys. EUR) i <a href="https://nlnet.nl/project/EmailPorting/">AEAP – przenoszenie adresu e-mail</a> (~90 tys. EUR), co zaowocowało lepszą obsługą wielu kont, ulepszonymi kontaktami i ustawieniami grup za pomocą kodów QR oraz wieloma ulepszeniami sieciowymi na wszystkich platformach.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">Fundacja NLnet</a> przekazała w latach 2019/2020 kwotę 46 tys. EUR na wykonanie wiązań Rust/Python i uruchomienie ekosystemu Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> przyznał nam pierwszy grant w 2018/2019 (~200 000 $), dzięki któremu znacznie ulepszyliśmy aplikację na Androida i wydaliśmy pierwszą wersję beta aplikacji na komputery stacjonarne, a także ugruntował rozwój naszych funkcji w badaniach UX w kontekście praw człowieka, zobacz nasz końcowy raport <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX</a>.
|
||||
Druga dotacja w 2019/2020 (~300 000 4) pomogła nam wydać wersje Delta/iOS, przekonwertować naszą podstawową bibliotekę na Rust i zapewnić nowe funkcje dla wszystkich platform.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Unijny projekt <a href="https://nextleap.eu">NEXTLEAP</a> sfinansował badania i wdrożenie zweryfikowanych grup i ustawień protokołów kontaktowych w latach 2017 i 2018, a także pomógł zintegrować szyfrowanie end-to-end poprzez <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1183,6 +1183,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1367,12 +1371,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1448,7 +1450,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1459,12 +1461,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1474,12 +1473,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1181,6 +1181,10 @@ Chatmail использует INBOX по умолчанию для ретран
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> используется для автоматической
|
||||
настройки сквозного шифрования между контактами и всеми членами группового чата.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит поддержку постквантового шифрования и прямой секретности.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Обмен контактом в
|
||||
@@ -1365,12 +1369,10 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
<p>Таким образом, серверы могут видеть только:</p>
|
||||
|
||||
<ul>
|
||||
<li>адрес отправителя и получателя</li>
|
||||
<li>а также размер сообщения.</li>
|
||||
<li>Адреса отправителя и получателя, по умолчанию генерируются случайным образом</li>
|
||||
<li>Размер сообщения</li>
|
||||
</ul>
|
||||
|
||||
<p>По умолчанию адреса генерируются случайным образом.</p>
|
||||
|
||||
<p>Все прочие метаданные сообщений, контактов и групп содержатся в части сообщений, защищённой сквозным шифрованием.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1427,7 +1429,7 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Нет, пока нет.</p>
|
||||
|
||||
<p>Мессенджер Signal внедрил функцию <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” (Засекреченный отправитель) в 2018 году</a>,
|
||||
чтобы их серверная инфраструктура не имела информации о том, кто отправляет сообщение группе получателей.
|
||||
@@ -1448,7 +1450,7 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Пока нет, но это будет реализовано в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>На данный момент, Delta Chat не поддерживает Perfect Forward Secrecy (PFS) (Совершенную прямую секретность).
|
||||
Это означает, что если ваш приватный ключ дешифрования будет скомпрометирован,
|
||||
@@ -1459,12 +1461,9 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
также может получить все ваши не удалённые сообщения
|
||||
и ему даже не нужно расшифровывать какие-либо ранее собранные сообщения.</p>
|
||||
|
||||
<p>Мы разработали подход к Forward Secrecy (Прямой секретности), который прошёл
|
||||
первичную проверку некоторыми криптографами и экспертами по реализации
|
||||
но требует более формального описания
|
||||
чтобы убедиться, что он надёжно работает в федеративном обмене сообщениями и при использовании нескольких устройств,
|
||||
прежде чем он может быть внедрён в <a href="https://github.com/chatmail/core">ядро chatmail</a>,
|
||||
что сделает его доступным во всех <a href="https://chatmail.at/clients">клиентах clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит надёжное удаление (прямую секретность) за счёт автоматической ротации ключей.
|
||||
Этот подход описан в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1474,12 +1473,13 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Нет, еще нет.</p>
|
||||
<p>Пока нет, но эта возможность появится в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat использует библиотеку OpenPGP на Rust <a href="https://github.com/rpgp/rpgp">rPGP</a>,
|
||||
которая поддерживает последний <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">черновик IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Мы планируем добавить поддержку PQC в <a href="https://github.com/chatmail/core">ядро chatmail</a> после того, как черновик будет окончательно утвержден в IETF
|
||||
в сотрудничестве с другими разработчиками OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
|
||||
обеспечит поддержку постквантового шифрования для защиты от атак с использованием квантовых компьютеров.
|
||||
Delta Chat использует Rust-библиотеку OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
которая поддерживает актуальный черновик IETF <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP</a>.
|
||||
Особенности реализации описаны в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
|
||||
|
||||
<h3 id="как-можно-вручную-проверить-информацию-о-шифровании">
|
||||
|
||||
@@ -1658,32 +1658,32 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS и macOS App Store, Microsof
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>В 2023 и 2024 годах мы были приняты в программу Next Generation Internet (NGI)
|
||||
за нашу работу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
в сотрудничестве с партнерами, работающими над
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> и
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
Все эти проекты частично завершены или будут завершены в начале 2025 года.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В 2021 г. мы получили дополнительное финансирование из ЕС для двух Next-Generation-Internet
|
||||
целей, а именно для <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97 тыс. евро) и <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90 тыс. евро). Это привело к улучшению поддержки нескольких профилей, улучшению настройки контактов и групп с помощью QR-кода и многим улучшениям в сетевом взаимодействии на всех платформах.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Фонд <a href="https://nlnet.nl/">NLnet Foundation</a> выделил в 2019/2020 году 46 тысяч евро на
|
||||
доработку связки Rust/Python и создание экосистемы чат-ботов..</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>Фонд <a href="https://opentechfund.org">Open Technology Fund</a> предоставил нам
|
||||
первый грант в 2018/2019 году (~$200 тыс.), благодаря которому мы существенно улучшили приложение для Android
|
||||
и выпустили первую бета-версию приложения для настольных систем, а также провели
|
||||
исследования в области UX в контексте прав человека,
|
||||
см. наш заключительный отчет <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Второй грант, полученный в 2019/2020 году (~$300 тыс.), помог нам
|
||||
выпустить версии Delta/iOS, перевести наш основной код на Rust и
|
||||
предоставить новые функции для всех платформ.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Проект ЕС <a href="https://nextleap.eu">NEXTLEAP</a> финансировал исследование
|
||||
|
||||
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
|
||||
@@ -1190,6 +1190,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
establishing end-to-end encryption between contacts and all members of a group chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
|
||||
@@ -1374,12 +1378,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1455,7 +1457,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1466,12 +1468,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1481,12 +1480,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
@@ -1675,28 +1675,31 @@ along with collaboration partners working on
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>Më 2021-n morëm financime të mëtejshme nga BE për dy propozime që shtrihen në
|
||||
“Internetin e Brezit Tjetër”, konkretisht për <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97K euro) dhe <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K euro) që sollën mbulim më të mirë për përdorues me shumë
|
||||
llogari, përmirësim të gjërave për kontakte me kod QR dhe grupe, si dhe mjaft
|
||||
përmirësime në punën në rrjet për krejt platformat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">Fondacioni NLnet</a> dhuroi 46K euro gjatë 2019/2020 për
|
||||
plotësimin e <em>Rust/Python bindings</em> dhe për t’i dhënë udhë një ekosistemi
|
||||
Chat-bot.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> na dha grantin e parë
|
||||
për 2018/2019 (~200 mijë dollarë) me të cilin përmirësuam ndjeshëm aplikacionin
|
||||
për Android dhe hodhëm në qarkullim një version të parë beta aplikacioni për Desktop,
|
||||
si dhe i afroi më tepër zhvillimet tona për veçori me kërkime UX në kontekste të drejtash të njeriut,
|
||||
shihni <a href="https://delta.chat/en/2019-07-19-uxreport">raportin tonë përfundimtar “Needfinding and UX”</a>.
|
||||
Granti i dytë për 2019/2020 (~$300K) na ndihmoi të hedhim në qarkullim
|
||||
versione Delta/iOS, për të shndërruar bibliotekën tonë bazë në Rust, si dhe
|
||||
për të sjellë veçori të reja për krejt platformat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
|
||||
and implementation of verified groups and setup contact protocols
|
||||
in 2017 and 2018 and also helped to integrate end-to-end Encryption
|
||||
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
<p>Projekti <a href="https://nextleap.eu">NEXTLEAP</a> i BE-së financoi kërkimin
|
||||
për dhe sendërtimin e grupeve të verifikuara dhe protokolleve të
|
||||
ujdisjes së kontakteve më 2017-n dhe 2018-n dhe ndihmoi gjithashtu
|
||||
të integrohet Fshehtëzim Skaj-më-Skaj përmes <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ndonjëherë marrim dhurime unike nga individë privatë.
|
||||
|
||||
@@ -140,17 +140,10 @@
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you are <strong>face to face</strong> with your friend or family,
|
||||
tap the <strong>QR Code</strong> icon <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
|
||||
on the main screen.<br />
|
||||
Ask your chat partner to <strong>scan</strong> the QR image
|
||||
with their Delta Chat app.</p>
|
||||
<p>Якщо ви перебуваєте <strong>віч-на-віч</strong> зі своїм другом або родиною, торкніться піктограми <strong>QR-код</strong> на головному екрані <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> на головному екрані. Попросіть вашого партнера по чату <strong>сканувати</strong> QR-зображення за допомогою їхнього застосунку Delta Chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For a <strong>remote</strong> contact setup,
|
||||
from the same screen,
|
||||
click “Copy” or “Share” and send the <strong>invite link</strong>
|
||||
through another private chat.</p>
|
||||
<p>Для <strong>віддаленого</strong> налаштування контакту, на тому ж самому екрані, натисніть “Копіювати” або “Поділитися” і відправте <strong>запрошувальне посилання</strong> через інший приватний чат.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1095,6 +1088,10 @@ to send anonymous usage statistics.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> використовується для автоматичного встановлення наскрізного шифрування між контактами і всіма учасниками групового чату.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Поширення контакту в чаті</a> дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
|
||||
</li>
|
||||
@@ -1298,11 +1295,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ні, поки ще ні.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat наразі не підтримує ідеальну пряму секретність (Perfect Forward Secrecy, PFS). Це означає, що якщо ваш приватний ключ для розшифрування буде скомпрометовано, а хтось заздалегідь зібрав ваші повідомлення під час передачі, він зможе розшифрувати та прочитати їх, використовуючи зламаний ключ. Зверніть увагу, що пряма секретність підвищує рівень безпеки лише в тому разі, якщо ви видаляєте повідомлення. Інакше, якщо хтось отримує доступ до ваших ключів розшифрування, він зазвичай також має доступ до всіх ваших невидалених повідомлень і навіть не потребує розшифровувати заздалегідь перехоплені дані.</p>
|
||||
|
||||
<p>Ми розробили підхід Forward Secrecy, який витримав початкову експертизу від деяких криптографів та експертів з реалізації але чекає на більш офіційний звіт щоб переконатися, що він надійно працює в об’єднаних системах обміну повідомленнями та при використанні декількох пристроїв, перш ніж його можна буде реалізувати в <a href="https://github.com/chatmail/core">ядрі чату</a>, що зробить його доступним у всіх <a href="https://chatmail.at/clients">клієнтах чату</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1312,9 +1311,13 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ні, поки ще ні.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat використовує бібліотеку Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> яка підтримує останню версію <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>. Ми плануємо додати підтримку PQC у <a href="https://github.com/chatmail/core">chatmail core</a> після того, як проект буде завершено у IETF у співпраці з іншими розробниками OpenPGP.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="як-я-можу-вручну-перевірити-інформацію-про-шифрування">
|
||||
|
||||
@@ -1445,32 +1448,24 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>У 2023 та 2024 роках нас прийняли до програми Next Generation Internet (NGI) за нашу роботу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, а також у співпраці з партнерами, які працюють над <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> та <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Усі ці проєкти частково завершені або будуть завершені на початку 2025 року.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>У 2021 році ми отримали подальше фінансування від ЄС на дві пропозиції щодо Інтернету наступного покоління а саме на <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - каталог перенесення провайдерів електронної пошти</a> (~97 тис. євро) та <a href="https://nlnet.nl/project/EmailPorting/">AEAP - перенесення адрес електронної пошти</a> (~90 тис. євро), що дозволило нам покращити багатопрофільну підтримку, вдосконалити налаштування контактів та груп за допомогою QR-коду та багато інших мережевих покращень на всіх платформах.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p>Фонд <a href="https://nlnet.nl/">NLnet</a> виділив у 2019/2020 роках 46 тисяч євро на
|
||||
завершення прив’язок Rust/Python та запуск екосистеми чат-ботів.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p><a href="https://opentechfund.org">Open Technology Fund</a> надав нам два гранти.
|
||||
Перший грант 2018/2019 року (~$200K), допоміг значно покращили додаток для Android
|
||||
і випустили першу бета-версію додатка для ПК, і який до того ж
|
||||
закріпив наші розробки функцій у дослідженнях UX у контексті прав людини,
|
||||
дивіться наш підсумковий звіт <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Другий грант 2019/2020 року (~$300K) допоміг нам
|
||||
випустити Delta/iOS версію, конвертувати нашу основному бібліотеку на Rust,
|
||||
і додати нові функції для всіх платформ.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Проект ЄС <a href="https://nextleap.eu">NEXTLEAP</a> фінансував дослідження та впровадження верифікованих груп і протоколів встановлення контактів у 2017 та 2018 роках, а також допоміг інтегрувати наскрізне шифрування через <a href="https://autocrypt.org">Autocrypt</a>.</p>
|
||||
|
||||
@@ -1161,6 +1161,10 @@ weekly statistics will be automatically sent to a bot.</p>
|
||||
<li>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
|
||||
用于在联系人和群聊的所有成员之间自动建立端到端加密。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption and forward secrecy.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">将联系人分享到聊天中
|
||||
@@ -1342,12 +1346,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
|
||||
<p>Servers can therefore only see:</p>
|
||||
|
||||
<ul>
|
||||
<li>the sender and receiver addresses</li>
|
||||
<li>and the message size.</li>
|
||||
<li>Sender and receiver addresses, randomly generated by default</li>
|
||||
<li>Message size</li>
|
||||
</ul>
|
||||
|
||||
<p>By default, the addresses are randomly generated.</p>
|
||||
|
||||
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
|
||||
|
||||
<h3 id="device-seizure">
|
||||
@@ -1423,7 +1425,7 @@ but an implementation has not been agreed as a priority yet.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat today doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your private decryption key is leaked,
|
||||
@@ -1434,12 +1436,9 @@ Otherwise, someone obtaining your decryption keys
|
||||
is typically also able to get all your non-deleted messages
|
||||
and doesn’t even need to decrypt any previously collected messages.</p>
|
||||
|
||||
<p>We designed a Forward Secrecy approach that withstood
|
||||
initial examination from some cryptographers and implementation experts
|
||||
but is pending a more formal write up
|
||||
to ascertain it reliably works in federated messaging and with multi-device usage,
|
||||
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
|
||||
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will provide reliable deletion (forward secrecy) through automatic key rotation.
|
||||
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="pqc">
|
||||
|
||||
@@ -1449,12 +1448,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, not yet.</p>
|
||||
<p>Not yet, but it’s coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
|
||||
|
||||
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
|
||||
in collaboration with other OpenPGP implementers.</p>
|
||||
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
|
||||
will bring post-quantum resistant encryption to protect against quantum computer attacks.
|
||||
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
|
||||
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
|
||||
|
||||
<h3 id="how-can-i-manually-check-encryption-information">
|
||||
|
||||
@@ -1624,32 +1624,22 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
|
||||
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
|
||||
along with collaboration partners working on
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
|
||||
All of these projects are partially completed or to be completed in early 2025.</p>
|
||||
<p>在 2023 年和 2024 年,我们的 <a href="https://nlnet.nl/project/WebXDC-Push/">WebXDC PUSH</a> 工作已在下一代互联网 (NGI) 中获得认可,
|
||||
并与致力于
|
||||
<a href="https://nlnet.nl/project/Webxdc-Evolve/">WebXDC evolve</a>、
|
||||
<a href="https://nlnet.nl/project/WebXDC-XMPP/">WebXDC XMPP</a>、
|
||||
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> 和
|
||||
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a> 的合作伙伴合作。
|
||||
所有这些项目都已部分完成或将在 2025 年初完成。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In 2021 we received further EU funding for two Next-Generation-Internet
|
||||
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
|
||||
<p>在 2021 年,我们从两项下一代互联网提案收到了欧盟的进一步资助,即 <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - 电子邮件提供商可移植性目录</a>(约 9.7 万欧元)和 <a href="https://nlnet.nl/project/EmailPorting/">AEAP - 电子邮件地址移植</a>(约 9 万欧元)。这带来了更好的多账户支持,改进的二维码联系人和群组设置,和所有平台上的多处网络改进。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
|
||||
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
|
||||
<p><a href="https://nlnet.nl/">NLnet 基金会</a> 2019/2020 年拨款 4.6 万欧元,用于完成 Rust/Python 绑定并建立聊天机器人生态系统。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
|
||||
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
|
||||
and released a first Desktop app beta version, and which moreover
|
||||
moored our feature developments in UX research in human rights contexts,
|
||||
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
The second 2019/2020 grant (~$300K) helped us to
|
||||
release Delta/iOS versions, to convert our core library to Rust, and
|
||||
to provide new features for all platforms.</p>
|
||||
<p>在<a href="https://opentechfund.org">开放技术基金</a> 2018/2019 年提供的第一笔赠款(约 20 万美元)期间,我们显著改善了安卓应用,发布了第一个桌面测试版,并根据人权方面的用户体验研究进行了功能开发,请参阅我们的结论<a href="https://delta.chat/en/2019-07-19-uxreport">《需求发现与用户体验报告》</a>。2019/2020 年的第二笔赠款(约 30 万美元)对发布 Delta/iOS 版本,将核心库转换到 Rust ,以及为所有平台开发新功能提供了帮助。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="https://nextleap.eu">NEXTLEAP</a>欧盟项目资助了以下研究和实施工作:在 2017 年和 2018 年实施的验证组和设置联系协议和通过 <a href="https://autocrypt.org">Autocrypt</a>整合了端到端加密。</p>
|
||||
|
||||
@@ -53,6 +53,11 @@ public class Rpc {
|
||||
return transport.callForResult(new TypeReference<Event>(){}, "get_next_event");
|
||||
}
|
||||
|
||||
/** Waits for at least one event and return a batch of events. */
|
||||
public java.util.List<Event> getNextEventBatch() throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<Event>>(){}, "get_next_event_batch");
|
||||
}
|
||||
|
||||
public Integer addAccount() throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "add_account");
|
||||
}
|
||||
@@ -682,7 +687,8 @@ public class Rpc {
|
||||
* Set group name.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
@@ -690,11 +696,37 @@ public class Rpc {
|
||||
transport.call("set_chat_name", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group or broadcast channel description.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
* <p>
|
||||
* See also [`Self::get_chat_description`] / `getChatDescription()`.
|
||||
*/
|
||||
public void setChatDescription(Integer accountId, Integer chatId, String description) throws RpcException {
|
||||
transport.call("set_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the chat description from the database.
|
||||
* <p>
|
||||
* UIs show this in the profile page of the chat,
|
||||
* it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
|
||||
*/
|
||||
public String getChatDescription(Integer accountId, Integer chatId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "get_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
* <p>
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* <p>
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
* <p>
|
||||
@@ -1315,8 +1347,8 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/** Starts an outgoing call. */
|
||||
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo));
|
||||
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo, Boolean hasVideo) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo), mapper.valueToTree(hasVideo));
|
||||
}
|
||||
|
||||
/** Accepts an incoming call. */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
package chat.delta.rpc.types;
|
||||
|
||||
public class CallInfo {
|
||||
/** True if SDP offer has a video. */
|
||||
/** True if the call is started as a video call. */
|
||||
public Boolean hasVideo;
|
||||
/**
|
||||
* SDP offer.
|
||||
|
||||
@@ -4,6 +4,7 @@ package chat.delta.rpc.types;
|
||||
public enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
GroupDescriptionChanged,
|
||||
GroupImageChanged,
|
||||
MemberAddedToGroup,
|
||||
MemberRemovedFromGroup,
|
||||
|
||||
@@ -2,64 +2,92 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcAccounts {
|
||||
|
||||
public DcAccounts(String dir) {
|
||||
accountsCPtr = createAccountsCPtr(dir);
|
||||
if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer");
|
||||
public DcAccounts(String dir, DcEventChannel channel) {
|
||||
accountsCPtr = createAccountsCPtr(dir, channel);
|
||||
if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unref();
|
||||
}
|
||||
|
||||
public void unref() {
|
||||
if (accountsCPtr != 0) {
|
||||
unrefAccountsCPtr();
|
||||
accountsCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unref();
|
||||
public DcEventEmitter getEventEmitter() {
|
||||
return new DcEventEmitter(getEventEmitterCPtr());
|
||||
}
|
||||
|
||||
public DcJsonrpcInstance getJsonrpcInstance() {
|
||||
return new DcJsonrpcInstance(getJsonrpcInstanceCPtr());
|
||||
}
|
||||
|
||||
public void startIo() {
|
||||
for (int accountId : getAll()) {
|
||||
DcContext acc = getAccount(accountId);
|
||||
if (acc.isEnabled()) {
|
||||
acc.startIo();
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
public void unref() {
|
||||
if (accountsCPtr != 0) {
|
||||
unrefAccountsCPtr();
|
||||
accountsCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); }
|
||||
public DcJsonrpcInstance getJsonrpcInstance () { return new DcJsonrpcInstance(getJsonrpcInstanceCPtr()); }
|
||||
public void startIo () {
|
||||
for (int accountId : getAll()) {
|
||||
DcContext acc = getAccount(accountId);
|
||||
if (acc.isEnabled()) {
|
||||
acc.startIo();
|
||||
}
|
||||
}
|
||||
};
|
||||
public native void startIo2 ();
|
||||
public native void stopIo ();
|
||||
public native void maybeNetwork ();
|
||||
public native void setPushDeviceToken (String token);
|
||||
public native boolean backgroundFetch (int timeoutSeconds);
|
||||
public native void stopBackgroundFetch ();
|
||||
|
||||
public native int migrateAccount (String dbfile);
|
||||
public native boolean removeAccount (int accountId);
|
||||
public native int[] getAll ();
|
||||
public DcContext getAccount (int accountId) { return new DcContext(getAccountCPtr(accountId)); }
|
||||
public DcContext getSelectedAccount () { return new DcContext(getSelectedAccountCPtr()); }
|
||||
public native boolean selectAccount (int accountId);
|
||||
|
||||
// working with raw c-data
|
||||
private long accountsCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native long createAccountsCPtr (String dir);
|
||||
private native void unrefAccountsCPtr ();
|
||||
private native long getEventEmitterCPtr ();
|
||||
private native long getJsonrpcInstanceCPtr ();
|
||||
private native long getAccountCPtr (int accountId);
|
||||
private native long getSelectedAccountCPtr ();
|
||||
|
||||
public boolean isAllChatmail() {
|
||||
for (int accountId : getAll()) {
|
||||
DcContext dcContext = getAccount(accountId);
|
||||
if (!dcContext.isChatmail()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
public native void startIo2();
|
||||
|
||||
public native void stopIo();
|
||||
|
||||
public native void maybeNetwork();
|
||||
|
||||
public native void setPushDeviceToken(String token);
|
||||
|
||||
public native boolean backgroundFetch(int timeoutSeconds);
|
||||
|
||||
public native void stopBackgroundFetch();
|
||||
|
||||
public native int migrateAccount(String dbfile);
|
||||
|
||||
public native boolean removeAccount(int accountId);
|
||||
|
||||
public native int[] getAll();
|
||||
|
||||
public DcContext getAccount(int accountId) {
|
||||
return new DcContext(getAccountCPtr(accountId));
|
||||
}
|
||||
|
||||
public DcContext getSelectedAccount() {
|
||||
return new DcContext(getSelectedAccountCPtr());
|
||||
}
|
||||
|
||||
public native boolean selectAccount(int accountId);
|
||||
|
||||
// working with raw c-data
|
||||
private long accountsCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native long createAccountsCPtr(String dir, DcEventChannel channel);
|
||||
|
||||
private native void unrefAccountsCPtr();
|
||||
|
||||
private native long getEventEmitterCPtr();
|
||||
|
||||
private native long getJsonrpcInstanceCPtr();
|
||||
|
||||
private native long getAccountCPtr(int accountId);
|
||||
|
||||
private native long getSelectedAccountCPtr();
|
||||
|
||||
public boolean isAllChatmail() {
|
||||
for (int accountId : getAll()) {
|
||||
DcContext dcContext = getAccount(accountId);
|
||||
if (!dcContext.isChatmail()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,35 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcBackupProvider {
|
||||
|
||||
public DcBackupProvider(long backupProviderCPtr) {
|
||||
this.backupProviderCPtr = backupProviderCPtr;
|
||||
public DcBackupProvider(long backupProviderCPtr) {
|
||||
this.backupProviderCPtr = backupProviderCPtr;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return backupProviderCPtr != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unref();
|
||||
}
|
||||
|
||||
public void unref() {
|
||||
if (backupProviderCPtr != 0) {
|
||||
unrefBackupProviderCPtr();
|
||||
backupProviderCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return backupProviderCPtr != 0;
|
||||
}
|
||||
public native String getQr();
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unref();
|
||||
}
|
||||
public native String getQrSvg();
|
||||
|
||||
public void unref() {
|
||||
if (backupProviderCPtr != 0) {
|
||||
unrefBackupProviderCPtr();
|
||||
backupProviderCPtr = 0;
|
||||
}
|
||||
}
|
||||
public native void waitForReceiver();
|
||||
|
||||
public native String getQr ();
|
||||
public native String getQrSvg ();
|
||||
public native void waitForReceiver ();
|
||||
// working with raw c-data
|
||||
private long backupProviderCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
// working with raw c-data
|
||||
private long backupProviderCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefBackupProviderCPtr();
|
||||
private native void unrefBackupProviderCPtr();
|
||||
}
|
||||
|
||||
@@ -1,76 +1,109 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class DcChat {
|
||||
|
||||
public static final int DC_CHAT_TYPE_UNDEFINED = 0;
|
||||
public static final int DC_CHAT_TYPE_SINGLE = 100;
|
||||
public static final int DC_CHAT_TYPE_GROUP = 120;
|
||||
public static final int DC_CHAT_TYPE_MAILINGLIST = 140;
|
||||
public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160;
|
||||
public static final int DC_CHAT_TYPE_IN_BROADCAST = 165;
|
||||
public static final int DC_CHAT_TYPE_UNDEFINED = 0;
|
||||
public static final int DC_CHAT_TYPE_SINGLE = 100;
|
||||
public static final int DC_CHAT_TYPE_GROUP = 120;
|
||||
public static final int DC_CHAT_TYPE_MAILINGLIST = 140;
|
||||
public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160;
|
||||
public static final int DC_CHAT_TYPE_IN_BROADCAST = 165;
|
||||
|
||||
public static final int DC_CHAT_NO_CHAT = 0;
|
||||
public final static int DC_CHAT_ID_ARCHIVED_LINK = 6;
|
||||
public final static int DC_CHAT_ID_ALLDONE_HINT = 7;
|
||||
public final static int DC_CHAT_ID_LAST_SPECIAL = 9;
|
||||
public static final int DC_CHAT_NO_CHAT = 0;
|
||||
public static final int DC_CHAT_ID_ARCHIVED_LINK = 6;
|
||||
public static final int DC_CHAT_ID_ALLDONE_HINT = 7;
|
||||
public static final int DC_CHAT_ID_LAST_SPECIAL = 9;
|
||||
|
||||
public final static int DC_CHAT_VISIBILITY_NORMAL = 0;
|
||||
public final static int DC_CHAT_VISIBILITY_ARCHIVED = 1;
|
||||
public final static int DC_CHAT_VISIBILITY_PINNED = 2;
|
||||
public static final int DC_CHAT_VISIBILITY_NORMAL = 0;
|
||||
public static final int DC_CHAT_VISIBILITY_ARCHIVED = 1;
|
||||
public static final int DC_CHAT_VISIBILITY_PINNED = 2;
|
||||
|
||||
private int accountId;
|
||||
private int accountId;
|
||||
|
||||
public DcChat(int accountId, long chatCPtr) {
|
||||
this.accountId = accountId;
|
||||
this.chatCPtr = chatCPtr;
|
||||
public DcChat(int accountId, long chatCPtr) {
|
||||
this.accountId = accountId;
|
||||
this.chatCPtr = chatCPtr;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefChatCPtr();
|
||||
chatCPtr = 0;
|
||||
}
|
||||
|
||||
public int getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public native int getId();
|
||||
|
||||
public native int getType();
|
||||
|
||||
public native int getVisibility();
|
||||
|
||||
public native String getName();
|
||||
|
||||
public native String getMailinglistAddr();
|
||||
|
||||
public native String getProfileImage();
|
||||
|
||||
public native int getColor();
|
||||
|
||||
public native boolean isEncrypted();
|
||||
|
||||
public native boolean isUnpromoted();
|
||||
|
||||
public native boolean isSelfTalk();
|
||||
|
||||
public native boolean isDeviceTalk();
|
||||
|
||||
public native boolean canSend();
|
||||
|
||||
public native boolean isSendingLocations();
|
||||
|
||||
public native boolean isMuted();
|
||||
|
||||
public native boolean isContactRequest();
|
||||
|
||||
// aliases and higher-level tools
|
||||
|
||||
public boolean isMultiUser() {
|
||||
int type = getType();
|
||||
return type != DC_CHAT_TYPE_SINGLE;
|
||||
}
|
||||
|
||||
public boolean shallLeaveBeforeDelete(DcContext dcContext) {
|
||||
if (isInBroadcast()) {
|
||||
final int[] members = dcContext.getChatContacts(getId());
|
||||
return Util.contains(members, DcContact.DC_CONTACT_ID_SELF);
|
||||
} else if (isMultiUser() && isEncrypted() && canSend() && !isOutBroadcast()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefChatCPtr();
|
||||
chatCPtr = 0;
|
||||
}
|
||||
public boolean isMailingList() {
|
||||
return getType() == DC_CHAT_TYPE_MAILINGLIST;
|
||||
}
|
||||
|
||||
public int getAccountId () { return accountId; }
|
||||
public native int getId ();
|
||||
public native int getType ();
|
||||
public native int getVisibility ();
|
||||
public native String getName ();
|
||||
public native String getMailinglistAddr();
|
||||
public native String getProfileImage ();
|
||||
public native int getColor ();
|
||||
public native boolean isEncrypted ();
|
||||
public native boolean isUnpromoted ();
|
||||
public native boolean isSelfTalk ();
|
||||
public native boolean isDeviceTalk ();
|
||||
public native boolean canSend ();
|
||||
public native boolean isSendingLocations();
|
||||
public native boolean isMuted ();
|
||||
public native boolean isContactRequest ();
|
||||
public boolean isInBroadcast() {
|
||||
return getType() == DC_CHAT_TYPE_IN_BROADCAST;
|
||||
}
|
||||
|
||||
public boolean isOutBroadcast() {
|
||||
return getType() == DC_CHAT_TYPE_OUT_BROADCAST;
|
||||
}
|
||||
|
||||
// aliases and higher-level tools
|
||||
// working with raw c-data
|
||||
|
||||
public boolean isMultiUser() {
|
||||
int type = getType();
|
||||
return type != DC_CHAT_TYPE_SINGLE;
|
||||
}
|
||||
private long chatCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
public boolean isMailingList() {
|
||||
return getType() == DC_CHAT_TYPE_MAILINGLIST;
|
||||
}
|
||||
|
||||
public boolean isInBroadcast() {
|
||||
return getType() == DC_CHAT_TYPE_IN_BROADCAST;
|
||||
}
|
||||
public boolean isOutBroadcast() {
|
||||
return getType() == DC_CHAT_TYPE_OUT_BROADCAST;
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
|
||||
private long chatCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefChatCPtr();
|
||||
public long getChatCPtr () { return chatCPtr; }
|
||||
private native void unrefChatCPtr();
|
||||
|
||||
public long getChatCPtr() {
|
||||
return chatCPtr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,45 +2,64 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcChatlist {
|
||||
|
||||
private int accountId;
|
||||
private int accountId;
|
||||
|
||||
public DcChatlist(int accountId, long chatlistCPtr) {
|
||||
this.accountId = accountId;
|
||||
this.chatlistCPtr = chatlistCPtr;
|
||||
}
|
||||
public DcChatlist(int accountId, long chatlistCPtr) {
|
||||
this.accountId = accountId;
|
||||
this.chatlistCPtr = chatlistCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefChatlistCPtr();
|
||||
chatlistCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefChatlistCPtr();
|
||||
chatlistCPtr = 0;
|
||||
}
|
||||
|
||||
public int getAccountId() { return accountId; }
|
||||
public native int getCnt ();
|
||||
public native int getChatId (int index);
|
||||
public DcChat getChat (int index) { return new DcChat(accountId, getChatCPtr(index)); }
|
||||
public native int getMsgId (int index);
|
||||
public DcMsg getMsg (int index) { return new DcMsg(getMsgCPtr(index)); }
|
||||
public DcLot getSummary(int index, DcChat chat) { return new DcLot(getSummaryCPtr(index, chat==null? 0 : chat.getChatCPtr())); }
|
||||
public int getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public class Item {
|
||||
public DcLot summary;
|
||||
public int msgId;
|
||||
public int chatId;
|
||||
}
|
||||
public native int getCnt();
|
||||
|
||||
public Item getItem(int index) {
|
||||
Item item = new Item();
|
||||
item.summary = getSummary(index, null);
|
||||
item.msgId = getMsgId(index);
|
||||
item.chatId = getChatId(index);
|
||||
return item;
|
||||
}
|
||||
public native int getChatId(int index);
|
||||
|
||||
// working with raw c-data
|
||||
private long chatlistCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefChatlistCPtr();
|
||||
private native long getChatCPtr (int index);
|
||||
private native long getMsgCPtr (int index);
|
||||
private native long getSummaryCPtr (int index, long chatCPtr);
|
||||
public DcChat getChat(int index) {
|
||||
return new DcChat(accountId, getChatCPtr(index));
|
||||
}
|
||||
|
||||
public native int getMsgId(int index);
|
||||
|
||||
public DcMsg getMsg(int index) {
|
||||
return new DcMsg(getMsgCPtr(index));
|
||||
}
|
||||
|
||||
public DcLot getSummary(int index, DcChat chat) {
|
||||
return new DcLot(getSummaryCPtr(index, chat == null ? 0 : chat.getChatCPtr()));
|
||||
}
|
||||
|
||||
public class Item {
|
||||
public DcLot summary;
|
||||
public int msgId;
|
||||
public int chatId;
|
||||
}
|
||||
|
||||
public Item getItem(int index) {
|
||||
Item item = new Item();
|
||||
item.summary = getSummary(index, null);
|
||||
item.msgId = getMsgId(index);
|
||||
item.chatId = getChatId(index);
|
||||
return item;
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long chatlistCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefChatlistCPtr();
|
||||
|
||||
private native long getChatCPtr(int index);
|
||||
|
||||
private native long getMsgCPtr(int index);
|
||||
|
||||
private native long getSummaryCPtr(int index, long chatCPtr);
|
||||
}
|
||||
|
||||
@@ -2,67 +2,82 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcContact {
|
||||
|
||||
public final static int DC_CONTACT_ID_SELF = 1;
|
||||
public final static int DC_CONTACT_ID_INFO = 2;
|
||||
public final static int DC_CONTACT_ID_DEVICE = 5;
|
||||
public final static int DC_CONTACT_ID_LAST_SPECIAL = 9;
|
||||
public final static int DC_CONTACT_ID_NEW_CLASSIC_CONTACT= -1; // used by the UI, not valid to the core
|
||||
public final static int DC_CONTACT_ID_NEW_GROUP = -2; // - " -
|
||||
public final static int DC_CONTACT_ID_ADD_MEMBER = -3; // - " -
|
||||
public final static int DC_CONTACT_ID_QR_INVITE = -4; // - " -
|
||||
public final static int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " -
|
||||
public final static int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " -
|
||||
public final static int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " -
|
||||
public static final int DC_CONTACT_ID_SELF = 1;
|
||||
public static final int DC_CONTACT_ID_INFO = 2;
|
||||
public static final int DC_CONTACT_ID_DEVICE = 5;
|
||||
public static final int DC_CONTACT_ID_LAST_SPECIAL = 9;
|
||||
public static final int DC_CONTACT_ID_NEW_CLASSIC_CONTACT =
|
||||
-1; // used by the UI, not valid to the core
|
||||
public static final int DC_CONTACT_ID_NEW_GROUP = -2; // - " -
|
||||
public static final int DC_CONTACT_ID_ADD_MEMBER = -3; // - " -
|
||||
public static final int DC_CONTACT_ID_QR_INVITE = -4; // - " -
|
||||
public static final int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " -
|
||||
public static final int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " -
|
||||
public static final int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " -
|
||||
|
||||
public DcContact(long contactCPtr) {
|
||||
this.contactCPtr = contactCPtr;
|
||||
public DcContact(long contactCPtr) {
|
||||
this.contactCPtr = contactCPtr;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefContactCPtr();
|
||||
contactCPtr = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof DcContact)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefContactCPtr();
|
||||
contactCPtr = 0;
|
||||
}
|
||||
DcContact that = (DcContact) other;
|
||||
return this.getId() == that.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof DcContact)) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAddr();
|
||||
}
|
||||
|
||||
DcContact that = (DcContact) other;
|
||||
return this.getId()==that.getId();
|
||||
}
|
||||
public native int getId();
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.getId();
|
||||
}
|
||||
public native String getName();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAddr();
|
||||
}
|
||||
public native String getAuthName();
|
||||
|
||||
public native int getId ();
|
||||
public native String getName ();
|
||||
public native String getAuthName ();
|
||||
public native String getDisplayName ();
|
||||
public native String getAddr ();
|
||||
public native String getProfileImage();
|
||||
public native int getColor ();
|
||||
public native String getStatus ();
|
||||
public native long getLastSeen ();
|
||||
public native boolean wasSeenRecently();
|
||||
public native boolean isBlocked ();
|
||||
public native boolean isVerified ();
|
||||
public native boolean isKeyContact ();
|
||||
public native int getVerifierId ();
|
||||
public native boolean isBot ();
|
||||
public native String getDisplayName();
|
||||
|
||||
// working with raw c-data
|
||||
private long contactCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefContactCPtr();
|
||||
public native String getAddr();
|
||||
|
||||
public native String getProfileImage();
|
||||
|
||||
public native int getColor();
|
||||
|
||||
public native String getStatus();
|
||||
|
||||
public native long getLastSeen();
|
||||
|
||||
public native boolean wasSeenRecently();
|
||||
|
||||
public native boolean isBlocked();
|
||||
|
||||
public native boolean isVerified();
|
||||
|
||||
public native boolean isKeyContact();
|
||||
|
||||
public native int getVerifierId();
|
||||
|
||||
public native boolean isBot();
|
||||
|
||||
// working with raw c-data
|
||||
private long contactCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefContactCPtr();
|
||||
}
|
||||
|
||||
@@ -2,288 +2,415 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcContext {
|
||||
|
||||
public final static int DC_EVENT_INFO = 100;
|
||||
public final static int DC_EVENT_WARNING = 300;
|
||||
public final static int DC_EVENT_ERROR = 400;
|
||||
public final static int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410;
|
||||
public final static int DC_EVENT_MSGS_CHANGED = 2000;
|
||||
public final static int DC_EVENT_REACTIONS_CHANGED = 2001;
|
||||
public final static int DC_EVENT_INCOMING_REACTION = 2002;
|
||||
public final static int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003;
|
||||
public final static int DC_EVENT_INCOMING_MSG = 2005;
|
||||
public final static int DC_EVENT_MSGS_NOTICED = 2008;
|
||||
public final static int DC_EVENT_MSG_DELIVERED = 2010;
|
||||
public final static int DC_EVENT_MSG_FAILED = 2012;
|
||||
public final static int DC_EVENT_MSG_READ = 2015;
|
||||
public final static int DC_EVENT_CHAT_MODIFIED = 2020;
|
||||
public final static int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021;
|
||||
public final static int DC_EVENT_CHAT_DELETED = 2023;
|
||||
public final static int DC_EVENT_CONTACTS_CHANGED = 2030;
|
||||
public final static int DC_EVENT_LOCATION_CHANGED = 2035;
|
||||
public final static int DC_EVENT_CONFIGURE_PROGRESS = 2041;
|
||||
public final static int DC_EVENT_IMEX_PROGRESS = 2051;
|
||||
public final static int DC_EVENT_IMEX_FILE_WRITTEN = 2052;
|
||||
public final static int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060;
|
||||
public final static int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061;
|
||||
public final static int DC_EVENT_CONNECTIVITY_CHANGED = 2100;
|
||||
public final static int DC_EVENT_SELFAVATAR_CHANGED = 2110;
|
||||
public final static int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120;
|
||||
public final static int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121;
|
||||
public final static int DC_EVENT_WEBXDC_REALTIME_DATA = 2150;
|
||||
public final static int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200;
|
||||
public final static int DC_EVENT_INCOMING_CALL = 2550;
|
||||
public final static int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560;
|
||||
public final static int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570;
|
||||
public final static int DC_EVENT_CALL_ENDED = 2580;
|
||||
public final static int DC_EVENT_TRANSPORTS_MODIFIED = 2600;
|
||||
public static final int DC_EVENT_INFO = 100;
|
||||
public static final int DC_EVENT_WARNING = 300;
|
||||
public static final int DC_EVENT_ERROR = 400;
|
||||
public static final int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410;
|
||||
public static final int DC_EVENT_MSGS_CHANGED = 2000;
|
||||
public static final int DC_EVENT_REACTIONS_CHANGED = 2001;
|
||||
public static final int DC_EVENT_INCOMING_REACTION = 2002;
|
||||
public static final int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003;
|
||||
public static final int DC_EVENT_INCOMING_MSG = 2005;
|
||||
public static final int DC_EVENT_MSGS_NOTICED = 2008;
|
||||
public static final int DC_EVENT_MSG_DELIVERED = 2010;
|
||||
public static final int DC_EVENT_MSG_FAILED = 2012;
|
||||
public static final int DC_EVENT_MSG_READ = 2015;
|
||||
public static final int DC_EVENT_MSG_DELETED = 2016;
|
||||
public static final int DC_EVENT_CHAT_MODIFIED = 2020;
|
||||
public static final int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021;
|
||||
public static final int DC_EVENT_CHAT_DELETED = 2023;
|
||||
public static final int DC_EVENT_CONTACTS_CHANGED = 2030;
|
||||
public static final int DC_EVENT_LOCATION_CHANGED = 2035;
|
||||
public static final int DC_EVENT_CONFIGURE_PROGRESS = 2041;
|
||||
public static final int DC_EVENT_IMEX_PROGRESS = 2051;
|
||||
public static final int DC_EVENT_IMEX_FILE_WRITTEN = 2052;
|
||||
public static final int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060;
|
||||
public static final int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061;
|
||||
public static final int DC_EVENT_CONNECTIVITY_CHANGED = 2100;
|
||||
public static final int DC_EVENT_SELFAVATAR_CHANGED = 2110;
|
||||
public static final int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120;
|
||||
public static final int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121;
|
||||
public static final int DC_EVENT_WEBXDC_REALTIME_DATA = 2150;
|
||||
public static final int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200;
|
||||
public static final int DC_EVENT_INCOMING_CALL = 2550;
|
||||
public static final int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560;
|
||||
public static final int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570;
|
||||
public static final int DC_EVENT_CALL_ENDED = 2580;
|
||||
public static final int DC_EVENT_TRANSPORTS_MODIFIED = 2600;
|
||||
|
||||
public final static int DC_IMEX_EXPORT_SELF_KEYS = 1;
|
||||
public final static int DC_IMEX_IMPORT_SELF_KEYS = 2;
|
||||
public final static int DC_IMEX_EXPORT_BACKUP = 11;
|
||||
public final static int DC_IMEX_IMPORT_BACKUP = 12;
|
||||
public static final int DC_IMEX_EXPORT_SELF_KEYS = 1;
|
||||
public static final int DC_IMEX_IMPORT_SELF_KEYS = 2;
|
||||
public static final int DC_IMEX_EXPORT_BACKUP = 11;
|
||||
public static final int DC_IMEX_IMPORT_BACKUP = 12;
|
||||
|
||||
public final static int DC_GCL_VERIFIED_ONLY = 1;
|
||||
public final static int DC_GCL_ADD_SELF = 2;
|
||||
public final static int DC_GCL_ADDRESS = 0x04;
|
||||
public final static int DC_GCL_ARCHIVED_ONLY = 0x01;
|
||||
public final static int DC_GCL_NO_SPECIALS = 0x02;
|
||||
public final static int DC_GCL_ADD_ALLDONE_HINT = 0x04;
|
||||
public final static int DC_GCL_FOR_FORWARDING = 0x08;
|
||||
public static final int DC_GCL_VERIFIED_ONLY = 1;
|
||||
public static final int DC_GCL_ADD_SELF = 2;
|
||||
public static final int DC_GCL_ADDRESS = 0x04;
|
||||
public static final int DC_GCL_ARCHIVED_ONLY = 0x01;
|
||||
public static final int DC_GCL_NO_SPECIALS = 0x02;
|
||||
public static final int DC_GCL_ADD_ALLDONE_HINT = 0x04;
|
||||
public static final int DC_GCL_FOR_FORWARDING = 0x08;
|
||||
|
||||
public final static int DC_GCM_ADDDAYMARKER = 0x01;
|
||||
public static final int DC_GCM_ADDDAYMARKER = 0x01;
|
||||
|
||||
public final static int DC_QR_ASK_VERIFYCONTACT = 200;
|
||||
public final static int DC_QR_ASK_VERIFYGROUP = 202;
|
||||
public final static int DC_QR_ASK_JOIN_BROADCAST= 204;
|
||||
public final static int DC_QR_FPR_OK = 210;
|
||||
public final static int DC_QR_FPR_MISMATCH = 220;
|
||||
public final static int DC_QR_FPR_WITHOUT_ADDR = 230;
|
||||
public final static int DC_QR_ACCOUNT = 250;
|
||||
public final static int DC_QR_BACKUP2 = 252;
|
||||
public final static int DC_QR_BACKUP_TOO_NEW = 255;
|
||||
public final static int DC_QR_WEBRTC = 260;
|
||||
public final static int DC_QR_PROXY = 271;
|
||||
public final static int DC_QR_ADDR = 320;
|
||||
public final static int DC_QR_TEXT = 330;
|
||||
public final static int DC_QR_URL = 332;
|
||||
public final static int DC_QR_ERROR = 400;
|
||||
public final static int DC_QR_WITHDRAW_VERIFYCONTACT = 500;
|
||||
public final static int DC_QR_WITHDRAW_VERIFYGROUP = 502;
|
||||
public final static int DC_QR_WITHDRAW_JOINBROADCAST = 504;
|
||||
public final static int DC_QR_REVIVE_VERIFYCONTACT = 510;
|
||||
public final static int DC_QR_REVIVE_VERIFYGROUP = 512;
|
||||
public final static int DC_QR_REVIVE_JOINBROADCAST = 514;
|
||||
public final static int DC_QR_LOGIN = 520;
|
||||
public static final int DC_QR_ASK_VERIFYCONTACT = 200;
|
||||
public static final int DC_QR_ASK_VERIFYGROUP = 202;
|
||||
public static final int DC_QR_ASK_JOIN_BROADCAST = 204;
|
||||
public static final int DC_QR_FPR_OK = 210;
|
||||
public static final int DC_QR_FPR_MISMATCH = 220;
|
||||
public static final int DC_QR_FPR_WITHOUT_ADDR = 230;
|
||||
public static final int DC_QR_ACCOUNT = 250;
|
||||
public static final int DC_QR_BACKUP2 = 252;
|
||||
public static final int DC_QR_BACKUP_TOO_NEW = 255;
|
||||
public static final int DC_QR_WEBRTC = 260;
|
||||
public static final int DC_QR_PROXY = 271;
|
||||
public static final int DC_QR_ADDR = 320;
|
||||
public static final int DC_QR_TEXT = 330;
|
||||
public static final int DC_QR_URL = 332;
|
||||
public static final int DC_QR_ERROR = 400;
|
||||
public static final int DC_QR_WITHDRAW_VERIFYCONTACT = 500;
|
||||
public static final int DC_QR_WITHDRAW_VERIFYGROUP = 502;
|
||||
public static final int DC_QR_WITHDRAW_JOINBROADCAST = 504;
|
||||
public static final int DC_QR_REVIVE_VERIFYCONTACT = 510;
|
||||
public static final int DC_QR_REVIVE_VERIFYGROUP = 512;
|
||||
public static final int DC_QR_REVIVE_JOINBROADCAST = 514;
|
||||
public static final int DC_QR_LOGIN = 520;
|
||||
|
||||
public final static int DC_SOCKET_AUTO = 0;
|
||||
public final static int DC_SOCKET_SSL = 1;
|
||||
public final static int DC_SOCKET_STARTTLS = 2;
|
||||
public final static int DC_SOCKET_PLAIN = 3;
|
||||
public static final int DC_SOCKET_AUTO = 0;
|
||||
public static final int DC_SOCKET_SSL = 1;
|
||||
public static final int DC_SOCKET_STARTTLS = 2;
|
||||
public static final int DC_SOCKET_PLAIN = 3;
|
||||
|
||||
public final static int DC_SHOW_EMAILS_OFF = 0;
|
||||
public final static int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1;
|
||||
public final static int DC_SHOW_EMAILS_ALL = 2;
|
||||
public static final int DC_SHOW_EMAILS_OFF = 0;
|
||||
public static final int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1;
|
||||
public static final int DC_SHOW_EMAILS_ALL = 2;
|
||||
|
||||
public final static int DC_MEDIA_QUALITY_BALANCED = 0;
|
||||
public final static int DC_MEDIA_QUALITY_WORSE = 1;
|
||||
public static final int DC_MEDIA_QUALITY_BALANCED = 0;
|
||||
public static final int DC_MEDIA_QUALITY_WORSE = 1;
|
||||
|
||||
public final static int DC_CONNECTIVITY_NOT_CONNECTED = 1000;
|
||||
public final static int DC_CONNECTIVITY_CONNECTING = 2000;
|
||||
public final static int DC_CONNECTIVITY_WORKING = 3000;
|
||||
public final static int DC_CONNECTIVITY_CONNECTED = 4000;
|
||||
public static final int DC_CONNECTIVITY_NOT_CONNECTED = 1000;
|
||||
public static final int DC_CONNECTIVITY_CONNECTING = 2000;
|
||||
public static final int DC_CONNECTIVITY_WORKING = 3000;
|
||||
public static final int DC_CONNECTIVITY_CONNECTED = 4000;
|
||||
|
||||
private static final String CONFIG_ACCOUNT_ENABLED = "ui.enabled";
|
||||
private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted";
|
||||
private static final String CONFIG_ACCOUNT_ENABLED = "ui.enabled";
|
||||
private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted";
|
||||
|
||||
// when using DcAccounts, use Rpc.addAccount() instead
|
||||
public DcContext(String osName, String dbfile) {
|
||||
contextCPtr = createContextCPtr(osName, dbfile);
|
||||
// when using DcAccounts, use Rpc.addAccount() instead
|
||||
public DcContext(String osName, String dbfile) {
|
||||
contextCPtr = createContextCPtr(osName, dbfile);
|
||||
}
|
||||
|
||||
public DcContext(long contextCPtr) {
|
||||
this.contextCPtr = contextCPtr;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return contextCPtr != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
if (contextCPtr != 0) {
|
||||
unrefContextCPtr();
|
||||
contextCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public DcContext(long contextCPtr) {
|
||||
this.contextCPtr = contextCPtr;
|
||||
public native int getAccountId();
|
||||
|
||||
// when using DcAccounts, use DcAccounts.getEventEmitter() instead
|
||||
public DcEventEmitter getEventEmitter() {
|
||||
return new DcEventEmitter(getEventEmitterCPtr());
|
||||
}
|
||||
|
||||
public native void setStockTranslation(int stockId, String translation);
|
||||
|
||||
public native String getBlobdir();
|
||||
|
||||
public native String getLastError();
|
||||
|
||||
public native void stopOngoingProcess();
|
||||
|
||||
public native int isConfigured();
|
||||
|
||||
public native boolean open(String passphrase);
|
||||
|
||||
public native boolean isOpen();
|
||||
|
||||
// when using DcAccounts, use DcAccounts.startIo() instead
|
||||
public native void startIo();
|
||||
|
||||
// when using DcAccounts, use DcAccounts.stopIo() instead
|
||||
public native void stopIo();
|
||||
|
||||
// when using DcAccounts, use DcAccounts.maybeNetwork() instead
|
||||
public native void maybeNetwork();
|
||||
|
||||
public native void setConfig(String key, String value);
|
||||
|
||||
public void setConfigInt(String key, int value) {
|
||||
setConfig(key, Integer.toString(value));
|
||||
}
|
||||
|
||||
public native boolean setConfigFromQr(String qr);
|
||||
|
||||
public native String getConfig(String key);
|
||||
|
||||
public int getConfigInt(String key) {
|
||||
return getConfigInt(key, 0);
|
||||
}
|
||||
|
||||
public int getConfigInt(String key, int defValue) {
|
||||
try {
|
||||
return Integer.parseInt(getConfig(key));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return defValue;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return contextCPtr != 0;
|
||||
}
|
||||
public native String getInfo();
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
if (contextCPtr != 0) {
|
||||
unrefContextCPtr();
|
||||
contextCPtr = 0;
|
||||
}
|
||||
}
|
||||
public native int getConnectivity();
|
||||
|
||||
public native int getAccountId ();
|
||||
public native String getConnectivityHtml();
|
||||
|
||||
// when using DcAccounts, use DcAccounts.getEventEmitter() instead
|
||||
public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); }
|
||||
public native String initiateKeyTransfer();
|
||||
|
||||
public native void setStockTranslation (int stockId, String translation);
|
||||
public native String getBlobdir ();
|
||||
public native String getLastError ();
|
||||
public native void stopOngoingProcess ();
|
||||
public native int isConfigured ();
|
||||
public native boolean open (String passphrase);
|
||||
public native boolean isOpen ();
|
||||
public native void imex(int what, String dir);
|
||||
|
||||
// when using DcAccounts, use DcAccounts.startIo() instead
|
||||
public native void startIo ();
|
||||
public native String imexHasBackup(String dir);
|
||||
|
||||
// when using DcAccounts, use DcAccounts.stopIo() instead
|
||||
public native void stopIo ();
|
||||
public DcBackupProvider newBackupProvider() {
|
||||
return new DcBackupProvider(newBackupProviderCPtr());
|
||||
}
|
||||
|
||||
// when using DcAccounts, use DcAccounts.maybeNetwork() instead
|
||||
public native void maybeNetwork ();
|
||||
public native boolean receiveBackup(String qr);
|
||||
|
||||
public native void setConfig (String key, String value);
|
||||
public void setConfigInt (String key, int value) { setConfig(key, Integer.toString(value)); }
|
||||
public native boolean setConfigFromQr (String qr);
|
||||
public native String getConfig (String key);
|
||||
public int getConfigInt (String key) { return getConfigInt(key, 0); }
|
||||
public int getConfigInt (String key, int defValue) { try{return Integer.parseInt(getConfig(key));} catch(Exception e) {} return defValue; }
|
||||
public native String getInfo ();
|
||||
public native int getConnectivity ();
|
||||
public native String getConnectivityHtml ();
|
||||
public native String initiateKeyTransfer ();
|
||||
public native void imex (int what, String dir);
|
||||
public native String imexHasBackup (String dir);
|
||||
public DcBackupProvider newBackupProvider () { return new DcBackupProvider(newBackupProviderCPtr()); }
|
||||
public native boolean receiveBackup (String qr);
|
||||
public native boolean mayBeValidAddr (String addr);
|
||||
public native int lookupContactIdByAddr(String addr);
|
||||
public native int[] getContacts (int flags, String query);
|
||||
public native int[] getBlockedContacts ();
|
||||
public DcContact getContact (int contact_id) { return new DcContact(getContactCPtr(contact_id)); }
|
||||
public native int createContact (String name, String addr);
|
||||
public native void blockContact (int id, int block);
|
||||
public native String getContactEncrInfo (int contact_id);
|
||||
public native boolean deleteContact (int id);
|
||||
public native int addAddressBook (String adrbook);
|
||||
public DcChatlist getChatlist (int listflags, String query, int queryId) { return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId)); }
|
||||
public DcChat getChat (int chat_id) { return new DcChat(getAccountId(), getChatCPtr(chat_id)); }
|
||||
public native String getChatEncrInfo (int chat_id);
|
||||
public native void markseenMsgs (int msg_ids[]);
|
||||
public native void marknoticedChat (int chat_id);
|
||||
public native void setChatVisibility (int chat_id, int visibility);
|
||||
public native int getChatIdByContactId (int contact_id);
|
||||
public native int createChatByContactId(int contact_id);
|
||||
public native int createGroupChat (String name);
|
||||
public native int createBroadcastList ();
|
||||
public native boolean isContactInChat (int chat_id, int contact_id);
|
||||
public native int addContactToChat (int chat_id, int contact_id);
|
||||
public native int removeContactFromChat(int chat_id, int contact_id);
|
||||
public native void setDraft (int chat_id, DcMsg msg/*null=delete*/);
|
||||
public DcMsg getDraft (int chat_id) { return new DcMsg(getDraftCPtr(chat_id)); }
|
||||
public native int setChatName (int chat_id, String name);
|
||||
public native int setChatProfileImage (int chat_id, String name);
|
||||
public native int[] getChatMsgs (int chat_id, int flags, int marker1before);
|
||||
public native int[] searchMsgs (int chat_id, String query);
|
||||
public native int[] getFreshMsgs ();
|
||||
public native int[] getChatMedia (int chat_id, int type1, int type2, int type3);
|
||||
public native int[] getChatContacts (int chat_id);
|
||||
public native int getChatEphemeralTimer (int chat_id);
|
||||
public native boolean setChatEphemeralTimer (int chat_id, int timer);
|
||||
public native boolean setChatMuteDuration (int chat_id, long duration);
|
||||
public native void deleteChat (int chat_id);
|
||||
public native void blockChat (int chat_id);
|
||||
public native void acceptChat (int chat_id);
|
||||
public DcMsg getMsg (int msg_id) { return new DcMsg(getMsgCPtr(msg_id)); }
|
||||
public native void sendEditRequest (int msg_id, String text);
|
||||
public native String getMsgInfo (int id);
|
||||
public native String getMsgHtml (int msg_id);
|
||||
public native void downloadFullMsg (int msg_id);
|
||||
public native int getFreshMsgCount (int chat_id);
|
||||
public native int estimateDeletionCount(boolean from_server, long seconds);
|
||||
public native void deleteMsgs (int msg_ids[]);
|
||||
public native void sendDeleteRequest (int msg_ids[]);
|
||||
public native void forwardMsgs (int msg_ids[], int chat_id);
|
||||
public native void saveMsgs (int msg_ids[]);
|
||||
public native boolean resendMsgs (int msg_ids[]);
|
||||
public native int sendMsg (int chat_id, DcMsg msg);
|
||||
public native int sendTextMsg (int chat_id, String text);
|
||||
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload);
|
||||
public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial);
|
||||
public native void setWebxdcIntegration (String file);
|
||||
public native int initWebxdcIntegration(int chat_id);
|
||||
public native int addDeviceMsg (String label, DcMsg msg);
|
||||
public native boolean wasDeviceMsgEverAdded(String label);
|
||||
public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); }
|
||||
public native String getSecurejoinQr (int chat_id);
|
||||
public native String getSecurejoinQrSvg (int chat_id);
|
||||
public native String createQrSvg (String payload);
|
||||
public native int joinSecurejoin (String qr);
|
||||
public native void sendLocationsToChat (int chat_id, int seconds);
|
||||
public native boolean isSendingLocationsToChat(int chat_id);
|
||||
public DcProvider getProviderFromEmailWithDns (String email) { long cptr = getProviderFromEmailWithDnsCPtr(email); return cptr!=0 ? new DcProvider(cptr) : null; }
|
||||
public native boolean mayBeValidAddr(String addr);
|
||||
|
||||
public boolean isEnabled() {
|
||||
return !"0".equals(getConfig(CONFIG_ACCOUNT_ENABLED));
|
||||
}
|
||||
public native int lookupContactIdByAddr(String addr);
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
setConfigInt(CONFIG_ACCOUNT_ENABLED, enabled? 1 : 0);
|
||||
if (enabled) {
|
||||
startIo();
|
||||
} else {
|
||||
stopIo();
|
||||
}
|
||||
}
|
||||
public native int[] getContacts(int flags, String query);
|
||||
|
||||
public boolean isMentionsEnabled() {
|
||||
return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1;
|
||||
}
|
||||
public native int[] getBlockedContacts();
|
||||
|
||||
public void setMentionsEnabled(boolean enabled) {
|
||||
setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled? 0 : 1);
|
||||
}
|
||||
public DcContact getContact(int contact_id) {
|
||||
return new DcContact(getContactCPtr(contact_id));
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
String displayname = getConfig("displayname");
|
||||
if (displayname.isEmpty()) {
|
||||
displayname = getContact(DcContact.DC_CONTACT_ID_SELF).getAddr();
|
||||
}
|
||||
return displayname;
|
||||
}
|
||||
public native int createContact(String name, String addr);
|
||||
|
||||
public boolean isChatmail() {
|
||||
return getConfigInt("is_chatmail") == 1;
|
||||
}
|
||||
public native void blockContact(int id, int block);
|
||||
|
||||
public boolean isMuted() {
|
||||
return getConfigInt("is_muted") == 1;
|
||||
}
|
||||
public native String getContactEncrInfo(int contact_id);
|
||||
|
||||
public void setMuted(boolean muted) {
|
||||
setConfigInt("is_muted", muted? 1 : 0);
|
||||
}
|
||||
public native boolean deleteContact(int id);
|
||||
|
||||
public void restartIo() {
|
||||
if (!isEnabled()) return;
|
||||
stopIo();
|
||||
public native int addAddressBook(String adrbook);
|
||||
|
||||
public DcChatlist getChatlist(int listflags, String query, int queryId) {
|
||||
return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId));
|
||||
}
|
||||
|
||||
public DcChat getChat(int chat_id) {
|
||||
return new DcChat(getAccountId(), getChatCPtr(chat_id));
|
||||
}
|
||||
|
||||
public native String getChatEncrInfo(int chat_id);
|
||||
|
||||
public native void markseenMsgs(int msg_ids[]);
|
||||
|
||||
public native void marknoticedChat(int chat_id);
|
||||
|
||||
public native void setChatVisibility(int chat_id, int visibility);
|
||||
|
||||
public native int getChatIdByContactId(int contact_id);
|
||||
|
||||
public native int createChatByContactId(int contact_id);
|
||||
|
||||
public native int createGroupChat(String name);
|
||||
|
||||
public native int createBroadcastList();
|
||||
|
||||
public native boolean isContactInChat(int chat_id, int contact_id);
|
||||
|
||||
public native int addContactToChat(int chat_id, int contact_id);
|
||||
|
||||
public native int removeContactFromChat(int chat_id, int contact_id);
|
||||
|
||||
public native void setDraft(int chat_id, DcMsg msg /*null=delete*/);
|
||||
|
||||
public DcMsg getDraft(int chat_id) {
|
||||
return new DcMsg(getDraftCPtr(chat_id));
|
||||
}
|
||||
|
||||
public native int setChatName(int chat_id, String name);
|
||||
|
||||
public native int setChatProfileImage(int chat_id, String name);
|
||||
|
||||
public native int[] getChatMsgs(int chat_id, int flags, int marker1before);
|
||||
|
||||
public native int[] searchMsgs(int chat_id, String query);
|
||||
|
||||
public native int[] getFreshMsgs();
|
||||
|
||||
public native int[] getChatMedia(int chat_id, int type1, int type2, int type3);
|
||||
|
||||
public native int[] getChatContacts(int chat_id);
|
||||
|
||||
public native int getChatEphemeralTimer(int chat_id);
|
||||
|
||||
public native boolean setChatEphemeralTimer(int chat_id, int timer);
|
||||
|
||||
public native boolean setChatMuteDuration(int chat_id, long duration);
|
||||
|
||||
public native void deleteChat(int chat_id);
|
||||
|
||||
public native void blockChat(int chat_id);
|
||||
|
||||
public native void acceptChat(int chat_id);
|
||||
|
||||
public DcMsg getMsg(int msg_id) {
|
||||
return new DcMsg(getMsgCPtr(msg_id));
|
||||
}
|
||||
|
||||
public native void sendEditRequest(int msg_id, String text);
|
||||
|
||||
public native String getMsgInfo(int id);
|
||||
|
||||
public native String getMsgHtml(int msg_id);
|
||||
|
||||
public native void downloadFullMsg(int msg_id);
|
||||
|
||||
public native int getFreshMsgCount(int chat_id);
|
||||
|
||||
public native int estimateDeletionCount(boolean from_server, long seconds);
|
||||
|
||||
public native void deleteMsgs(int msg_ids[]);
|
||||
|
||||
public native void sendDeleteRequest(int msg_ids[]);
|
||||
|
||||
public native void forwardMsgs(int msg_ids[], int chat_id);
|
||||
|
||||
public native void saveMsgs(int msg_ids[]);
|
||||
|
||||
public native boolean resendMsgs(int msg_ids[]);
|
||||
|
||||
public native int sendMsg(int chat_id, DcMsg msg);
|
||||
|
||||
public native int sendTextMsg(int chat_id, String text);
|
||||
|
||||
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload);
|
||||
|
||||
public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial);
|
||||
|
||||
public native void setWebxdcIntegration(String file);
|
||||
|
||||
public native int initWebxdcIntegration(int chat_id);
|
||||
|
||||
public native int addDeviceMsg(String label, DcMsg msg);
|
||||
|
||||
public native boolean wasDeviceMsgEverAdded(String label);
|
||||
|
||||
public DcLot checkQr(String qr) {
|
||||
return new DcLot(checkQrCPtr(qr));
|
||||
}
|
||||
|
||||
public native String getSecurejoinQr(int chat_id);
|
||||
|
||||
public native String getSecurejoinQrSvg(int chat_id);
|
||||
|
||||
public native String createQrSvg(String payload);
|
||||
|
||||
public native int joinSecurejoin(String qr);
|
||||
|
||||
public native void sendLocationsToChat(int chat_id, int seconds);
|
||||
|
||||
public native boolean isSendingLocationsToChat(int chat_id);
|
||||
|
||||
public DcProvider getProviderFromEmailWithDns(String email) {
|
||||
long cptr = getProviderFromEmailWithDnsCPtr(email);
|
||||
return cptr != 0 ? new DcProvider(cptr) : null;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return !"0".equals(getConfig(CONFIG_ACCOUNT_ENABLED));
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
setConfigInt(CONFIG_ACCOUNT_ENABLED, enabled ? 1 : 0);
|
||||
if (enabled) {
|
||||
startIo();
|
||||
} else {
|
||||
stopIo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if at least one chat has location streaming enabled
|
||||
*/
|
||||
public native boolean setLocation (float latitude, float longitude, float accuracy);
|
||||
public boolean isMentionsEnabled() {
|
||||
return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1;
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long contextCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native long createContextCPtr(String osName, String dbfile);
|
||||
private native void unrefContextCPtr ();
|
||||
private native long getEventEmitterCPtr();
|
||||
public native long createMsgCPtr (int viewtype);
|
||||
private native long getChatlistCPtr (int listflags, String query, int queryId);
|
||||
private native long getChatCPtr (int chat_id);
|
||||
private native long getMsgCPtr (int id);
|
||||
private native long getDraftCPtr (int id);
|
||||
private native long getContactCPtr (int id);
|
||||
private native long checkQrCPtr (String qr);
|
||||
private native long getProviderFromEmailWithDnsCPtr (String addr);
|
||||
private native long newBackupProviderCPtr();
|
||||
public void setMentionsEnabled(boolean enabled) {
|
||||
setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled ? 0 : 1);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
String displayname = getConfig("displayname");
|
||||
if (displayname.isEmpty()) {
|
||||
displayname = getConfig("addr");
|
||||
}
|
||||
return displayname;
|
||||
}
|
||||
|
||||
public boolean isChatmail() {
|
||||
return getConfigInt("is_chatmail") == 1;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return getConfigInt("is_muted") == 1;
|
||||
}
|
||||
|
||||
public void setMuted(boolean muted) {
|
||||
setConfigInt("is_muted", muted ? 1 : 0);
|
||||
}
|
||||
|
||||
public void restartIo() {
|
||||
if (!isEnabled()) return;
|
||||
stopIo();
|
||||
startIo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if at least one chat has location streaming enabled
|
||||
*/
|
||||
public native boolean setLocation(float latitude, float longitude, float accuracy);
|
||||
|
||||
// working with raw c-data
|
||||
private long contextCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native long createContextCPtr(String osName, String dbfile);
|
||||
|
||||
private native void unrefContextCPtr();
|
||||
|
||||
private native long getEventEmitterCPtr();
|
||||
|
||||
public native long createMsgCPtr(int viewtype);
|
||||
|
||||
private native long getChatlistCPtr(int listflags, String query, int queryId);
|
||||
|
||||
private native long getChatCPtr(int chat_id);
|
||||
|
||||
private native long getMsgCPtr(int id);
|
||||
|
||||
private native long getDraftCPtr(int id);
|
||||
|
||||
private native long getContactCPtr(int id);
|
||||
|
||||
private native long checkQrCPtr(String qr);
|
||||
|
||||
private native long getProviderFromEmailWithDnsCPtr(String addr);
|
||||
|
||||
private native long newBackupProviderCPtr();
|
||||
}
|
||||
|
||||
@@ -2,24 +2,31 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcEvent {
|
||||
|
||||
public DcEvent(long eventCPtr) {
|
||||
this.eventCPtr = eventCPtr;
|
||||
}
|
||||
public DcEvent(long eventCPtr) {
|
||||
this.eventCPtr = eventCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefEventCPtr();
|
||||
eventCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefEventCPtr();
|
||||
eventCPtr = 0;
|
||||
}
|
||||
|
||||
public native int getId ();
|
||||
public native int getData1Int ();
|
||||
public native int getData2Int ();
|
||||
public native String getData2Str ();
|
||||
public native byte[] getData2Blob();
|
||||
public native int getAccountId();
|
||||
public native int getId();
|
||||
|
||||
// working with raw c-data
|
||||
private long eventCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefEventCPtr();
|
||||
public native int getData1Int();
|
||||
|
||||
public native int getData2Int();
|
||||
|
||||
public native String getData2Str();
|
||||
|
||||
public native byte[] getData2Blob();
|
||||
|
||||
public native int getAccountId();
|
||||
|
||||
// working with raw c-data
|
||||
private long eventCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefEventCPtr();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
public class DcEventChannel {
|
||||
|
||||
public DcEventChannel() {
|
||||
eventChannelCPtr = createEventChannelCPtr();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
if (eventChannelCPtr != 0) {
|
||||
unrefEventChannelCPtr();
|
||||
eventChannelCPtr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public DcEventEmitter getEventEmitter() {
|
||||
return new DcEventEmitter(getEventEmitterCPtr());
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long eventChannelCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native long createEventChannelCPtr();
|
||||
|
||||
private native void unrefEventChannelCPtr();
|
||||
|
||||
private native long getEventEmitterCPtr();
|
||||
}
|
||||
@@ -2,23 +2,26 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcEventEmitter {
|
||||
|
||||
public DcEventEmitter(long eventEmitterCPtr) {
|
||||
this.eventEmitterCPtr = eventEmitterCPtr;
|
||||
}
|
||||
public DcEventEmitter(long eventEmitterCPtr) {
|
||||
this.eventEmitterCPtr = eventEmitterCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefEventEmitterCPtr();
|
||||
eventEmitterCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefEventEmitterCPtr();
|
||||
eventEmitterCPtr = 0;
|
||||
}
|
||||
|
||||
public DcEvent getNextEvent () {
|
||||
long eventCPtr = getNextEventCPtr();
|
||||
return eventCPtr == 0 ? null : new DcEvent(eventCPtr);
|
||||
}
|
||||
public DcEvent getNextEvent() {
|
||||
long eventCPtr = getNextEventCPtr();
|
||||
return eventCPtr == 0 ? null : new DcEvent(eventCPtr);
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native long getNextEventCPtr ();
|
||||
private native void unrefEventEmitterCPtr();
|
||||
// working with raw c-data
|
||||
private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native long getNextEventCPtr();
|
||||
|
||||
private native void unrefEventEmitterCPtr();
|
||||
}
|
||||
|
||||
@@ -2,20 +2,23 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcJsonrpcInstance {
|
||||
|
||||
public DcJsonrpcInstance(long jsonrpcInstanceCPtr) {
|
||||
this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr;
|
||||
}
|
||||
public DcJsonrpcInstance(long jsonrpcInstanceCPtr) {
|
||||
this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefJsonrpcInstanceCPtr();
|
||||
jsonrpcInstanceCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefJsonrpcInstanceCPtr();
|
||||
jsonrpcInstanceCPtr = 0;
|
||||
}
|
||||
|
||||
public native void request(String request);
|
||||
public native String getNextResponse();
|
||||
public native void request(String request);
|
||||
|
||||
// working with raw c-data
|
||||
private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefJsonrpcInstanceCPtr();
|
||||
public native String getNextResponse();
|
||||
|
||||
// working with raw c-data
|
||||
private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefJsonrpcInstanceCPtr();
|
||||
}
|
||||
|
||||
@@ -2,28 +2,35 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcLot {
|
||||
|
||||
public final static int DC_TEXT1_DRAFT = 1;
|
||||
public final static int DC_TEXT1_USERNAME = 2;
|
||||
public final static int DC_TEXT1_SELF = 3;
|
||||
public static final int DC_TEXT1_DRAFT = 1;
|
||||
public static final int DC_TEXT1_USERNAME = 2;
|
||||
public static final int DC_TEXT1_SELF = 3;
|
||||
|
||||
public DcLot(long lotCPtr) {
|
||||
this.lotCPtr = lotCPtr;
|
||||
}
|
||||
public DcLot(long lotCPtr) {
|
||||
this.lotCPtr = lotCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefLotCPtr();
|
||||
lotCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefLotCPtr();
|
||||
lotCPtr = 0;
|
||||
}
|
||||
|
||||
public native String getText1 ();
|
||||
public native int getText1Meaning();
|
||||
public native String getText2 ();
|
||||
public native long getTimestamp ();
|
||||
public native int getState ();
|
||||
public native int getId ();
|
||||
public native String getText1();
|
||||
|
||||
// working with raw c-data
|
||||
private long lotCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefLotCPtr();
|
||||
public native int getText1Meaning();
|
||||
|
||||
public native String getText2();
|
||||
|
||||
public native long getTimestamp();
|
||||
|
||||
public native int getState();
|
||||
|
||||
public native int getId();
|
||||
|
||||
// working with raw c-data
|
||||
private long lotCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefLotCPtr();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
/**
|
||||
* Contains a list of media entries, their respective positions and ability to move through it.
|
||||
*/
|
||||
/** Contains a list of media entries, their respective positions and ability to move through it. */
|
||||
public class DcMediaGalleryElement {
|
||||
|
||||
final int[] mediaMsgs;
|
||||
int position;
|
||||
final DcContext context;
|
||||
public DcMediaGalleryElement(int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) {
|
||||
|
||||
public DcMediaGalleryElement(
|
||||
int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) {
|
||||
this.mediaMsgs = mediaMsgs;
|
||||
this.position = position;
|
||||
this.context = context;
|
||||
@@ -33,7 +33,7 @@ public class DcMediaGalleryElement {
|
||||
}
|
||||
|
||||
public void moveToPosition(int newPosition) {
|
||||
if(newPosition < 0 || newPosition >= mediaMsgs.length)
|
||||
if (newPosition < 0 || newPosition >= mediaMsgs.length)
|
||||
throw new IllegalArgumentException("can't move outside of known area.");
|
||||
position = newPosition;
|
||||
}
|
||||
|
||||
@@ -1,266 +1,332 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Set;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class DcMsg {
|
||||
|
||||
public final static int DC_MSG_UNDEFINED = 0;
|
||||
public final static int DC_MSG_TEXT = 10;
|
||||
public final static int DC_MSG_IMAGE = 20;
|
||||
public final static int DC_MSG_GIF = 21;
|
||||
public final static int DC_MSG_STICKER = 23;
|
||||
public final static int DC_MSG_AUDIO = 40;
|
||||
public final static int DC_MSG_VOICE = 41;
|
||||
public final static int DC_MSG_VIDEO = 50;
|
||||
public final static int DC_MSG_FILE = 60;
|
||||
public final static int DC_MSG_CALL = 71;
|
||||
public final static int DC_MSG_WEBXDC = 80;
|
||||
public final static int DC_MSG_VCARD = 90;
|
||||
public static final int DC_MSG_UNDEFINED = 0;
|
||||
public static final int DC_MSG_TEXT = 10;
|
||||
public static final int DC_MSG_IMAGE = 20;
|
||||
public static final int DC_MSG_GIF = 21;
|
||||
public static final int DC_MSG_STICKER = 23;
|
||||
public static final int DC_MSG_AUDIO = 40;
|
||||
public static final int DC_MSG_VOICE = 41;
|
||||
public static final int DC_MSG_VIDEO = 50;
|
||||
public static final int DC_MSG_FILE = 60;
|
||||
public static final int DC_MSG_CALL = 71;
|
||||
public static final int DC_MSG_WEBXDC = 80;
|
||||
public static final int DC_MSG_VCARD = 90;
|
||||
|
||||
public final static int DC_INFO_UNKNOWN = 0;
|
||||
public final static int DC_INFO_GROUP_NAME_CHANGED = 2;
|
||||
public final static int DC_INFO_GROUP_IMAGE_CHANGED = 3;
|
||||
public final static int DC_INFO_MEMBER_ADDED_TO_GROUP = 4;
|
||||
public final static int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5;
|
||||
public final static int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6;
|
||||
public final static int DC_INFO_SECURE_JOIN_MESSAGE = 7;
|
||||
public final static int DC_INFO_LOCATIONSTREAMING_ENABLED = 8;
|
||||
public final static int DC_INFO_LOCATION_ONLY = 9;
|
||||
public final static int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10;
|
||||
public final static int DC_INFO_PROTECTION_ENABLED = 11;
|
||||
public final static int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13;
|
||||
public final static int DC_INFO_WEBXDC_INFO_MESSAGE = 32;
|
||||
public final static int DC_INFO_CHAT_E2EE = 50;
|
||||
public static final int DC_INFO_UNKNOWN = 0;
|
||||
public static final int DC_INFO_GROUP_NAME_CHANGED = 2;
|
||||
public static final int DC_INFO_GROUP_IMAGE_CHANGED = 3;
|
||||
public static final int DC_INFO_MEMBER_ADDED_TO_GROUP = 4;
|
||||
public static final int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5;
|
||||
public static final int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6;
|
||||
public static final int DC_INFO_SECURE_JOIN_MESSAGE = 7;
|
||||
public static final int DC_INFO_LOCATIONSTREAMING_ENABLED = 8;
|
||||
public static final int DC_INFO_LOCATION_ONLY = 9;
|
||||
public static final int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10;
|
||||
public static final int DC_INFO_PROTECTION_ENABLED = 11;
|
||||
public static final int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13;
|
||||
public static final int DC_INFO_WEBXDC_INFO_MESSAGE = 32;
|
||||
public static final int DC_INFO_CHAT_E2EE = 50;
|
||||
public static final int DC_INFO_CHAT_DESCRIPTION_CHANGED = 70;
|
||||
|
||||
public final static int DC_STATE_UNDEFINED = 0;
|
||||
public final static int DC_STATE_IN_FRESH = 10;
|
||||
public final static int DC_STATE_IN_NOTICED = 13;
|
||||
public final static int DC_STATE_IN_SEEN = 16;
|
||||
public final static int DC_STATE_OUT_PREPARING = 18;
|
||||
public final static int DC_STATE_OUT_DRAFT = 19;
|
||||
public final static int DC_STATE_OUT_PENDING = 20;
|
||||
public final static int DC_STATE_OUT_FAILED = 24;
|
||||
public final static int DC_STATE_OUT_DELIVERED = 26;
|
||||
public final static int DC_STATE_OUT_MDN_RCVD = 28;
|
||||
public static final int DC_STATE_UNDEFINED = 0;
|
||||
public static final int DC_STATE_IN_FRESH = 10;
|
||||
public static final int DC_STATE_IN_NOTICED = 13;
|
||||
public static final int DC_STATE_IN_SEEN = 16;
|
||||
public static final int DC_STATE_OUT_PREPARING = 18;
|
||||
public static final int DC_STATE_OUT_DRAFT = 19;
|
||||
public static final int DC_STATE_OUT_PENDING = 20;
|
||||
public static final int DC_STATE_OUT_FAILED = 24;
|
||||
public static final int DC_STATE_OUT_DELIVERED = 26;
|
||||
public static final int DC_STATE_OUT_MDN_RCVD = 28;
|
||||
|
||||
public final static int DC_DOWNLOAD_DONE = 0;
|
||||
public final static int DC_DOWNLOAD_AVAILABLE = 10;
|
||||
public final static int DC_DOWNLOAD_FAILURE = 20;
|
||||
public final static int DC_DOWNLOAD_UNDECIPHERABLE = 30;
|
||||
public final static int DC_DOWNLOAD_IN_PROGRESS = 1000;
|
||||
public static final int DC_DOWNLOAD_DONE = 0;
|
||||
public static final int DC_DOWNLOAD_AVAILABLE = 10;
|
||||
public static final int DC_DOWNLOAD_FAILURE = 20;
|
||||
public static final int DC_DOWNLOAD_UNDECIPHERABLE = 30;
|
||||
public static final int DC_DOWNLOAD_IN_PROGRESS = 1000;
|
||||
|
||||
public static final int DC_MSG_NO_ID = 0;
|
||||
public final static int DC_MSG_ID_MARKER1 = 1;
|
||||
public final static int DC_MSG_ID_DAYMARKER = 9;
|
||||
public static final int DC_MSG_NO_ID = 0;
|
||||
public static final int DC_MSG_ID_MARKER1 = 1;
|
||||
public static final int DC_MSG_ID_DAYMARKER = 9;
|
||||
|
||||
public final static int DC_VIDEOCHATTYPE_UNKNOWN = 0;
|
||||
public final static int DC_VIDEOCHATTYPE_BASICWEBRTC = 1;
|
||||
public static final int DC_VIDEOCHATTYPE_UNKNOWN = 0;
|
||||
public static final int DC_VIDEOCHATTYPE_BASICWEBRTC = 1;
|
||||
|
||||
private static final String TAG = DcMsg.class.getSimpleName();
|
||||
private static final String TAG = DcMsg.class.getSimpleName();
|
||||
|
||||
public DcMsg(DcContext context, int viewtype) {
|
||||
msgCPtr = context.createMsgCPtr(viewtype);
|
||||
public DcMsg(DcContext context, int viewtype) {
|
||||
msgCPtr = context.createMsgCPtr(viewtype);
|
||||
}
|
||||
|
||||
public DcMsg(long msgCPtr) {
|
||||
this.msgCPtr = msgCPtr;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return msgCPtr != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefMsgCPtr();
|
||||
msgCPtr = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof DcMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public DcMsg(long msgCPtr) {
|
||||
this.msgCPtr = msgCPtr;
|
||||
}
|
||||
DcMsg that = (DcMsg) other;
|
||||
return this.getId() == that.getId() && this.getId() != 0;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return msgCPtr != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefMsgCPtr();
|
||||
msgCPtr = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof DcMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DcMsg that = (DcMsg) other;
|
||||
return this.getId()==that.getId() && this.getId()!=0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If given a message, calculates the position of the message in the chat
|
||||
*/
|
||||
public static int getMessagePosition(DcMsg msg, DcContext dcContext) {
|
||||
int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0);
|
||||
int startingPosition = -1;
|
||||
int msgId = msg.getId();
|
||||
for (int i = 0; i < msgs.length; i++) {
|
||||
if (msgs[i] == msgId) {
|
||||
startingPosition = msgs.length - 1 - i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return startingPosition;
|
||||
}
|
||||
|
||||
public native int getId ();
|
||||
public native String getText ();
|
||||
public native String getSubject ();
|
||||
public native long getTimestamp ();
|
||||
public native long getSortTimestamp ();
|
||||
public native boolean hasDeviatingTimestamp();
|
||||
public native boolean hasLocation ();
|
||||
private native int getViewType ();
|
||||
public int getType () { return getDownloadState()==DC_DOWNLOAD_DONE? getViewType() : DC_MSG_TEXT; }
|
||||
public native int getInfoType ();
|
||||
public native int getInfoContactId ();
|
||||
public native int getState ();
|
||||
public native int getDownloadState ();
|
||||
public native int getChatId ();
|
||||
public native int getFromId ();
|
||||
public native int getWidth (int def);
|
||||
public native int getHeight (int def);
|
||||
public native int getDuration ();
|
||||
public native void lateFilingMediaSize(int width, int height, int duration);
|
||||
public DcLot getSummary (DcChat chat) { return new DcLot(getSummaryCPtr(chat.getChatCPtr())); }
|
||||
public native String getSummarytext (int approx_characters);
|
||||
public native int showPadlock ();
|
||||
public boolean hasFile () { String file = getFile(); return file!=null && !file.isEmpty(); }
|
||||
public native String getFile ();
|
||||
public native String getFilemime ();
|
||||
public native String getFilename ();
|
||||
public native long getFilebytes ();
|
||||
public native byte[] getWebxdcBlob (String filename);
|
||||
public JSONObject getWebxdcInfo () {
|
||||
try {
|
||||
String json = getWebxdcInfoJson();
|
||||
if (json != null && !json.isEmpty()) return new JSONObject(json);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
/** If given a message, calculates the position of the message in the chat */
|
||||
public static int getMessagePosition(DcMsg msg, DcContext dcContext) {
|
||||
int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0);
|
||||
int startingPosition = -1;
|
||||
int msgId = msg.getId();
|
||||
for (int i = 0; i < msgs.length; i++) {
|
||||
if (msgs[i] == msgId) {
|
||||
startingPosition = msgs.length - 1 - i;
|
||||
break;
|
||||
}
|
||||
return new JSONObject();
|
||||
}
|
||||
public native String getWebxdcHref ();
|
||||
public native boolean isForwarded ();
|
||||
public native boolean isInfo ();
|
||||
public native boolean hasHtml ();
|
||||
public native String getSetupCodeBegin ();
|
||||
public native void setText (String text);
|
||||
public native void setSubject (String text);
|
||||
public native void setHtml (String text);
|
||||
public native void forceSticker ();
|
||||
public native void setFileAndDeduplicate(String file, String name, String filemime);
|
||||
public native void setDimension (int width, int height);
|
||||
public native void setDuration (int duration);
|
||||
public native void setLocation (float latitude, float longitude);
|
||||
public native String getPOILocation ();
|
||||
public void setQuote (DcMsg quote) { setQuoteCPtr(quote.msgCPtr); }
|
||||
public native String getQuotedText ();
|
||||
public native String getError ();
|
||||
public native String getOverrideSenderName();
|
||||
public native boolean isEdited ();
|
||||
return startingPosition;
|
||||
}
|
||||
|
||||
public String getSenderName(DcContact dcContact) {
|
||||
String overrideName = getOverrideSenderName();
|
||||
if (overrideName != null) {
|
||||
return "~" + overrideName;
|
||||
} else {
|
||||
return dcContact.getDisplayName();
|
||||
}
|
||||
}
|
||||
public native int getId();
|
||||
|
||||
public DcMsg getQuotedMsg () {
|
||||
long cPtr = getQuotedMsgCPtr();
|
||||
return cPtr != 0 ? new DcMsg(cPtr) : null;
|
||||
}
|
||||
public native String getText();
|
||||
|
||||
public DcMsg getParent() {
|
||||
long cPtr = getParentCPtr();
|
||||
return cPtr != 0 ? new DcMsg(cPtr) : null;
|
||||
}
|
||||
public native String getSubject();
|
||||
|
||||
public native int getOriginalMsgId ();
|
||||
public native int getSavedMsgId ();
|
||||
public native long getTimestamp();
|
||||
|
||||
public boolean canSave() {
|
||||
// saving info-messages out of context results in confusion, see https://github.com/deltachat/deltachat-ios/issues/2567
|
||||
return !isInfo();
|
||||
}
|
||||
public native long getSortTimestamp();
|
||||
|
||||
public File getFileAsFile() {
|
||||
if(getFile()==null)
|
||||
throw new AssertionError("expected a file to be present.");
|
||||
return new File(getFile());
|
||||
}
|
||||
public native boolean hasDeviatingTimestamp();
|
||||
|
||||
// aliases and higher-level tools
|
||||
public static int[] msgSetToIds(final Set<DcMsg> dcMsgs) {
|
||||
if (dcMsgs == null) {
|
||||
return new int[0];
|
||||
}
|
||||
int[] ids = new int[dcMsgs.size()];
|
||||
int i = 0;
|
||||
for (DcMsg dcMsg : dcMsgs) {
|
||||
ids[i++] = dcMsg.getId();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
public native boolean hasLocation();
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return getFromId() == DcContact.DC_CONTACT_ID_SELF;
|
||||
}
|
||||
private native int getViewType();
|
||||
|
||||
public String getDisplayBody() {
|
||||
return getText();
|
||||
}
|
||||
public int getType() {
|
||||
return getDownloadState() == DC_DOWNLOAD_DONE ? getViewType() : DC_MSG_TEXT;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return getText();
|
||||
}
|
||||
public native int getInfoType();
|
||||
|
||||
public long getDateReceived() {
|
||||
return getTimestamp();
|
||||
}
|
||||
public native int getInfoContactId();
|
||||
|
||||
public boolean isFailed() {
|
||||
return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError()));
|
||||
}
|
||||
public boolean isPreparing() {
|
||||
return getState() == DC_STATE_OUT_PREPARING;
|
||||
}
|
||||
public boolean isSecure() {
|
||||
return showPadlock()!=0;
|
||||
}
|
||||
public boolean isPending() {
|
||||
return getState() == DC_STATE_OUT_PENDING;
|
||||
}
|
||||
public boolean isDelivered() {
|
||||
return getState() == DC_STATE_OUT_DELIVERED;
|
||||
}
|
||||
public boolean isRemoteRead() {
|
||||
return getState() == DC_STATE_OUT_MDN_RCVD;
|
||||
}
|
||||
public boolean isSeen() {
|
||||
return getState() == DC_STATE_IN_SEEN;
|
||||
}
|
||||
public native int getState();
|
||||
|
||||
public native int getDownloadState();
|
||||
|
||||
// working with raw c-data
|
||||
private long msgCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefMsgCPtr ();
|
||||
private native long getSummaryCPtr (long chatCPtr);
|
||||
private native void setQuoteCPtr (long quoteCPtr);
|
||||
private native long getQuotedMsgCPtr ();
|
||||
private native long getParentCPtr ();
|
||||
private native String getWebxdcInfoJson ();
|
||||
};
|
||||
public native int getChatId();
|
||||
|
||||
public native int getFromId();
|
||||
|
||||
public native int getWidth(int def);
|
||||
|
||||
public native int getHeight(int def);
|
||||
|
||||
public native int getDuration();
|
||||
|
||||
public native void lateFilingMediaSize(int width, int height, int duration);
|
||||
|
||||
public DcLot getSummary(DcChat chat) {
|
||||
return new DcLot(getSummaryCPtr(chat.getChatCPtr()));
|
||||
}
|
||||
|
||||
public native String getSummarytext(int approx_characters);
|
||||
|
||||
public native int showPadlock();
|
||||
|
||||
public boolean hasFile() {
|
||||
String file = getFile();
|
||||
return file != null && !file.isEmpty();
|
||||
}
|
||||
|
||||
public native String getFile();
|
||||
|
||||
public native String getFilemime();
|
||||
|
||||
public native String getFilename();
|
||||
|
||||
public native long getFilebytes();
|
||||
|
||||
public native byte[] getWebxdcBlob(String filename);
|
||||
|
||||
public JSONObject getWebxdcInfo() {
|
||||
try {
|
||||
String json = getWebxdcInfoJson();
|
||||
if (json != null && !json.isEmpty()) return new JSONObject(json);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new JSONObject();
|
||||
}
|
||||
|
||||
public native String getWebxdcHref();
|
||||
|
||||
public native boolean isForwarded();
|
||||
|
||||
public native boolean isInfo();
|
||||
|
||||
public native boolean hasHtml();
|
||||
|
||||
public native String getSetupCodeBegin();
|
||||
|
||||
public native void setText(String text);
|
||||
|
||||
public native void setSubject(String text);
|
||||
|
||||
public native void setHtml(String text);
|
||||
|
||||
public native void forceSticker();
|
||||
|
||||
public native void setFileAndDeduplicate(String file, String name, String filemime);
|
||||
|
||||
public native void setDimension(int width, int height);
|
||||
|
||||
public native void setDuration(int duration);
|
||||
|
||||
public native void setLocation(float latitude, float longitude);
|
||||
|
||||
public native String getPOILocation();
|
||||
|
||||
public void setQuote(DcMsg quote) {
|
||||
setQuoteCPtr(quote.msgCPtr);
|
||||
}
|
||||
|
||||
public native String getQuotedText();
|
||||
|
||||
public native String getError();
|
||||
|
||||
public native String getOverrideSenderName();
|
||||
|
||||
public native boolean isEdited();
|
||||
|
||||
public String getSenderName(DcContact dcContact) {
|
||||
String overrideName = getOverrideSenderName();
|
||||
if (overrideName != null) {
|
||||
return "~" + overrideName;
|
||||
} else {
|
||||
return dcContact.getDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
public DcMsg getQuotedMsg() {
|
||||
long cPtr = getQuotedMsgCPtr();
|
||||
return cPtr != 0 ? new DcMsg(cPtr) : null;
|
||||
}
|
||||
|
||||
public DcMsg getParent() {
|
||||
long cPtr = getParentCPtr();
|
||||
return cPtr != 0 ? new DcMsg(cPtr) : null;
|
||||
}
|
||||
|
||||
public native int getOriginalMsgId();
|
||||
|
||||
public native int getSavedMsgId();
|
||||
|
||||
public boolean canSave() {
|
||||
// saving info-messages out of context results in confusion, see
|
||||
// https://github.com/deltachat/deltachat-ios/issues/2567
|
||||
return !isInfo();
|
||||
}
|
||||
|
||||
public File getFileAsFile() {
|
||||
if (getFile() == null) throw new AssertionError("expected a file to be present.");
|
||||
return new File(getFile());
|
||||
}
|
||||
|
||||
// aliases and higher-level tools
|
||||
public static int[] msgSetToIds(final Set<DcMsg> dcMsgs) {
|
||||
if (dcMsgs == null) {
|
||||
return new int[0];
|
||||
}
|
||||
int[] ids = new int[dcMsgs.size()];
|
||||
int i = 0;
|
||||
for (DcMsg dcMsg : dcMsgs) {
|
||||
ids[i++] = dcMsg.getId();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return getFromId() == DcContact.DC_CONTACT_ID_SELF;
|
||||
}
|
||||
|
||||
public String getDisplayBody() {
|
||||
return getText();
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return getText();
|
||||
}
|
||||
|
||||
public long getDateReceived() {
|
||||
return getTimestamp();
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError()));
|
||||
}
|
||||
|
||||
public boolean isPreparing() {
|
||||
return getState() == DC_STATE_OUT_PREPARING;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return showPadlock() != 0;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return getState() == DC_STATE_OUT_PENDING;
|
||||
}
|
||||
|
||||
public boolean isDelivered() {
|
||||
return getState() == DC_STATE_OUT_DELIVERED;
|
||||
}
|
||||
|
||||
public boolean isRemoteRead() {
|
||||
return getState() == DC_STATE_OUT_MDN_RCVD;
|
||||
}
|
||||
|
||||
public boolean isSeen() {
|
||||
return getState() == DC_STATE_IN_SEEN;
|
||||
}
|
||||
|
||||
// working with raw c-data
|
||||
private long msgCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefMsgCPtr();
|
||||
|
||||
private native long getSummaryCPtr(long chatCPtr);
|
||||
|
||||
private native void setQuoteCPtr(long quoteCPtr);
|
||||
|
||||
private native long getQuotedMsgCPtr();
|
||||
|
||||
private native long getParentCPtr();
|
||||
|
||||
private native String getWebxdcInfoJson();
|
||||
}
|
||||
;
|
||||
|
||||
@@ -2,25 +2,29 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcProvider {
|
||||
|
||||
public final static int DC_PROVIDER_STATUS_OK = 1;
|
||||
public final static int DC_PROVIDER_STATUS_PREPARATION = 2;
|
||||
public final static int DC_PROVIDER_STATUS_BROKEN = 3;
|
||||
public static final int DC_PROVIDER_STATUS_OK = 1;
|
||||
public static final int DC_PROVIDER_STATUS_PREPARATION = 2;
|
||||
public static final int DC_PROVIDER_STATUS_BROKEN = 3;
|
||||
|
||||
public DcProvider(long providerCPtr) {
|
||||
this.providerCPtr = providerCPtr;
|
||||
}
|
||||
public DcProvider(long providerCPtr) {
|
||||
this.providerCPtr = providerCPtr;
|
||||
}
|
||||
|
||||
@Override protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefProviderCPtr();
|
||||
providerCPtr = 0;
|
||||
}
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
unrefProviderCPtr();
|
||||
providerCPtr = 0;
|
||||
}
|
||||
|
||||
public native int getStatus ();
|
||||
public native String getBeforeLoginHint ();
|
||||
public native String getOverviewPage ();
|
||||
public native int getStatus();
|
||||
|
||||
// working with raw c-data
|
||||
private long providerCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefProviderCPtr();
|
||||
public native String getBeforeLoginHint();
|
||||
|
||||
public native String getOverviewPage();
|
||||
|
||||
// working with raw c-data
|
||||
private long providerCPtr; // CAVE: the name is referenced in the JNI
|
||||
|
||||
private native void unrefProviderCPtr();
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ public class FFITransport extends BaseTransport {
|
||||
protected String getResponse() {
|
||||
return dcJsonrpcInstance.getNextResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1292
-1284
File diff suppressed because it is too large
Load Diff
@@ -1,61 +1,73 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.ArrayList;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
implements DcEventCenter.DcEventDelegate
|
||||
{
|
||||
implements DcEventCenter.DcEventDelegate {
|
||||
private static final String TAG = AllMediaActivity.class.getSimpleName();
|
||||
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String CONTACT_ID_EXTRA = "contact_id";
|
||||
public static final String FORCE_GALLERY = "force_gallery";
|
||||
public static final String FORCE_GALLERY = "force_gallery";
|
||||
|
||||
static class TabData {
|
||||
final int title;
|
||||
final int type1;
|
||||
final int type2;
|
||||
final int type3;
|
||||
|
||||
TabData(int title, int type1, int type2, int type3) {
|
||||
this.title = title;
|
||||
this.type1 = type1;
|
||||
this.type2 = type2;
|
||||
this.type3 = type3;
|
||||
}
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
private DcContext dcContext;
|
||||
private int chatId;
|
||||
private int contactId;
|
||||
private DcContext dcContext;
|
||||
private int chatId;
|
||||
private int contactId;
|
||||
|
||||
private final ArrayList<TabData> tabs = new ArrayList<>();
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
@@ -67,7 +79,9 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
tabs.add(new TabData(R.string.webxdc_apps, DcMsg.DC_MSG_WEBXDC, 0, 0));
|
||||
tabs.add(new TabData(R.string.tab_gallery, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO));
|
||||
tabs.add(
|
||||
new TabData(
|
||||
R.string.tab_gallery, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO));
|
||||
tabs.add(new TabData(R.string.audio, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0));
|
||||
tabs.add(new TabData(R.string.files, DcMsg.DC_MSG_FILE, 0, 0));
|
||||
|
||||
@@ -79,7 +93,8 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
ActionBar supportActionBar = getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
supportActionBar.setTitle(isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
|
||||
supportActionBar.setTitle(
|
||||
isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
|
||||
}
|
||||
|
||||
this.tabLayout.setupWithViewPager(viewPager);
|
||||
@@ -91,41 +106,80 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
}
|
||||
public void handleEvent(@NonNull DcEvent event) {}
|
||||
|
||||
private void initializeResources() {
|
||||
chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0);
|
||||
contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0);
|
||||
chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0);
|
||||
contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0);
|
||||
|
||||
if (contactId!=0) {
|
||||
if (contactId != 0) {
|
||||
chatId = dcContext.getChatIdByContactId(contactId);
|
||||
}
|
||||
|
||||
if(chatId!=0) {
|
||||
if (chatId != 0) {
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
if(!dcChat.isMultiUser()) {
|
||||
if (!dcChat.isMultiUser()) {
|
||||
final int[] members = dcContext.getChatContacts(chatId);
|
||||
contactId = members.length>=1? members[0] : 0;
|
||||
contactId = members.length >= 1 ? members[0] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.viewPager = ViewUtil.findById(this, R.id.pager);
|
||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken =
|
||||
new SessionToken(this, new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken).buildAsync();
|
||||
mediaControllerFuture.addListener(
|
||||
() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
addActivityContext(this.getIntent().getExtras(), this.getClass().getName());
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
private boolean isGlobalGallery() {
|
||||
return contactId==0 && chatId==0;
|
||||
return contactId == 0 && chatId == 0;
|
||||
}
|
||||
|
||||
private class AllMediaPagerAdapter extends FragmentStatePagerAdapter {
|
||||
@@ -159,10 +213,14 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
if (data.type1 == DcMsg.DC_MSG_IMAGE) {
|
||||
fragment = new AllMediaGalleryFragment();
|
||||
args.putInt(AllMediaGalleryFragment.CHAT_ID_EXTRA, (chatId==0&&!isGlobalGallery())? -1 : chatId);
|
||||
args.putInt(
|
||||
AllMediaGalleryFragment.CHAT_ID_EXTRA,
|
||||
(chatId == 0 && !isGlobalGallery()) ? -1 : chatId);
|
||||
} else {
|
||||
fragment = new AllMediaDocumentsFragment();
|
||||
args.putInt(AllMediaDocumentsFragment.CHAT_ID_EXTRA, (chatId==0&&!isGlobalGallery())? -1 : chatId);
|
||||
args.putInt(
|
||||
AllMediaDocumentsFragment.CHAT_ID_EXTRA,
|
||||
(chatId == 0 && !isGlobalGallery()) ? -1 : chatId);
|
||||
args.putInt(AllMediaDocumentsFragment.VIEWTYPE1, data.type1);
|
||||
args.putInt(AllMediaDocumentsFragment.VIEWTYPE2, data.type2);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -21,30 +22,27 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
private final Context context;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
private final Context context;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
|
||||
private BucketedThreadMedia media;
|
||||
private BucketedThreadMedia media;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
||||
private final DocumentView documentView;
|
||||
private final AudioView audioView;
|
||||
private final WebxdcView webxdcView;
|
||||
private final TextView date;
|
||||
private final AudioView audioView;
|
||||
private final WebxdcView webxdcView;
|
||||
private final TextView date;
|
||||
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
documentView = v.findViewById(R.id.document_view);
|
||||
audioView = v.findViewById(R.id.audio_view);
|
||||
webxdcView = v.findViewById(R.id.webxdc_view);
|
||||
date = v.findViewById(R.id.date);
|
||||
documentView = v.findViewById(R.id.document_view);
|
||||
audioView = v.findViewById(R.id.audio_view);
|
||||
webxdcView = v.findViewById(R.id.webxdc_view);
|
||||
date = v.findViewById(R.id.date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,86 +55,108 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
AllMediaDocumentsAdapter(@NonNull Context context,
|
||||
BucketedThreadMedia media,
|
||||
ItemClickListener clickListener)
|
||||
{
|
||||
this.context = context;
|
||||
this.media = media;
|
||||
AllMediaDocumentsAdapter(
|
||||
@NonNull Context context, BucketedThreadMedia media, ItemClickListener clickListener) {
|
||||
this.context = context;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.selected = new HashSet<>();
|
||||
this.selected = new HashSet<>();
|
||||
}
|
||||
|
||||
public void setMedia(BucketedThreadMedia media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(
|
||||
ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(
|
||||
LayoutInflater.from(context)
|
||||
.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
||||
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.profile_document_item, parent, false));
|
||||
return new ViewHolder(
|
||||
LayoutInflater.from(context).inflate(R.layout.profile_document_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section));
|
||||
public void onBindHeaderViewHolder(
|
||||
StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder) viewHolder).textView.setText(media.getName(section));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ItemViewHolder itemViewHolder, int section, int offset) {
|
||||
ViewHolder viewHolder = ((ViewHolder)itemViewHolder);
|
||||
DcMsg dcMsg = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForMsg(context, dcMsg);
|
||||
ViewHolder viewHolder = ((ViewHolder) itemViewHolder);
|
||||
DcMsg dcMsg = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForMsg(context, dcMsg);
|
||||
|
||||
if (slide != null && slide.hasAudio()) {
|
||||
viewHolder.documentView.setVisibility(View.GONE);
|
||||
viewHolder.webxdcView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.audioView.setVisibility(View.VISIBLE);
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
|
||||
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
|
||||
viewHolder.audioView.setAudio((AudioSlide) slide);
|
||||
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.audioView.setOnLongClickListener(
|
||||
view -> {
|
||||
itemClickListener.onMediaLongClicked(dcMsg);
|
||||
return true;
|
||||
});
|
||||
viewHolder.audioView.disablePlayer(!selected.isEmpty());
|
||||
viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else if (slide != null && slide.isWebxdcDocument()) {
|
||||
} else if (slide != null && slide.isWebxdcDocument()) {
|
||||
viewHolder.audioView.setVisibility(View.GONE);
|
||||
viewHolder.documentView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.webxdcView.setVisibility(View.VISIBLE);
|
||||
viewHolder.webxdcView.setWebxdc(dcMsg, "");
|
||||
viewHolder.webxdcView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.webxdcView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.webxdcView.setOnLongClickListener(
|
||||
view -> {
|
||||
itemClickListener.onMediaLongClicked(dcMsg);
|
||||
return true;
|
||||
});
|
||||
viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.date.setVisibility(View.GONE);
|
||||
}
|
||||
else if (slide != null && slide.hasDocument()) {
|
||||
} else if (slide != null && slide.hasDocument()) {
|
||||
viewHolder.audioView.setVisibility(View.GONE);
|
||||
viewHolder.webxdcView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.documentView.setVisibility(View.VISIBLE);
|
||||
viewHolder.documentView.setDocument((DocumentSlide)slide);
|
||||
viewHolder.documentView.setDocument((DocumentSlide) slide);
|
||||
viewHolder.documentView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.documentView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.documentView.setOnLongClickListener(
|
||||
view -> {
|
||||
itemClickListener.onMediaLongClicked(dcMsg);
|
||||
return true;
|
||||
});
|
||||
viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
viewHolder.documentView.setVisibility(View.GONE);
|
||||
viewHolder.audioView.setVisibility(View.GONE);
|
||||
viewHolder.webxdcView.setVisibility(View.GONE);
|
||||
viewHolder.date.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
viewHolder.itemView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.itemView.setOnLongClickListener(
|
||||
view -> {
|
||||
itemClickListener.onMediaLongClicked(dcMsg);
|
||||
return true;
|
||||
});
|
||||
viewHolder.itemView.setSelected(selected.contains(dcMsg));
|
||||
|
||||
viewHolder.date.setText(DateUtils.getBriefRelativeTimeSpanString(context, dcMsg.getTimestamp()));
|
||||
viewHolder.date.setText(
|
||||
DateUtils.getBriefRelativeTimeSpanString(context, dcMsg.getTimestamp()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,6 +197,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull DcMsg mediaRecord);
|
||||
|
||||
void onMediaLongClicked(DcMsg mediaRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,31 +12,27 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class AllMediaDocumentsFragment
|
||||
extends MessageSelectorFragment
|
||||
public class AllMediaDocumentsFragment extends MessageSelectorFragment
|
||||
implements LoaderManager.LoaderCallbacks<BucketedThreadMediaLoader.BucketedThreadMedia>,
|
||||
AllMediaDocumentsAdapter.ItemClickListener
|
||||
{
|
||||
AllMediaDocumentsAdapter.ItemClickListener {
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String VIEWTYPE1 = "viewtype1";
|
||||
public static final String VIEWTYPE2 = "viewtype2";
|
||||
@@ -48,7 +44,7 @@ public class AllMediaDocumentsFragment
|
||||
private int viewtype1;
|
||||
private int viewtype2;
|
||||
|
||||
protected int chatId;
|
||||
protected int chatId;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
@@ -62,19 +58,23 @@ public class AllMediaDocumentsFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(
|
||||
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.profile_documents_fragment, container, false);
|
||||
|
||||
this.recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_documents);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(1);
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_documents);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(1);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
|
||||
|
||||
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this));
|
||||
AllMediaDocumentsAdapter adapter =
|
||||
new AllMediaDocumentsAdapter(
|
||||
getContext(), new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()), this);
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
adapter.setPlaybackViewModel(
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
@@ -105,12 +105,15 @@ public class AllMediaDocumentsFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> onCreateLoader(
|
||||
int i, Bundle bundle) {
|
||||
return new BucketedThreadMediaLoader(getContext(), chatId, viewtype1, viewtype2, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<BucketedThreadMediaLoader.BucketedThreadMedia> loader, BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) {
|
||||
public void onLoadFinished(
|
||||
Loader<BucketedThreadMediaLoader.BucketedThreadMedia> loader,
|
||||
BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) {
|
||||
((AllMediaDocumentsAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia);
|
||||
((AllMediaDocumentsAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
@@ -118,7 +121,7 @@ public class AllMediaDocumentsFragment
|
||||
if (chatId == DC_CHAT_NO_CHAT) {
|
||||
if (viewtype1 == DcMsg.DC_MSG_WEBXDC) {
|
||||
noMedia.setText(R.string.all_apps_empty_hint);
|
||||
} else if (viewtype1 == DcMsg.DC_MSG_FILE){
|
||||
} else if (viewtype1 == DcMsg.DC_MSG_FILE) {
|
||||
noMedia.setText(R.string.all_files_empty_hint);
|
||||
} else {
|
||||
noMedia.setText(R.string.tab_all_media_empty_hint);
|
||||
@@ -133,7 +136,8 @@ public class AllMediaDocumentsFragment
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<BucketedThreadMediaLoader.BucketedThreadMedia> cursorLoader) {
|
||||
((AllMediaDocumentsAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()));
|
||||
((AllMediaDocumentsAdapter) recyclerView.getAdapter())
|
||||
.setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -164,7 +168,7 @@ public class AllMediaDocumentsFragment
|
||||
|
||||
private void handleMediaPreviewClick(@NonNull DcMsg dcMsg) {
|
||||
// audio is started by the play-button
|
||||
if (dcMsg.getType()==DcMsg.DC_MSG_AUDIO || dcMsg.getType()==DcMsg.DC_MSG_VOICE) {
|
||||
if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO || dcMsg.getType() == DcMsg.DC_MSG_VOICE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,7 +216,8 @@ public class AllMediaDocumentsFragment
|
||||
}
|
||||
menu.findItem(R.id.menu_resend).setVisible(canResend);
|
||||
|
||||
boolean webxdcApp = singleSelection && messageRecords.iterator().next().getType() == DcMsg.DC_MSG_WEBXDC;
|
||||
boolean webxdcApp =
|
||||
singleSelection && messageRecords.iterator().next().getType() == DcMsg.DC_MSG_WEBXDC;
|
||||
menu.findItem(R.id.menu_add_to_home_screen).setVisible(webxdcApp);
|
||||
}
|
||||
|
||||
@@ -239,19 +244,26 @@ public class AllMediaDocumentsFragment
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
if (itemId == R.id.details) {
|
||||
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.delete) {
|
||||
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
|
||||
handleDeleteMessages(
|
||||
chatId,
|
||||
getListAdapter().getSelectedMedia(),
|
||||
playbackViewModel::stopByIds,
|
||||
playbackViewModel::stopByIds);
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.share) {
|
||||
handleShare(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_add_to_home_screen) {
|
||||
WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedMedia()).getId());
|
||||
WebxdcActivity.addToHomeScreen(
|
||||
getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedMedia()).getId());
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.show_in_chat) {
|
||||
|
||||
@@ -5,38 +5,34 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class AllMediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
|
||||
private BucketedThreadMedia media;
|
||||
private BucketedThreadMedia media;
|
||||
|
||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
||||
final ThumbnailView imageView;
|
||||
final View selectedIndicator;
|
||||
final View selectedIndicator;
|
||||
|
||||
ViewHolder(View v) {
|
||||
super(v);
|
||||
imageView = v.findViewById(R.id.image);
|
||||
imageView = v.findViewById(R.id.image);
|
||||
selectedIndicator = v.findViewById(R.id.selected_indicator);
|
||||
}
|
||||
}
|
||||
@@ -50,16 +46,16 @@ class AllMediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
AllMediaGalleryAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
BucketedThreadMedia media,
|
||||
ItemClickListener clickListener)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.media = media;
|
||||
AllMediaGalleryAdapter(
|
||||
@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
BucketedThreadMedia media,
|
||||
ItemClickListener clickListener) {
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.selected = new HashSet<>();
|
||||
this.selected = new HashSet<>();
|
||||
}
|
||||
|
||||
public void setMedia(BucketedThreadMedia media) {
|
||||
@@ -67,36 +63,42 @@ class AllMediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(
|
||||
ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(
|
||||
LayoutInflater.from(context)
|
||||
.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
||||
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.profile_gallery_item, parent, false));
|
||||
return new ViewHolder(
|
||||
LayoutInflater.from(context).inflate(R.layout.profile_gallery_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section));
|
||||
public void onBindHeaderViewHolder(
|
||||
StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder) viewHolder).textView.setText(media.getName(section));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
|
||||
DcMsg mediaRecord = media.get(section, offset);
|
||||
ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView;
|
||||
View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator;
|
||||
Slide slide = MediaUtil.getSlideForMsg(context, mediaRecord);
|
||||
DcMsg mediaRecord = media.get(section, offset);
|
||||
ThumbnailView thumbnailView = ((ViewHolder) viewHolder).imageView;
|
||||
View selectedIndicator = ((ViewHolder) viewHolder).selectedIndicator;
|
||||
Slide slide = MediaUtil.getSlideForMsg(context, mediaRecord);
|
||||
|
||||
if (slide != null) {
|
||||
thumbnailView.setImageResource(glideRequests, slide);
|
||||
}
|
||||
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
thumbnailView.setOnLongClickListener(view -> {
|
||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
||||
return true;
|
||||
});
|
||||
thumbnailView.setOnLongClickListener(
|
||||
view -> {
|
||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
||||
return true;
|
||||
});
|
||||
|
||||
selectedIndicator.setVisibility(selected.contains(mediaRecord) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
@@ -139,6 +141,7 @@ class AllMediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull DcMsg mediaRecord);
|
||||
|
||||
void onMediaLongClicked(DcMsg mediaRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,17 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
@@ -32,13 +30,9 @@ import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class AllMediaGalleryFragment
|
||||
extends MessageSelectorFragment
|
||||
public class AllMediaGalleryFragment extends MessageSelectorFragment
|
||||
implements LoaderManager.LoaderCallbacks<BucketedThreadMediaLoader.BucketedThreadMedia>,
|
||||
AllMediaGalleryAdapter.ItemClickListener
|
||||
{
|
||||
AllMediaGalleryAdapter.ItemClickListener {
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
|
||||
protected TextView noMedia;
|
||||
@@ -46,7 +40,7 @@ public class AllMediaGalleryFragment
|
||||
private StickyHeaderGridLayoutManager gridManager;
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
|
||||
private int chatId;
|
||||
private int chatId;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
@@ -58,20 +52,23 @@ public class AllMediaGalleryFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(
|
||||
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.profile_gallery_fragment, container, false);
|
||||
|
||||
this.recyclerView = ViewUtil.findById(view, R.id.media_grid);
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_images);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getCols());
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_images);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getCols());
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
|
||||
|
||||
this.recyclerView.setAdapter(new AllMediaGalleryAdapter(getContext(),
|
||||
GlideApp.with(this),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this));
|
||||
this.recyclerView.setAdapter(
|
||||
new AllMediaGalleryAdapter(
|
||||
getContext(),
|
||||
GlideApp.with(this),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
@@ -94,7 +91,9 @@ public class AllMediaGalleryFragment
|
||||
}
|
||||
|
||||
private int getCols() {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE? 5 : 3;
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
? 5
|
||||
: 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,12 +106,16 @@ public class AllMediaGalleryFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
return new BucketedThreadMediaLoader(getContext(), chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO);
|
||||
public Loader<BucketedThreadMediaLoader.BucketedThreadMedia> onCreateLoader(
|
||||
int i, Bundle bundle) {
|
||||
return new BucketedThreadMediaLoader(
|
||||
getContext(), chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<BucketedThreadMediaLoader.BucketedThreadMedia> loader, BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) {
|
||||
public void onLoadFinished(
|
||||
Loader<BucketedThreadMediaLoader.BucketedThreadMedia> loader,
|
||||
BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) {
|
||||
((AllMediaGalleryAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia);
|
||||
((AllMediaGalleryAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
@@ -125,7 +128,8 @@ public class AllMediaGalleryFragment
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<BucketedThreadMediaLoader.BucketedThreadMedia> cursorLoader) {
|
||||
((AllMediaGalleryAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()));
|
||||
((AllMediaGalleryAdapter) recyclerView.getAdapter())
|
||||
.setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.work.WorkManager;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcEventChannel;
|
||||
import com.b44t.messenger.DcEventEmitter;
|
||||
import com.b44t.messenger.FFITransport;
|
||||
|
||||
@@ -164,7 +165,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(stringWriter, true));
|
||||
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
|
||||
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + " Crash Report";
|
||||
errorMsg += "\n" + LogViewFragment.grabLogcat();
|
||||
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
|
||||
@@ -195,7 +197,29 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
Util.runOnBackground(() -> {
|
||||
synchronized (initLock) {
|
||||
try {
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
|
||||
DcEventChannel eventChannel = new DcEventChannel();
|
||||
DcEventEmitter emitter = eventChannel.getEventEmitter();
|
||||
eventCenter = new DcEventCenter(this);
|
||||
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event == null) {
|
||||
break;
|
||||
}
|
||||
if (isInitialized) {
|
||||
eventCenter.handleEvent(event);
|
||||
} else {
|
||||
// not fully initialized, only handle logging events,
|
||||
// ex. account migrations during DcAccounts initialization
|
||||
eventCenter.handleLogging(event);
|
||||
}
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
}, "eventThread").start();
|
||||
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
|
||||
Log.i(TAG, "DcAccounts created");
|
||||
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
|
||||
Log.i(TAG, "Rpc created");
|
||||
@@ -235,28 +259,12 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
dcContext = dcAccounts.getSelectedAccount();
|
||||
notificationCenter = new NotificationCenter(this);
|
||||
eventCenter = new DcEventCenter(this);
|
||||
dcLocationManager = new DcLocationManager(this, dcContext);
|
||||
|
||||
// Mark as initialized before starting threads that depend on it
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
Log.i(TAG, "DcAccounts initialization complete");
|
||||
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
DcEventEmitter emitter = dcAccounts.getEventEmitter();
|
||||
Log.i(TAG, "DcEventEmitter obtained");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event==null) {
|
||||
break;
|
||||
}
|
||||
eventCenter.handleEvent(event);
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
}, "eventThread").start();
|
||||
|
||||
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
|
||||
DcHelper.setStockTranslations(this);
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -30,19 +31,17 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.PrivacyPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.PrivacyPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.qr.BackupTransferActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -55,24 +54,22 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
* The Activity for application preference display and management.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_PRIVACY = "preference_category_privacy";
|
||||
private static final String PREFERENCE_CATEGORY_MULTIDEVICE = "preference_category_multidevice";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_CONNECTIVITY = "preference_category_connectivity";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS =
|
||||
"preference_category_notifications";
|
||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_PRIVACY = "preference_category_privacy";
|
||||
private static final String PREFERENCE_CATEGORY_MULTIDEVICE = "preference_category_multidevice";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_CONNECTIVITY = "preference_category_connectivity";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
|
||||
public static final int REQUEST_CODE_SET_BACKGROUND = 11;
|
||||
public static final int REQUEST_CODE_SET_BACKGROUND = 11;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
@@ -89,18 +86,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK && requestCode == ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS) {
|
||||
showBackupProvider();
|
||||
return;
|
||||
}
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment);
|
||||
fragment.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
@@ -125,34 +110,51 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
public void showBackupProvider() {
|
||||
Intent intent = new Intent(this, BackupTransferActivity.class);
|
||||
intent.putExtra(BackupTransferActivity.TRANSFER_MODE, BackupTransferActivity.TransferMode.SENDER_SHOW_QR.getInt());
|
||||
intent.putExtra(
|
||||
BackupTransferActivity.TRANSFER_MODE,
|
||||
BackupTransferActivity.TransferMode.SENDER_SHOW_QR.getInt());
|
||||
startActivity(intent);
|
||||
overridePendingTransition(0, 0); // let the activity appear in the same way as the other pages (which are mostly fragments)
|
||||
overridePendingTransition(
|
||||
0, 0); // let the activity appear in the same way as the other pages (which are mostly
|
||||
// fragments)
|
||||
finishAffinity(); // see comment (**2) in BackupTransferActivity.doFinish()
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment implements DcEventCenter.DcEventDelegate {
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment
|
||||
implements DcEventCenter.DcEventDelegate {
|
||||
private ActivityResultLauncher<Intent> screenLockLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
screenLockLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
((ApplicationPreferencesActivity) getActivity()).showBackupProvider();
|
||||
}
|
||||
});
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
.setOnPreferenceClickListener(
|
||||
new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CONNECTIVITY));
|
||||
.setOnPreferenceClickListener(
|
||||
new CategoryClickListener(PREFERENCE_CATEGORY_CONNECTIVITY));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_PRIVACY)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PRIVACY));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PRIVACY));
|
||||
this.findPreference(PREFERENCE_CATEGORY_MULTIDEVICE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_MULTIDEVICE));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_MULTIDEVICE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
@@ -160,7 +162,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
|
||||
DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONNECTIVITY_CHANGED, this);
|
||||
DcHelper.getEventCenter(getActivity())
|
||||
.addObserver(DcContext.DC_EVENT_CONNECTIVITY_CHANGED, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -172,7 +175,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
//noinspection ConstantConditions
|
||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.menu_settings);
|
||||
((ApplicationPreferencesActivity) getActivity())
|
||||
.getSupportActionBar()
|
||||
.setTitle(R.string.menu_settings);
|
||||
setCategorySummaries();
|
||||
}
|
||||
|
||||
@@ -186,12 +191,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
if (event.getId() == DcContext.DC_EVENT_CONNECTIVITY_CHANGED) {
|
||||
this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY)
|
||||
.setSummary(DcHelper.getConnectivitySummary(getActivity(), getString(R.string.connectivity_connected)));
|
||||
.setSummary(
|
||||
DcHelper.getConnectivitySummary(
|
||||
getActivity(), getString(R.string.connectivity_connected)));
|
||||
}
|
||||
}
|
||||
|
||||
private void setCategorySummaries() {
|
||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||
((ProfilePreference) this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setSummary(NotificationsPreferenceFragment.getSummary(getActivity()));
|
||||
@@ -202,7 +209,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_PRIVACY)
|
||||
.setSummary(PrivacyPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY)
|
||||
.setSummary(DcHelper.getConnectivitySummary(getActivity(), getString(R.string.connectivity_connected)));
|
||||
.setSummary(
|
||||
DcHelper.getConnectivitySummary(
|
||||
getActivity(), getString(R.string.connectivity_connected)));
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setSummary(AdvancedPreferenceFragment.getVersion(getActivity()));
|
||||
}
|
||||
@@ -219,63 +228,76 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
Fragment fragment = null;
|
||||
|
||||
switch (category) {
|
||||
case PREFERENCE_CATEGORY_NOTIFICATIONS:
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getActivity());
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || notificationManager.areNotificationsEnabled()) {
|
||||
fragment = new NotificationsPreferenceFragment();
|
||||
} else {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.notifications_disabled)
|
||||
.setMessage(R.string.perm_explain_access_to_notifications_denied)
|
||||
.setPositiveButton(R.string.perm_continue, (dialog, which) -> getActivity().startActivity(Permissions.getApplicationSettingsIntent(getActivity())))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_CONNECTIVITY:
|
||||
startActivity(new Intent(getActivity(), ConnectivityActivity.class));
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_APPEARANCE:
|
||||
fragment = new AppearancePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_CHATS:
|
||||
fragment = new ChatsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_PRIVACY:
|
||||
fragment = new PrivacyPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_MULTIDEVICE:
|
||||
if (!ScreenLockUtil.applyScreenLock(getActivity(), getString(R.string.multidevice_title),
|
||||
getString(R.string.multidevice_this_creates_a_qr_code) + "\n\n" + getString(R.string.enter_system_secret_to_continue),
|
||||
ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS)) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.multidevice_title)
|
||||
.setMessage(R.string.multidevice_this_creates_a_qr_code)
|
||||
.setPositiveButton(R.string.perm_continue,
|
||||
(dialog, which) -> ((ApplicationPreferencesActivity)getActivity()).showBackupProvider())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
;
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_ADVANCED:
|
||||
fragment = new AdvancedPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
IntentUtils.showInBrowser(requireActivity(), "https://arcanechat.me/#contribute");
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
startActivity(new Intent(getActivity(), LocalHelpActivity.class));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
case PREFERENCE_CATEGORY_NOTIFICATIONS:
|
||||
NotificationManagerCompat notificationManager =
|
||||
NotificationManagerCompat.from(getActivity());
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
|| notificationManager.areNotificationsEnabled()) {
|
||||
fragment = new NotificationsPreferenceFragment();
|
||||
} else {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.notifications_disabled)
|
||||
.setMessage(R.string.perm_explain_access_to_notifications_denied)
|
||||
.setPositiveButton(
|
||||
R.string.perm_continue,
|
||||
(dialog, which) ->
|
||||
getActivity()
|
||||
.startActivity(
|
||||
Permissions.getApplicationSettingsIntent(getActivity())))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_CONNECTIVITY:
|
||||
startActivity(new Intent(getActivity(), ConnectivityActivity.class));
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_APPEARANCE:
|
||||
fragment = new AppearancePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_CHATS:
|
||||
fragment = new ChatsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_PRIVACY:
|
||||
fragment = new PrivacyPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_MULTIDEVICE:
|
||||
if (!ScreenLockUtil.applyScreenLock(
|
||||
getActivity(),
|
||||
getString(R.string.multidevice_title),
|
||||
getString(R.string.multidevice_this_creates_a_qr_code)
|
||||
+ "\n\n"
|
||||
+ getString(R.string.enter_system_secret_to_continue),
|
||||
screenLockLauncher)) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.multidevice_title)
|
||||
.setMessage(R.string.multidevice_this_creates_a_qr_code)
|
||||
.setPositiveButton(
|
||||
R.string.perm_continue,
|
||||
(dialog, which) ->
|
||||
((ApplicationPreferencesActivity) getActivity()).showBackupProvider())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
;
|
||||
}
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_ADVANCED:
|
||||
fragment = new AdvancedPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
IntentUtils.showInBrowser(requireActivity(), "https://arcanechat.me/#contribute");
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
startActivity(new Intent(getActivity(), LocalHelpActivity.class));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (fragment != null) {
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
fragmentTransaction.replace(R.id.fragment, fragment);
|
||||
fragmentTransaction.addToBackStack(null);
|
||||
@@ -297,7 +319,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
public class AttachContactActivity extends ContactSelectionActivity {
|
||||
|
||||
public static final String CONTACT_ID_EXTRA = "contact_id_extra";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -15,14 +13,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
|
||||
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
||||
@@ -40,11 +35,13 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
// Only enable Edge-to-Edge if it is well supported
|
||||
if (ViewUtil.isEdgeToEdgeSupported()) {
|
||||
// docs says to use: WindowCompat.enableEdgeToEdge(getWindow());
|
||||
// but it actually makes things worse, the next takes care of setting the 3-buttons navigation bar background
|
||||
// but it actually makes things worse, the next takes care of setting the 3-buttons navigation
|
||||
// bar background
|
||||
EdgeToEdge.enable(this);
|
||||
|
||||
// force white text in status bar so it visible over background color
|
||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()).setAppearanceLightStatusBars(false);
|
||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView())
|
||||
.setAppearanceLightStatusBars(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +65,12 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from: http://stackoverflow.com/a/13098824
|
||||
*/
|
||||
/** Modified from: http://stackoverflow.com/a/13098824 */
|
||||
private void forceOverflowMenu() {
|
||||
try {
|
||||
ViewConfiguration config = ViewConfiguration.get(this);
|
||||
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
|
||||
if(menuKeyField != null) {
|
||||
ViewConfiguration config = ViewConfiguration.get(this);
|
||||
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
|
||||
if (menuKeyField != null) {
|
||||
menuKeyField.setAccessible(true);
|
||||
menuKeyField.setBoolean(config, false);
|
||||
}
|
||||
@@ -97,21 +92,18 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
} else if (item == searchItem) {
|
||||
; // searchItem is just always visible
|
||||
} else {
|
||||
item.setVisible(!visible); // if search is shown, other items are hidden - and the other way round
|
||||
item.setVisible(
|
||||
!visible); // if search is shown, other items are hidden - and the other way round
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@NonNull T fragment)
|
||||
{
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target, @NonNull T fragment) {
|
||||
return initFragment(target, fragment, null);
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@NonNull T fragment,
|
||||
@Nullable Bundle extras)
|
||||
{
|
||||
protected <T extends Fragment> T initFragment(
|
||||
@IdRes int target, @NonNull T fragment, @Nullable Bundle extras) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if (extras != null) {
|
||||
@@ -119,9 +111,10 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
fragment.setArguments(args);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(target, fragment)
|
||||
.commitAllowingStateLoss();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(target, fragment)
|
||||
.commitAllowingStateLoss();
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,42 +6,37 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
|
||||
public abstract class BaseConversationItem extends LinearLayout
|
||||
implements BindableConversationItem
|
||||
{
|
||||
implements BindableConversationItem {
|
||||
static final long PULSE_HIGHLIGHT_MILLIS = 500;
|
||||
|
||||
protected DcMsg messageRecord;
|
||||
protected DcChat dcChat;
|
||||
protected TextView bodyText;
|
||||
protected DcMsg messageRecord;
|
||||
protected DcChat dcChat;
|
||||
protected TextView bodyText;
|
||||
|
||||
protected final Context context;
|
||||
protected final DcContext dcContext;
|
||||
protected final Context context;
|
||||
protected final DcContext dcContext;
|
||||
protected final Rpc rpc;
|
||||
protected Recipient conversationRecipient;
|
||||
protected Recipient conversationRecipient;
|
||||
|
||||
protected @NonNull Set<DcMsg> batchSelected = new HashSet<>();
|
||||
protected @NonNull Set<DcMsg> batchSelected = new HashSet<>();
|
||||
|
||||
protected final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
protected final PassthroughClickListener passthroughClickListener =
|
||||
new PassthroughClickListener();
|
||||
|
||||
public BaseConversationItem(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -50,32 +45,32 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
this.rpc = DcHelper.getRpc(context);
|
||||
}
|
||||
|
||||
protected void bind(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.dcChat = dcChat;
|
||||
this.batchSelected = batchSelected;
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
protected void bindPartial(
|
||||
@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.dcChat = dcChat;
|
||||
this.batchSelected = batchSelected;
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
setInteractionState(messageRecord, pulseHighlight);
|
||||
}
|
||||
|
||||
protected void setInteractionState(DcMsg messageRecord, boolean pulseHighlight) {
|
||||
final int[] attributes = new int[] {
|
||||
R.attr.conversation_item_background,
|
||||
R.attr.conversation_item_background_animated,
|
||||
};
|
||||
final int[] attributes =
|
||||
new int[] {
|
||||
R.attr.conversation_item_background, R.attr.conversation_item_background_animated,
|
||||
};
|
||||
|
||||
if (batchSelected.contains(messageRecord)) {
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
ViewUtil.setBackground(this, attrs.getDrawable(0));
|
||||
attrs.recycle();
|
||||
setSelected(true);
|
||||
} else if (pulseHighlight) {
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
ViewUtil.setBackground(this, attrs.getDrawable(1));
|
||||
attrs.recycle();
|
||||
setSelected(true);
|
||||
@@ -92,15 +87,16 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
|
||||
protected boolean shouldInterceptClicks(DcMsg messageRecord) {
|
||||
return batchSelected.isEmpty()
|
||||
&& (messageRecord.isFailed()
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL);
|
||||
&& (messageRecord.isFailed()
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL);
|
||||
}
|
||||
|
||||
protected void onAccessibilityClick() {}
|
||||
|
||||
protected class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener {
|
||||
protected class PassthroughClickListener
|
||||
implements View.OnLongClickListener, View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
@@ -126,6 +122,8 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
|
||||
public void onClick(View v) {
|
||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
// The click workaround on ConversationItem shall be revised.
|
||||
// In fact, it is probably better rethinking accessibility approach for the items.
|
||||
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
|
||||
BaseConversationItem.this.onAccessibilityClick();
|
||||
}
|
||||
@@ -135,13 +133,15 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
TextView detailsText = view.findViewById(R.id.details_text);
|
||||
detailsText.setText(messageRecord.getError());
|
||||
|
||||
AlertDialog d = new AlertDialog.Builder(context)
|
||||
AlertDialog d =
|
||||
new AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(R.string.error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.create();
|
||||
d.show();
|
||||
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE || messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED) {
|
||||
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE
|
||||
|| messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED) {
|
||||
DcHelper.showProtectionEnabledDialog(context);
|
||||
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL) {
|
||||
DcHelper.showInvalidUnencryptedDialog(context);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class BaseConversationListAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
|
||||
protected final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
|
||||
protected boolean batchMode = false;
|
||||
public abstract class BaseConversationListAdapter<T extends RecyclerView.ViewHolder>
|
||||
extends RecyclerView.Adapter<T> {
|
||||
protected final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
|
||||
protected boolean batchMode = false;
|
||||
|
||||
public abstract void selectAllThreads();
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -27,36 +26,36 @@ import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.connect.DirectShareUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class BaseConversationListFragment extends Fragment implements ActionMode.Callback {
|
||||
protected ActionMode actionMode;
|
||||
protected PulsingFloatingActionButton fab;
|
||||
|
||||
protected abstract boolean offerToArchive();
|
||||
|
||||
protected abstract void setFabVisibility(boolean isActionMode);
|
||||
|
||||
protected abstract BaseConversationListAdapter getListAdapter();
|
||||
|
||||
protected void onItemClick(long chatId) {
|
||||
if (actionMode == null) {
|
||||
((ConversationSelectedListener) requireActivity()).onCreateConversation((int)chatId);
|
||||
((ConversationSelectedListener) requireActivity()).onCreateConversation((int) chatId);
|
||||
} else {
|
||||
BaseConversationListAdapter adapter = getListAdapter();
|
||||
adapter.toggleThreadInBatchSet(chatId);
|
||||
@@ -71,8 +70,9 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void onItemLongClick(long chatId) {
|
||||
actionMode = ((AppCompatActivity)requireActivity()).startSupportActionMode(this);
|
||||
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(this);
|
||||
|
||||
if (actionMode != null) {
|
||||
getListAdapter().initializeBatchMode(true);
|
||||
@@ -89,36 +89,54 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
Intent intent = new Intent(getActivity(), NewConversationActivity.class);
|
||||
if (isRelayingMessageContent(getActivity())) {
|
||||
if (isActionMode) {
|
||||
fab.setOnClickListener(v -> {
|
||||
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
|
||||
ArrayList<Uri> uris = getSharedUris(getActivity());
|
||||
String message;
|
||||
if (isForwarding(getActivity())) {
|
||||
message = String.format(Util.getLocale(), getString(R.string.ask_forward_multiple), selectedChats.size());
|
||||
} else if (!uris.isEmpty()) {
|
||||
message = String.format(Util.getLocale(), getString(R.string.ask_send_files_to_selected_chats), uris.size(), selectedChats.size());
|
||||
} else {
|
||||
message = String.format(Util.getLocale(), getString(R.string.share_text_multiple_chats), selectedChats.size(), getSharedText(getActivity()));
|
||||
}
|
||||
fab.setOnClickListener(
|
||||
v -> {
|
||||
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
|
||||
ArrayList<Uri> uris = getSharedUris(getActivity());
|
||||
String message;
|
||||
if (isForwarding(getActivity())) {
|
||||
message =
|
||||
String.format(
|
||||
Util.getLocale(),
|
||||
getString(R.string.ask_forward_multiple),
|
||||
selectedChats.size());
|
||||
} else if (!uris.isEmpty()) {
|
||||
message =
|
||||
String.format(
|
||||
Util.getLocale(),
|
||||
getString(R.string.ask_send_files_to_selected_chats),
|
||||
uris.size(),
|
||||
selectedChats.size());
|
||||
} else {
|
||||
message =
|
||||
String.format(
|
||||
Util.getLocale(),
|
||||
getString(R.string.share_text_multiple_chats),
|
||||
selectedChats.size(),
|
||||
getSharedText(getActivity()));
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
if (SendRelayedMessageUtil.containsVideoType(context, uris)) {
|
||||
message += "\n\n" + getString(R.string.videos_sent_without_recoding);
|
||||
}
|
||||
new AlertDialog.Builder(context)
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
if (SendRelayedMessageUtil.containsVideoType(context, uris)) {
|
||||
message += "\n\n" + getString(R.string.videos_sent_without_recoding);
|
||||
}
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> {}))
|
||||
.setPositiveButton(R.string.menu_send, (dialog, which) -> {
|
||||
SendRelayedMessageUtil.immediatelyRelay(getActivity(), selectedChats.toArray(new Long[selectedChats.size()]));
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
getActivity().finish();
|
||||
})
|
||||
.setPositiveButton(
|
||||
R.string.menu_send,
|
||||
(dialog, which) -> {
|
||||
SendRelayedMessageUtil.immediatelyRelay(
|
||||
getActivity(), selectedChats.toArray(new Long[selectedChats.size()]));
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
getActivity().finish();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
acquireRelayMessageContent(getActivity(), intent);
|
||||
fab.setOnClickListener(v -> requireActivity().startActivity(intent));
|
||||
@@ -132,8 +150,8 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
|
||||
for (long chatId : selectedChats) {
|
||||
DcChat dcChat = dcContext.getChat((int)chatId);
|
||||
if (dcChat.getVisibility()!=DcChat.DC_CHAT_VISIBILITY_PINNED) {
|
||||
DcChat dcChat = dcContext.getChat((int) chatId);
|
||||
if (dcChat.getVisibility() != DcChat.DC_CHAT_VISIBILITY_PINNED) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -144,7 +162,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
|
||||
for (long chatId : selectedChats) {
|
||||
DcChat dcChat = dcContext.getChat((int)chatId);
|
||||
DcChat dcChat = dcContext.getChat((int) chatId);
|
||||
if (!dcChat.isMuted()) {
|
||||
return true;
|
||||
}
|
||||
@@ -153,12 +171,14 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
}
|
||||
|
||||
private void handlePinAllSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations = new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations =
|
||||
new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
boolean doPin = areSomeSelectedChatsUnpinned();
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.setChatVisibility((int)chatId,
|
||||
doPin? DcChat.DC_CHAT_VISIBILITY_PINNED : DcChat.DC_CHAT_VISIBILITY_NORMAL);
|
||||
dcContext.setChatVisibility(
|
||||
(int) chatId,
|
||||
doPin ? DcChat.DC_CHAT_VISIBILITY_PINNED : DcChat.DC_CHAT_VISIBILITY_NORMAL);
|
||||
}
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
@@ -167,23 +187,26 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
}
|
||||
|
||||
private void handleMuteAllSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations = new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations =
|
||||
new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
if (areSomeSelectedChatsUnmuted()) {
|
||||
MuteDialog.show(getActivity(), duration -> {
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.setChatMuteDuration((int)chatId, duration);
|
||||
}
|
||||
MuteDialog.show(
|
||||
getActivity(),
|
||||
duration -> {
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.setChatMuteDuration((int) chatId, duration);
|
||||
}
|
||||
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// unmute
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.setChatMuteDuration((int)chatId, 0);
|
||||
dcContext.setChatMuteDuration((int) chatId, 0);
|
||||
}
|
||||
|
||||
if (actionMode != null) {
|
||||
@@ -194,10 +217,11 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
}
|
||||
|
||||
private void handleMarknoticedSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations = new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations =
|
||||
new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.marknoticedChat((int)chatId);
|
||||
dcContext.marknoticedChat((int) chatId);
|
||||
}
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
@@ -207,22 +231,21 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleArchiveAllSelected() {
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations = new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
final boolean archive = offerToArchive();
|
||||
final DcContext dcContext = DcHelper.getContext(requireActivity());
|
||||
final Set<Long> selectedConversations =
|
||||
new HashSet<Long>(getListAdapter().getBatchSelections());
|
||||
final boolean archive = offerToArchive();
|
||||
|
||||
int snackBarTitleId;
|
||||
|
||||
if (archive) snackBarTitleId = R.plurals.chat_archived;
|
||||
else snackBarTitleId = R.plurals.chat_unarchived;
|
||||
else snackBarTitleId = R.plurals.chat_unarchived;
|
||||
|
||||
int count = selectedConversations.size();
|
||||
int count = selectedConversations.size();
|
||||
String snackBarTitle = getResources().getQuantityString(snackBarTitleId, count, count);
|
||||
|
||||
new SnackbarAsyncTask<Void>(getView(), snackBarTitle,
|
||||
getString(R.string.undo),
|
||||
Snackbar.LENGTH_LONG, true)
|
||||
{
|
||||
new SnackbarAsyncTask<Void>(
|
||||
getView(), snackBarTitle, getString(R.string.undo), Snackbar.LENGTH_LONG, true) {
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
@@ -237,16 +260,18 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
@Override
|
||||
protected void executeAction(@Nullable Void parameter) {
|
||||
for (long chatId : selectedConversations) {
|
||||
dcContext.setChatVisibility((int)chatId,
|
||||
archive? DcChat.DC_CHAT_VISIBILITY_ARCHIVED : DcChat.DC_CHAT_VISIBILITY_NORMAL);
|
||||
dcContext.setChatVisibility(
|
||||
(int) chatId,
|
||||
archive ? DcChat.DC_CHAT_VISIBILITY_ARCHIVED : DcChat.DC_CHAT_VISIBILITY_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reverseAction(@Nullable Void parameter) {
|
||||
for (long threadId : selectedConversations) {
|
||||
dcContext.setChatVisibility((int)threadId,
|
||||
archive? DcChat.DC_CHAT_VISIBILITY_NORMAL : DcChat.DC_CHAT_VISIBILITY_ARCHIVED);
|
||||
dcContext.setChatVisibility(
|
||||
(int) threadId,
|
||||
archive ? DcChat.DC_CHAT_VISIBILITY_NORMAL : DcChat.DC_CHAT_VISIBILITY_ARCHIVED);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
@@ -262,51 +287,73 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
final String alertText;
|
||||
if (chatsCount == 1) {
|
||||
long chatId = selectedChats.iterator().next();
|
||||
alertText = activity.getResources().getString(R.string.ask_delete_named_chat, dcContext.getChat((int)chatId).getName());
|
||||
alertText =
|
||||
activity
|
||||
.getResources()
|
||||
.getString(R.string.ask_delete_named_chat, dcContext.getChat((int) chatId).getName());
|
||||
} else {
|
||||
alertText = activity.getResources().getQuantityString(R.plurals.ask_delete_chat, chatsCount, chatsCount);
|
||||
alertText =
|
||||
activity
|
||||
.getResources()
|
||||
.getQuantityString(R.plurals.ask_delete_chat, chatsCount, chatsCount);
|
||||
}
|
||||
|
||||
String alertButton = getString(R.string.delete_for_me);
|
||||
for (long chatId : selectedChats) {
|
||||
if (dcContext.getChat((int) chatId).shallLeaveBeforeDelete(dcContext)) {
|
||||
alertButton = getString(R.string.menu_leave_and_delete);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
alert.setMessage(alertText);
|
||||
alert.setCancelable(true);
|
||||
|
||||
alert.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
alert.setPositiveButton(
|
||||
alertButton,
|
||||
(dialog, which) -> {
|
||||
if (!selectedChats.isEmpty()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
private ProgressDialog dialog;
|
||||
|
||||
if (!selectedChats.isEmpty()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
private ProgressDialog dialog;
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
dialog =
|
||||
ProgressDialog.show(
|
||||
getActivity(),
|
||||
"",
|
||||
requireActivity().getString(R.string.one_moment),
|
||||
true,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
dialog = ProgressDialog.show(getActivity(),
|
||||
"",
|
||||
requireActivity().getString(R.string.one_moment),
|
||||
true, false);
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
int accountId = dcContext.getAccountId();
|
||||
for (long chatId : selectedChats) {
|
||||
DcHelper.getNotificationCenter(requireContext())
|
||||
.removeNotifications(accountId, (int) chatId);
|
||||
if (dcContext.getChat((int) chatId).shallLeaveBeforeDelete(dcContext)) {
|
||||
dcContext.removeContactFromChat((int) chatId, DcContact.DC_CONTACT_ID_SELF);
|
||||
}
|
||||
dcContext.deleteChat((int) chatId);
|
||||
DirectShareUtil.clearShortcut(requireContext(), (int) chatId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
dialog.dismiss();
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
int accountId = dcContext.getAccountId();
|
||||
for (long chatId : selectedChats) {
|
||||
DcHelper.getNotificationCenter(requireContext()).removeNotifications(accountId, (int) chatId);
|
||||
dcContext.deleteChat((int) chatId);
|
||||
DirectShareUtil.clearShortcut(requireContext(), (int) chatId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
dialog.dismiss();
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
alert.setNegativeButton(android.R.string.cancel, null);
|
||||
AlertDialog dialog = alert.show();
|
||||
@@ -331,22 +378,30 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
intent.putExtra(ShareActivity.EXTRA_CHAT_ID, chat.getId());
|
||||
|
||||
Recipient recipient = new Recipient(activity, chat);
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
Bitmap avatar = DirectShareUtil.getIconForShortcut(activity, recipient);
|
||||
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(activity, "chat-" + dcContext.getAccountId() + "-" + chat.getId())
|
||||
.setShortLabel(chat.getName())
|
||||
.setIcon(IconCompat.createWithAdaptiveBitmap(avatar))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
Util.runOnMain(() -> {
|
||||
if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) {
|
||||
Toast.makeText(activity, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show();
|
||||
} else if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
Bitmap avatar = DirectShareUtil.getIconForShortcut(activity, recipient);
|
||||
ShortcutInfoCompat shortcutInfoCompat =
|
||||
new ShortcutInfoCompat.Builder(
|
||||
activity, "chat-" + dcContext.getAccountId() + "-" + chat.getId())
|
||||
.setShortLabel(chat.getName())
|
||||
.setIcon(IconCompat.createWithAdaptiveBitmap(avatar))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
"ErrAddToHomescreen: requestPinShortcut() failed",
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void updateActionModeItems(Menu menu) {
|
||||
@@ -356,11 +411,11 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
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);
|
||||
archiveItem.setTitle(R.string.menu_archive_chat);
|
||||
archiveItem.setIcon(R.drawable.ic_archive_white_24dp);
|
||||
archiveItem.setTitle(R.string.menu_archive_chat);
|
||||
} else {
|
||||
archiveItem.setIcon(R.drawable.ic_unarchive_white_24dp);
|
||||
archiveItem.setTitle(R.string.menu_unarchive_chat);
|
||||
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()) {
|
||||
@@ -383,7 +438,8 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
if (isRelayingMessageContent(getActivity())) {
|
||||
if (ShareUtil.getSharedContactId(getActivity()) != 0) {
|
||||
return false; // no sharing of a contact to multiple recipients at the same time, we can reconsider when that becomes a real-world need
|
||||
return false; // no sharing of a contact to multiple recipients at the same time, we can
|
||||
// reconsider when that becomes a real-world need
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
@@ -451,6 +507,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
|
||||
public interface ConversationSelectedListener {
|
||||
void onCreateConversation(int chatId);
|
||||
|
||||
void onSwitchToArchive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,24 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight);
|
||||
void bind(
|
||||
@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener);
|
||||
|
||||
DcMsg getMessageRecord();
|
||||
|
||||
@@ -25,9 +27,15 @@ public interface BindableConversationItem extends Unbindable {
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(DcMsg messageRecord);
|
||||
|
||||
void onJumpToOriginalClicked(DcMsg messageRecord);
|
||||
|
||||
void onShowFullClicked(DcMsg messageRecord);
|
||||
|
||||
void onDownloadClicked(DcMsg messageRecord);
|
||||
|
||||
void onReactionClicked(DcMsg messageRecord);
|
||||
|
||||
void onStickerClicked(DcMsg messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcLot;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationListItem extends Unbindable {
|
||||
|
||||
public void bind(@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
public void bind(
|
||||
@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -14,10 +13,8 @@ import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcContactsLoader;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -39,17 +36,18 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class BlockedAndShareContactsFragment
|
||||
extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<DcContactsLoader.Ret>,
|
||||
DcEventCenter.DcEventDelegate, ContactSelectionListAdapter.ItemClickListener {
|
||||
|
||||
public static class BlockedAndShareContactsFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<DcContactsLoader.Ret>,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
ContactSelectionListAdapter.ItemClickListener {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TextView emptyStateView;
|
||||
@@ -57,7 +55,7 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
@@ -81,11 +79,8 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void initializeAdapter() {
|
||||
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
|
||||
GlideApp.with(this),
|
||||
this,
|
||||
false,
|
||||
false);
|
||||
ContactSelectionListAdapter adapter =
|
||||
new ContactSelectionListAdapter(getActivity(), GlideApp.with(this), this, false, false);
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@@ -112,7 +107,7 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
if (event.getId()==DcContext.DC_EVENT_CONTACTS_CHANGED) {
|
||||
if (event.getId() == DcContext.DC_EVENT_CONTACTS_CHANGED) {
|
||||
restartLoader();
|
||||
}
|
||||
}
|
||||
@@ -128,10 +123,12 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem item, boolean handleActionMode) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setMessage(R.string.ask_unblock_contact)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.menu_unblock_contact, (dialog, which) -> unblockContact(item.getContactId())).show();
|
||||
.setMessage(R.string.ask_unblock_contact)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(
|
||||
R.string.menu_unblock_contact, (dialog, which) -> unblockContact(item.getContactId()))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void unblockContact(int contactId) {
|
||||
@@ -143,5 +140,4 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onItemLongClick(ContactSelectionListItem view) {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
@@ -35,8 +32,10 @@ public class ConnectivityActivity extends WebViewActivity implements DcEventCent
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
final String connectivityHtml = DcHelper.getContext(this).getConnectivityHtml()
|
||||
.replace("</style>", " html { color-scheme: dark light; }</style>");
|
||||
final String connectivityHtml =
|
||||
DcHelper.getContext(this)
|
||||
.getConnectivityHtml()
|
||||
.replace("</style>", " html { color-scheme: dark light; }</style>");
|
||||
webView.loadDataWithBaseURL(null, connectivityHtml, "text/html", "utf-8", null);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,11 +28,11 @@ import java.util.List;
|
||||
* Activity container for selecting a list of contacts.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
|
||||
|
||||
public static final String CONTACTS_EXTRA = "contacts_extra";
|
||||
public static final String DESELECTED_CONTACTS_EXTRA = "deselected_contacts_extra";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
@@ -71,7 +70,10 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
|
||||
private void saveSelection() {
|
||||
Intent resultIntent = getIntent();
|
||||
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<Integer> deselectedContacts = contactsFragment.getDeselectedContacts();
|
||||
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
|
||||
resultIntent.putIntegerArrayListExtra(
|
||||
DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts));
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -27,11 +26,9 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
* Base activity container for selecting a list of contacts.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener
|
||||
{
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
protected ContactSelectionListFragment contactsFragment;
|
||||
@@ -61,7 +58,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
getSupportActionBar().setIcon(null);
|
||||
@@ -69,7 +66,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment =
|
||||
(ContactSelectionListFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -31,7 +30,8 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -41,11 +41,14 @@ import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.connect.DcContactsLoader;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -57,46 +60,52 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class ContactSelectionListFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<DcContactsLoader.Ret>,
|
||||
DcEventCenter.DcEventDelegate
|
||||
{
|
||||
public class ContactSelectionListFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<DcContactsLoader.Ret>, DcEventCenter.DcEventDelegate {
|
||||
private static final String TAG = ContactSelectionListFragment.class.getSimpleName();
|
||||
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String SELECT_UNENCRYPTED_EXTRA = "select_unencrypted_extra";
|
||||
public static final String ALLOW_CREATION = "allow_creation";
|
||||
public static final String PRESELECTED_CONTACTS = "preselected_contacts";
|
||||
public static final int CONTACT_ADDR_RESULT_CODE = 61123;
|
||||
|
||||
private DcContext dcContext;
|
||||
|
||||
private Set<Integer> selectedContacts;
|
||||
private Set<Integer> selectedContacts;
|
||||
private Set<Integer> deselectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private TextView emptyView;
|
||||
private ActionMode actionMode;
|
||||
private ActionMode.Callback actionModeCallback;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private TextView emptyView;
|
||||
private ActionMode actionMode;
|
||||
private ActionMode.Callback actionModeCallback;
|
||||
private ActivityResultLauncher<Intent> newContactLauncher;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle icicle) {
|
||||
super.onActivityCreated(icicle);
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
dcContext = DcHelper.getContext(getActivity());
|
||||
DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
initializeCursor();
|
||||
dcContext = DcHelper.getContext(requireContext());
|
||||
newContactLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
int contactId =
|
||||
result.getData().getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
|
||||
if (contactId != 0) {
|
||||
selectedContacts.add(contactId);
|
||||
deselectedContacts.remove(contactId);
|
||||
}
|
||||
LoaderManager.getInstance(this)
|
||||
.restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,52 +121,59 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
emptyView = ViewUtil.findById(view, android.R.id.empty);
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
emptyView = ViewUtil.findById(view, android.R.id.empty);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
actionModeCallback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.contact_list, menu);
|
||||
updateActionModeState(actionMode);
|
||||
return true;
|
||||
}
|
||||
actionModeCallback =
|
||||
new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.contact_list, menu);
|
||||
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
|
||||
updateActionModeState(actionMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.menu_select_all) {
|
||||
handleSelectAll();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_view_profile) {
|
||||
handleViewProfile();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_delete_selected) {
|
||||
handleDeleteSelected();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.menu_select_all) {
|
||||
handleSelectAll();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_view_profile) {
|
||||
handleViewProfile();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_delete_selected) {
|
||||
handleDeleteSelected();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode actionMode) {
|
||||
ContactSelectionListFragment.this.actionMode = null;
|
||||
getContactSelectionListAdapter().resetActionModeSelection();
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode actionMode) {
|
||||
ContactSelectionListFragment.this.actionMode = null;
|
||||
getContactSelectionListAdapter().resetActionModeSelection();
|
||||
}
|
||||
};
|
||||
|
||||
DcHelper.getEventCenter(requireActivity())
|
||||
.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
initializeCursor();
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -189,22 +205,28 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleDeleteSelected() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(getActivity())
|
||||
.setMessage(R.string.ask_delete_contacts)
|
||||
.setPositiveButton(R.string.delete, (d, i) -> {
|
||||
ContactSelectionListAdapter adapter = getContactSelectionListAdapter();
|
||||
final SparseIntArray actionModeSelection = adapter.getActionModeSelection().clone();
|
||||
new Thread(() -> {
|
||||
for (int index = 0; index < actionModeSelection.size(); index++) {
|
||||
int contactId = actionModeSelection.valueAt(index);
|
||||
dcContext.deleteContact(contactId);
|
||||
}
|
||||
}).start();
|
||||
adapter.resetActionModeSelection();
|
||||
actionMode.finish();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
AlertDialog dialog =
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setMessage(R.string.ask_delete_contacts)
|
||||
.setPositiveButton(
|
||||
R.string.delete,
|
||||
(d, i) -> {
|
||||
ContactSelectionListAdapter adapter = getContactSelectionListAdapter();
|
||||
final SparseIntArray actionModeSelection =
|
||||
adapter.getActionModeSelection().clone();
|
||||
new Thread(
|
||||
() -> {
|
||||
for (int index = 0; index < actionModeSelection.size(); index++) {
|
||||
int contactId = actionModeSelection.valueAt(index);
|
||||
dcContext.deleteContact(contactId);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
adapter.resetActionModeSelection();
|
||||
actionMode.finish();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
Util.redPositiveButton(dialog);
|
||||
}
|
||||
|
||||
@@ -213,7 +235,8 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@@ -226,6 +249,15 @@ public class ContactSelectionListFragment extends Fragment
|
||||
return selected;
|
||||
}
|
||||
|
||||
public @NonNull List<Integer> getDeselectedContacts() {
|
||||
List<Integer> deselected = new LinkedList<>();
|
||||
if (deselectedContacts != null) {
|
||||
deselected.addAll(deselectedContacts);
|
||||
}
|
||||
|
||||
return deselected;
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
@@ -235,14 +267,14 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
|
||||
GlideApp.with(this),
|
||||
new ListClickListener(),
|
||||
isMulti(),
|
||||
true);
|
||||
ContactSelectionListAdapter adapter =
|
||||
new ContactSelectionListAdapter(
|
||||
getActivity(), GlideApp.with(this), new ListClickListener(), isMulti(), true);
|
||||
selectedContacts = adapter.getSelectedContacts();
|
||||
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
|
||||
if(preselectedContacts!=null) {
|
||||
deselectedContacts = new HashSet<>();
|
||||
ArrayList<Integer> preselectedContacts =
|
||||
getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
|
||||
if (preselectedContacts != null) {
|
||||
selectedContacts.addAll(preselectedContacts);
|
||||
}
|
||||
recyclerView.setAdapter(adapter);
|
||||
@@ -257,11 +289,20 @@ public class ContactSelectionListFragment extends Fragment
|
||||
public Loader<DcContactsLoader.Ret> onCreateLoader(int id, Bundle args) {
|
||||
final boolean allowCreation = getActivity().getIntent().getBooleanExtra(ALLOW_CREATION, true);
|
||||
final boolean addCreateContactLink = allowCreation && isUnencrypted();
|
||||
final boolean addCreateGroupLinks = allowCreation && !isRelayingMessageContent(getActivity()) && !isMulti();
|
||||
final boolean addCreateGroupLinks =
|
||||
allowCreation && !isRelayingMessageContent(getActivity()) && !isMulti();
|
||||
final boolean addScanQRLink = allowCreation && !isMulti();
|
||||
|
||||
final int listflags = DcContext.DC_GCL_ADD_SELF | (isUnencrypted()? DcContext.DC_GCL_ADDRESS : 0);
|
||||
return new DcContactsLoader(getActivity(), listflags, cursorFilter, addCreateGroupLinks, addCreateContactLink, addScanQRLink, false);
|
||||
final int listflags =
|
||||
DcContext.DC_GCL_ADD_SELF | (isUnencrypted() ? DcContext.DC_GCL_ADDRESS : 0);
|
||||
return new DcContactsLoader(
|
||||
getActivity(),
|
||||
listflags,
|
||||
cursorFilter,
|
||||
addCreateGroupLinks,
|
||||
addCreateContactLink,
|
||||
addScanQRLink,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -285,15 +326,14 @@ public class ContactSelectionListFragment extends Fragment
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact, boolean handleActionMode)
|
||||
{
|
||||
public void onItemClick(ContactSelectionListItem contact, boolean handleActionMode) {
|
||||
if (handleActionMode) {
|
||||
if (actionMode != null) {
|
||||
updateActionModeState(actionMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int contactId = contact.getSpecialId();
|
||||
int contactId = contact.getSpecialId();
|
||||
if (!isMulti() || !selectedContacts.contains(contactId)) {
|
||||
if (contactId == DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT) {
|
||||
Intent intent = new Intent(getContext(), NewContactActivity.class);
|
||||
@@ -301,7 +341,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
intent.putExtra(NewContactActivity.ADDR_EXTRA, cursorFilter);
|
||||
}
|
||||
if (isMulti()) {
|
||||
startActivityForResult(intent, CONTACT_ADDR_RESULT_CODE);
|
||||
newContactLauncher.launch(intent);
|
||||
} else {
|
||||
requireContext().startActivity(intent);
|
||||
}
|
||||
@@ -309,12 +349,14 @@ public class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
selectedContacts.add(contactId);
|
||||
deselectedContacts.remove(contactId);
|
||||
contact.setChecked(true);
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contactId);
|
||||
}
|
||||
} else {
|
||||
selectedContacts.remove(contactId);
|
||||
deselectedContacts.add(contactId);
|
||||
contact.setChecked(false);
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contactId);
|
||||
@@ -325,37 +367,26 @@ public class ContactSelectionListFragment extends Fragment
|
||||
@Override
|
||||
public void onItemLongClick(ContactSelectionListItem view) {
|
||||
if (actionMode == null) {
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
|
||||
} else {
|
||||
updateActionModeState(actionMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
this.onContactSelectedListener = onContactSelectedListener;
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
void onContactSelected(int contactId);
|
||||
|
||||
void onContactDeselected(int contactId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
if (event.getId()==DcContext.DC_EVENT_CONTACTS_CHANGED) {
|
||||
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
if (resultCode == Activity.RESULT_OK && reqCode == CONTACT_ADDR_RESULT_CODE) {
|
||||
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
|
||||
if (contactId != 0) {
|
||||
selectedContacts.add(contactId);
|
||||
}
|
||||
if (event.getId() == DcContext.DC_EVENT_CONTACTS_CHANGED) {
|
||||
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
@@ -65,7 +66,12 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
@@ -76,7 +82,7 @@ import com.b44t.messenger.DcMsg;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
@@ -86,6 +92,8 @@ import org.thoughtcrime.securesms.components.InputPanel;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||
import org.thoughtcrime.securesms.components.ScaleStableImageView;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -104,19 +112,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -138,14 +146,13 @@ import chat.delta.util.SettableFuture;
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener
|
||||
{
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener, AudioView.OnActionListener {
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
@@ -185,6 +192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private MediaKeyboard emojiPicker;
|
||||
protected HidingLinearLayout quickAttachmentToggle;
|
||||
private InputPanel inputPanel;
|
||||
private @Nullable MediaController mediaController;
|
||||
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private ApplicationContext context;
|
||||
private Recipient recipient;
|
||||
@@ -217,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeActionBar();
|
||||
initializeViews();
|
||||
initializeResources();
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
|
||||
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -267,6 +281,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken = new SessionToken(this,
|
||||
new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
|
||||
.buildAsync();
|
||||
mediaControllerFuture.addListener(() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -337,7 +381,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DcHelper.getNotificationCenter(this).clearVisibleChat();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
||||
inputPanel.onPause();
|
||||
AudioSlidePlayer.stopAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -357,6 +400,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -483,15 +531,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
menu.findItem(R.id.menu_ephemeral_messages).setVisible(false);
|
||||
}
|
||||
|
||||
if (isMultiUser()) {
|
||||
if (dcChat.isInBroadcast() && !dcChat.isContactRequest()) {
|
||||
menu.findItem(R.id.menu_leave).setTitle(R.string.menu_leave_channel).setVisible(true);
|
||||
} else if (dcChat.isEncrypted()
|
||||
&& dcChat.canSend()
|
||||
&& !dcChat.isOutBroadcast()
|
||||
&& !dcChat.isMailingList()) {
|
||||
menu.findItem(R.id.menu_leave).setVisible(true);
|
||||
if (dcChat.shallLeaveBeforeDelete(DcHelper.getContext(context))) {
|
||||
if (dcChat.isInBroadcast()) {
|
||||
menu.findItem(R.id.menu_leave).setTitle(R.string.menu_leave_channel);
|
||||
}
|
||||
menu.findItem(R.id.menu_leave).setVisible(true);
|
||||
menu.findItem(R.id.menu_delete_chat).setVisible(false);
|
||||
}
|
||||
|
||||
if (isArchived()) {
|
||||
@@ -569,8 +614,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
} else if (itemId == R.id.menu_show_map) {
|
||||
WebxdcActivity.openMaps(this, chatId);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_call) {
|
||||
CallUtil.startCall(this, chatId);
|
||||
} else if (itemId == R.id.menu_start_audio_call) {
|
||||
CallUtil.startCall(this, chatId, false);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_video_call) {
|
||||
CallUtil.startCall(this, chatId, true);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_all_media) {
|
||||
handleAllMedia();
|
||||
@@ -631,6 +679,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
|
||||
}
|
||||
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
|
||||
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -679,9 +729,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DcHelper.getContext(context).removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF);
|
||||
Toast.makeText(this, getString(R.string.done), Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNegativeButton(R.string.menu_leave_and_delete, (d, which) -> {
|
||||
DcHelper.getContext(context).removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF);
|
||||
DcHelper.getContext(context).deleteChat(chatId);
|
||||
DirectShareUtil.clearShortcut(this, chatId);
|
||||
finish();
|
||||
})
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.show();
|
||||
Util.redPositiveButton(dialog);
|
||||
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
|
||||
}
|
||||
|
||||
private void handleArchiveChat() {
|
||||
@@ -700,7 +757,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private void handleDeleteChat() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setMessage(getResources().getString(R.string.ask_delete_named_chat, dcChat.getName()))
|
||||
.setPositiveButton(R.string.delete, (d, which) -> {
|
||||
.setPositiveButton(R.string.delete_for_me, (d, which) -> {
|
||||
DcHelper.getContext(context).deleteChat(chatId);
|
||||
DirectShareUtil.clearShortcut(this, chatId);
|
||||
finish();
|
||||
@@ -1004,20 +1061,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient = new Recipient(this, dcChat);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(true);
|
||||
initializeContactRequest();
|
||||
}
|
||||
|
||||
private void setComposePanelVisibility() {
|
||||
private void setComposePanelVisibility(boolean isInitialization) {
|
||||
if (dcChat.canSend()) {
|
||||
composePanel.setVisibility(View.VISIBLE);
|
||||
attachmentManager.setHidden(false);
|
||||
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
|
||||
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
|
||||
// fragment.handleRemoveBottomInsets();
|
||||
} else {
|
||||
composePanel.setVisibility(View.GONE);
|
||||
attachmentManager.setHidden(true);
|
||||
hideSoftKeyboard();
|
||||
inputPanel.setSubjectVisible(false);
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
|
||||
/*
|
||||
if (isInitialization) {
|
||||
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
|
||||
fragment.handleAddBottomInsets();
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,11 +1121,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private void addAttachmentContactInfo(int contactId) {
|
||||
@@ -1110,7 +1177,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel.clearSubject();
|
||||
}
|
||||
|
||||
// Stop draft audio playback regardless, since it is unlikely
|
||||
// we will need background playback for drafts
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
final int currentChatId = dcChat.getId();
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcMsg msg = null;
|
||||
int recompress = 0;
|
||||
@@ -1120,9 +1192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (action == ACTION_SEND_OUT) {
|
||||
dcContext.sendEditRequest(msgId, body);
|
||||
} else {
|
||||
dcContext.setDraft(chatId, null);
|
||||
dcContext.setDraft(currentChatId, null);
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1133,7 +1205,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
if (slideDeck.getWebxdctDraftId() != 0) {
|
||||
msg = dcContext.getDraft(chatId);
|
||||
msg = dcContext.getDraft(currentChatId);
|
||||
} else {
|
||||
List<Attachment> attachments = slideDeck.asAttachments();
|
||||
for (Attachment attachment : attachments) {
|
||||
@@ -1182,7 +1254,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
// for WEBXDC, drafts are just sent out as is.
|
||||
// for preparations and other cases, cleanup draft soon.
|
||||
if (msg == null || msg.getType() != DcMsg.DC_MSG_WEBXDC) {
|
||||
dcContext.setDraft(dcChat.getId(), null);
|
||||
dcContext.setDraft(currentChatId, null);
|
||||
}
|
||||
|
||||
if(msg!=null) {
|
||||
@@ -1198,7 +1270,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
false
|
||||
);
|
||||
});
|
||||
doSend = VideoRecoder.prepareVideo(ConversationActivity.this, dcChat.getId(), msg);
|
||||
doSend = VideoRecoder.prepareVideo(ConversationActivity.this, currentChatId, msg);
|
||||
Util.runOnMain(() -> {
|
||||
try {
|
||||
if (progressDialog != null) progressDialog.dismiss();
|
||||
@@ -1209,48 +1281,34 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
if (doSend) {
|
||||
if (dcContext.sendMsg(dcChat.getId(), msg) == 0) {
|
||||
if (dcContext.sendMsg(currentChatId, msg) == 0) {
|
||||
String lastError = dcContext.getLastError();
|
||||
if (!"".equals(lastError)) {
|
||||
Util.runOnMain(() -> Toast.makeText(ConversationActivity.this, lastError, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Util.runOnMain(() -> sendComplete(dcChat.getId()));
|
||||
Util.runOnMain(() -> sendComplete());
|
||||
}
|
||||
} else {
|
||||
dcContext.setDraft(dcChat.getId(), msg);
|
||||
dcContext.setDraft(currentChatId, msg);
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
|
||||
protected void sendComplete(int chatId) {
|
||||
boolean refreshFragment = (chatId != this.chatId);
|
||||
this.chatId = chatId;
|
||||
|
||||
protected void sendComplete() {
|
||||
if (fragment == null || !fragment.isVisible() || isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.setLastSeen(-1);
|
||||
|
||||
if (refreshFragment) {
|
||||
fragment.reload(recipient, chatId);
|
||||
try {
|
||||
int accId = rpc.getSelectedAccountId();
|
||||
DcHelper.getNotificationCenter(this).updateVisibleChat(accId, chatId);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "rpc.getSelectedAccountId() failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.scrollToBottom();
|
||||
attachmentManager.cleanup();
|
||||
}
|
||||
@@ -1389,6 +1447,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
isEditing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerPicked(Uri stickerUri) {
|
||||
if (isEditing) return;
|
||||
sendSticker(stickerUri, "image/webp");
|
||||
}
|
||||
|
||||
public void refreshStickerPicker() {
|
||||
if (emojiPicker != null) {
|
||||
emojiPicker.refreshStickerPicker();
|
||||
}
|
||||
}
|
||||
|
||||
// media selected by the system keyboard
|
||||
@Override
|
||||
public void onMediaSelected(@NonNull Uri uri, String contentType) {
|
||||
@@ -1422,6 +1492,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Listeners
|
||||
|
||||
@Override
|
||||
public void onPlayPauseButtonClicked(View view) {
|
||||
addActivityContext(
|
||||
this.getIntent().getExtras(),
|
||||
this.getClass().getName()
|
||||
);
|
||||
}
|
||||
|
||||
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
|
||||
@Override
|
||||
public void onClick(int type) {
|
||||
@@ -1590,7 +1668,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dcChat = dcContext.getChat(chatId);
|
||||
titleView.setTitle(glideRequests, dcChat);
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(false);
|
||||
initializeContactRequest();
|
||||
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|
||||
|| eventId == DcContext.DC_EVENT_MSG_READ)
|
||||
|
||||
@@ -23,17 +23,23 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -43,60 +49,52 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A DC adapter for a conversation thread. Ultimately
|
||||
* used by ConversationActivity to display a conversation
|
||||
* thread in a ListActivity.
|
||||
* A DC adapter for a conversation thread. Ultimately used by ConversationActivity to display a
|
||||
* conversation thread in a ListActivity.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
// FIXME: this breaks type checks, that is why there are so many casts.
|
||||
public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
extends RecyclerView.Adapter
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
||||
{
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder> {
|
||||
|
||||
private static final int MAX_CACHE_SIZE = 40;
|
||||
private final Map<Integer,SoftReference<DcMsg>> recordCache =
|
||||
Collections.synchronizedMap(new LRUCache<Integer,SoftReference<DcMsg>>(MAX_CACHE_SIZE));
|
||||
private final Map<Integer, SoftReference<DcMsg>> recordCache =
|
||||
Collections.synchronizedMap(new LRUCache<Integer, SoftReference<DcMsg>>(MAX_CACHE_SIZE));
|
||||
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 0;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 1;
|
||||
private static final int MESSAGE_TYPE_INFO = 2;
|
||||
private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3;
|
||||
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 0;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 1;
|
||||
private static final int MESSAGE_TYPE_INFO = 2;
|
||||
private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3;
|
||||
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
|
||||
private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5;
|
||||
private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
|
||||
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
|
||||
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
|
||||
private static final int MESSAGE_TYPE_STICKER_INCOMING = 9;
|
||||
private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10;
|
||||
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
|
||||
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
|
||||
private static final int MESSAGE_TYPE_STICKER_INCOMING = 9;
|
||||
private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10;
|
||||
|
||||
private final Set<DcMsg> batchSelected = Collections.synchronizedSet(new HashSet<DcMsg>());
|
||||
|
||||
private final @Nullable ItemClickListener clickListener;
|
||||
private final @NonNull GlideRequests glideRequests;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @NonNull LayoutInflater inflater;
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull Calendar calendar;
|
||||
private final @NonNull GlideRequests glideRequests;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @NonNull LayoutInflater inflater;
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull Calendar calendar;
|
||||
|
||||
private final DcContext dcContext;
|
||||
private @NonNull DcChat dcChat;
|
||||
private @NonNull int[] dcMsgList = new int[0];
|
||||
private int positionToPulseHighlight = -1;
|
||||
private int positionCurrentlyPulseHighlighting = -1;
|
||||
private long pulseHighlightingSince = -1;
|
||||
private int lastSeenPosition = -1;
|
||||
private long lastSeen = -1;
|
||||
private final DcContext dcContext;
|
||||
private @NonNull DcChat dcChat;
|
||||
private @NonNull int[] dcMsgList = new int[0];
|
||||
private int positionToPulseHighlight = -1;
|
||||
private int positionCurrentlyPulseHighlighting = -1;
|
||||
private long pulseHighlightingSince = -1;
|
||||
private int lastSeenPosition = -1;
|
||||
private long lastSeen = -1;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
private AudioView.OnActionListener audioPlayPauseListener;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
||||
@@ -105,7 +103,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <V extends View & BindableConversationItem> V getView() {
|
||||
return (V)itemView;
|
||||
return (V) itemView;
|
||||
}
|
||||
|
||||
public BindableConversationItem getItem() {
|
||||
@@ -113,16 +111,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isActive() {
|
||||
return dcMsgList.length > 0;
|
||||
}
|
||||
|
||||
public @NonNull DcChat getChat(){
|
||||
public @NonNull DcChat getChat() {
|
||||
return dcChat;
|
||||
}
|
||||
|
||||
|
||||
public void setLastSeen(long timestamp) {
|
||||
lastSeen = timestamp;
|
||||
}
|
||||
@@ -146,14 +142,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (position<0 || position>=dcMsgList.length) {
|
||||
if (position < 0 || position >= dcMsgList.length) {
|
||||
return 0;
|
||||
}
|
||||
return dcMsgList[dcMsgList.length-1-position];
|
||||
return dcMsgList[dcMsgList.length - 1 - position];
|
||||
}
|
||||
|
||||
public @NonNull DcMsg getMsg(int position) {
|
||||
if(position<0 || position>=dcMsgList.length) {
|
||||
if (position < 0 || position >= dcMsgList.length) {
|
||||
return new DcMsg(0);
|
||||
}
|
||||
|
||||
@@ -165,17 +161,23 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
final DcMsg fromDb = dcContext.getMsg((int)getItemId(position));
|
||||
final DcMsg fromDb = dcContext.getMsg((int) getItemId(position));
|
||||
recordCache.put(position, new SoftReference<>(fromDb));
|
||||
return fromDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the message with msgId in the chat list, counted from the top
|
||||
*/
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
|
||||
this.audioPlayPauseListener = audioPlayPauseListener;
|
||||
}
|
||||
|
||||
/** Returns the position of the message with msgId in the chat list, counted from the top */
|
||||
public int msgIdToPosition(int msgId) {
|
||||
for(int i=0; i<dcMsgList.length; i++ ) {
|
||||
if(dcMsgList[i]==msgId) {
|
||||
for (int i = 0; i < dcMsgList.length; i++) {
|
||||
if (dcMsgList[i] == msgId) {
|
||||
return dcMsgList.length - 1 - i;
|
||||
}
|
||||
}
|
||||
@@ -200,17 +202,18 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(DcMsg item);
|
||||
|
||||
void onItemLongClick(DcMsg item, View view);
|
||||
}
|
||||
|
||||
public ConversationAdapter(@NonNull Context context,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient) {
|
||||
public ConversationAdapter(
|
||||
@NonNull Context context,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient) {
|
||||
this.dcChat = dcChat;
|
||||
this.glideRequests = glideRequests;
|
||||
this.clickListener = clickListener;
|
||||
@@ -218,14 +221,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
this.context = context;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.calendar = Calendar.getInstance();
|
||||
this.dcContext = DcHelper.getContext(context);
|
||||
this.dcContext = DcHelper.getContext(context);
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder)viewHolder;
|
||||
ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder) viewHolder;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (position == positionToPulseHighlight) {
|
||||
@@ -235,14 +238,25 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
long elapsed = now - pulseHighlightingSince;
|
||||
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
|
||||
boolean pulseHighlight =
|
||||
(positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
|
||||
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
|
||||
holder
|
||||
.getItem()
|
||||
.bind(
|
||||
getMsg(position),
|
||||
dcChat,
|
||||
glideRequests,
|
||||
batchSelected,
|
||||
recipient,
|
||||
pulseHighlight,
|
||||
playbackViewModel,
|
||||
audioPlayPauseListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder.itemView instanceof ConversationItem) {
|
||||
if (viewHolder.itemView instanceof ConversationItem) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
||||
}
|
||||
}
|
||||
@@ -250,17 +264,19 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemClick(itemView.getMessageRecord());
|
||||
}
|
||||
});
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView.getMessageRecord(), view);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
itemView.setOnClickListener(
|
||||
view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemClick(itemView.getMessageRecord());
|
||||
}
|
||||
});
|
||||
itemView.setOnLongClickListener(
|
||||
view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView.getMessageRecord(), view);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
itemView.setEventListener(clickListener);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
@@ -271,14 +287,19 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
|
||||
case MESSAGE_TYPE_DOCUMENT_OUTGOING:
|
||||
case MESSAGE_TYPE_STICKER_OUTGOING:
|
||||
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
|
||||
case MESSAGE_TYPE_OUTGOING:
|
||||
return R.layout.conversation_item_sent;
|
||||
case MESSAGE_TYPE_AUDIO_INCOMING:
|
||||
case MESSAGE_TYPE_THUMBNAIL_INCOMING:
|
||||
case MESSAGE_TYPE_DOCUMENT_INCOMING:
|
||||
case MESSAGE_TYPE_STICKER_INCOMING:
|
||||
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
|
||||
case MESSAGE_TYPE_INFO: return R.layout.conversation_item_update;
|
||||
default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
|
||||
case MESSAGE_TYPE_INCOMING:
|
||||
return R.layout.conversation_item_received;
|
||||
case MESSAGE_TYPE_INFO:
|
||||
return R.layout.conversation_item_update;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"unsupported item view type given to ConversationAdapter");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,21 +309,18 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
int type = dcMsg.getType();
|
||||
if (dcMsg.isInfo()) {
|
||||
return MESSAGE_TYPE_INFO;
|
||||
}
|
||||
else if (type==DcMsg.DC_MSG_AUDIO || type==DcMsg.DC_MSG_VOICE) {
|
||||
return dcMsg.isOutgoing()? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING;
|
||||
}
|
||||
else if (type==DcMsg.DC_MSG_FILE) {
|
||||
return dcMsg.isOutgoing()? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING;
|
||||
}
|
||||
else if (type==DcMsg.DC_MSG_IMAGE || type==DcMsg.DC_MSG_GIF || type==DcMsg.DC_MSG_VIDEO) {
|
||||
return dcMsg.isOutgoing()? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING;
|
||||
}
|
||||
else if (type == DcMsg.DC_MSG_STICKER) {
|
||||
return dcMsg.isOutgoing()? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING;
|
||||
}
|
||||
else {
|
||||
return dcMsg.isOutgoing()? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING;
|
||||
} else if (type == DcMsg.DC_MSG_AUDIO || type == DcMsg.DC_MSG_VOICE) {
|
||||
return dcMsg.isOutgoing() ? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING;
|
||||
} else if (type == DcMsg.DC_MSG_FILE) {
|
||||
return dcMsg.isOutgoing() ? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING;
|
||||
} else if (type == DcMsg.DC_MSG_IMAGE
|
||||
|| type == DcMsg.DC_MSG_GIF
|
||||
|| type == DcMsg.DC_MSG_VIDEO) {
|
||||
return dcMsg.isOutgoing() ? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING;
|
||||
} else if (type == DcMsg.DC_MSG_STICKER) {
|
||||
return dcMsg.isOutgoing() ? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING;
|
||||
} else {
|
||||
return dcMsg.isOutgoing() ? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,16 +343,16 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
public void pulseHighlightItem(int position) {
|
||||
if (position>=0 && position < getItemCount()) {
|
||||
if (position >= 0 && position < getItemCount()) {
|
||||
positionToPulseHighlight = position;
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
public long getSortTimestamp(int position) {
|
||||
if (!isActive()) return 0;
|
||||
if (!isActive()) return 0;
|
||||
if (position >= getItemCount()) return 0;
|
||||
if (position < 0) return 0;
|
||||
if (position < 0) return 0;
|
||||
|
||||
DcMsg msg = getMsg(position);
|
||||
return msg.getSortTimestamp();
|
||||
@@ -348,7 +366,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (position >= getItemCount()) return -1;
|
||||
if (position < 0) return -1;
|
||||
if (position < 0) return -1;
|
||||
|
||||
calendar.setTime(new Date(getSortTimestamp(position)));
|
||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
||||
@@ -356,18 +374,17 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
|
||||
return new HeaderViewHolder(
|
||||
LayoutInflater.from(getContext())
|
||||
.inflate(R.layout.conversation_item_header, parent, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* date header view
|
||||
*/
|
||||
/** date header view */
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
||||
viewHolder.setText(DateUtils.getRelativeDate(getContext(), getSortTimestamp(position)));
|
||||
}
|
||||
|
||||
|
||||
public void changeData(@Nullable int[] dcMsgList) {
|
||||
// should be called when there are new messages
|
||||
this.dcMsgList = dcMsgList == null ? new int[0] : dcMsgList;
|
||||
@@ -387,12 +404,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
private int findLastSeenPosition(long lastSeen) {
|
||||
if (lastSeen <= 0) return -1;
|
||||
if (!isActive()) return -1;
|
||||
if (lastSeen <= 0) return -1;
|
||||
if (!isActive()) return -1;
|
||||
|
||||
int count = getItemCount();
|
||||
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
DcMsg msg = getMsg(i);
|
||||
if (msg.isOutgoing() || msg.getTimestamp() <= lastSeen) {
|
||||
@@ -404,11 +420,16 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
|
||||
public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
|
||||
return new HeaderViewHolder(
|
||||
LayoutInflater.from(getContext())
|
||||
.inflate(R.layout.conversation_item_last_seen, parent, false));
|
||||
}
|
||||
|
||||
public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) {
|
||||
viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1)));
|
||||
viewHolder.setText(
|
||||
getContext()
|
||||
.getResources()
|
||||
.getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1)));
|
||||
}
|
||||
|
||||
static class LastSeenHeader extends StickyHeaderDecoration {
|
||||
@@ -416,32 +437,45 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
|
||||
LastSeenHeader(ConversationAdapter adapter) {
|
||||
super(adapter, false, false);
|
||||
this.adapter = adapter;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
return adapter.isActive() && position == adapter.getLastSeenPosition();
|
||||
protected boolean hasHeader(
|
||||
RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
return adapter.isActive() && position == adapter.getLastSeenPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
|
||||
protected int getHeaderTop(
|
||||
RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
|
||||
return parent.getLayoutManager().getDecoratedTop(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
protected HeaderViewHolder getHeader(
|
||||
RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent);
|
||||
adapter.onBindLastSeenViewHolder(viewHolder, position);
|
||||
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
|
||||
int heightSpec =
|
||||
View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
|
||||
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
|
||||
int childWidth =
|
||||
ViewGroup.getChildMeasureSpec(
|
||||
widthSpec,
|
||||
parent.getPaddingLeft() + parent.getPaddingRight(),
|
||||
viewHolder.itemView.getLayoutParams().width);
|
||||
int childHeight =
|
||||
ViewGroup.getChildMeasureSpec(
|
||||
heightSpec,
|
||||
parent.getPaddingTop() + parent.getPaddingBottom(),
|
||||
viewHolder.itemView.getLayoutParams().height);
|
||||
|
||||
viewHolder.itemView.measure(childWidth, childHeight);
|
||||
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
|
||||
viewHolder.itemView.layout(
|
||||
0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
|
||||
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,7 @@ import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.CallItemView;
|
||||
@@ -52,6 +51,8 @@ import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -71,7 +72,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -181,9 +181,11 @@ public class ConversationItem extends BaseConversationItem
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight)
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
this.glideRequests = glideRequests;
|
||||
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
|
||||
|
||||
@@ -204,7 +206,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
setGutterSizes(messageRecord, showSender);
|
||||
setMessageShape(messageRecord);
|
||||
setMediaAttributes(messageRecord, showSender);
|
||||
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
|
||||
setBodyText(messageRecord);
|
||||
setBubbleState(messageRecord);
|
||||
setContactPhoto();
|
||||
@@ -482,24 +484,10 @@ public class ConversationItem extends BaseConversationItem
|
||||
}
|
||||
|
||||
private void setMediaAttributes(@NonNull DcMsg messageRecord,
|
||||
boolean showSender)
|
||||
boolean showSender,
|
||||
AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
class SetDurationListener implements AudioSlidePlayer.Listener {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onStop() {}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
messageRecord.lateFilingMediaSize(0,0, millis);
|
||||
audioViewStub.get().setDuration(millis);
|
||||
}
|
||||
}
|
||||
if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
@@ -509,15 +497,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
|
||||
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
int duration = messageRecord.getDuration();
|
||||
if (duration == 0) {
|
||||
AudioSlide audio = new AudioSlide(context, messageRecord);
|
||||
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
|
||||
audioSlidePlayer.requestDuration();
|
||||
}
|
||||
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
|
||||
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
|
||||
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord));
|
||||
audioViewStub.get().setOnClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
@@ -825,8 +807,6 @@ public class ConversationItem extends BaseConversationItem
|
||||
return stickerStub.get().getFooter();
|
||||
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getText())) {
|
||||
return mediaThumbnailStub.get().getFooter();
|
||||
} else if (messageRecord.getType() == DcMsg.DC_MSG_CALL) {
|
||||
return callViewStub.get().getFooter();
|
||||
} else {
|
||||
return footer;
|
||||
}
|
||||
@@ -987,6 +967,8 @@ public class ConversationItem extends BaseConversationItem
|
||||
public void onClick(final View v, final Slide slide) {
|
||||
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
|
||||
performClick();
|
||||
} else if (eventListener != null) {
|
||||
eventListener.onStickerClicked(messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1000,9 +982,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
int chatId = messageRecord.getChatId();
|
||||
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
|
||||
int callId = messageRecord.getId();
|
||||
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
|
||||
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer, callInfo.hasVideo);
|
||||
} else {
|
||||
CallUtil.startCall(getContext(), accId, chatId);
|
||||
CallUtil.startCall(getContext(), accId, chatId, callInfo.hasVideo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,40 +6,37 @@ import android.graphics.Canvas;
|
||||
import android.os.Vibrator;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.util.AccessibilityUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private static final float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX;
|
||||
private static final long SWIPE_SUCCESS_VIBE_TIME_MS = 10;
|
||||
private static final float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX;
|
||||
private static final long SWIPE_SUCCESS_VIBE_TIME_MS = 10;
|
||||
|
||||
private boolean swipeBack;
|
||||
private boolean shouldTriggerSwipeFeedback;
|
||||
private boolean canTriggerSwipe;
|
||||
private float latestDownX;
|
||||
private float latestDownY;
|
||||
private float latestDownX;
|
||||
private float latestDownY;
|
||||
|
||||
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
||||
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
||||
private final ConversationItemTouchListener itemTouchListener;
|
||||
private final OnSwipeListener onSwipeListener;
|
||||
private final OnSwipeListener onSwipeListener;
|
||||
|
||||
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
||||
@NonNull OnSwipeListener onSwipeListener)
|
||||
{
|
||||
ConversationItemSwipeCallback(
|
||||
@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
||||
@NonNull OnSwipeListener onSwipeListener) {
|
||||
super(0, ItemTouchHelper.END);
|
||||
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
||||
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
||||
this.onSwipeListener = onSwipeListener;
|
||||
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
||||
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
||||
this.onSwipeListener = onSwipeListener;
|
||||
this.shouldTriggerSwipeFeedback = true;
|
||||
this.canTriggerSwipe = true;
|
||||
this.canTriggerSwipe = true;
|
||||
}
|
||||
|
||||
void attachToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
@@ -48,21 +45,19 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target)
|
||||
{
|
||||
public boolean onMove(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
}
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {}
|
||||
|
||||
@Override
|
||||
public int getSwipeDirs(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder)
|
||||
{
|
||||
public int getSwipeDirs(
|
||||
@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (cannotSwipeViewHolder(viewHolder)) return 0;
|
||||
return super.getSwipeDirs(recyclerView, viewHolder);
|
||||
}
|
||||
@@ -78,18 +73,21 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
@Override
|
||||
public void onChildDraw(
|
||||
@NonNull Canvas c,
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx, float dy, int actionState, boolean isCurrentlyActive)
|
||||
{
|
||||
@NonNull Canvas c,
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx,
|
||||
float dy,
|
||||
int actionState,
|
||||
boolean isCurrentlyActive) {
|
||||
if (cannotSwipeViewHolder(viewHolder)) return;
|
||||
|
||||
float sign = getSignFromDirection(viewHolder.itemView);
|
||||
float sign = getSignFromDirection(viewHolder.itemView);
|
||||
boolean isCorrectSwipeDir = sameSign(dx, sign);
|
||||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
||||
ConversationSwipeAnimationHelper.update(
|
||||
(ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
||||
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
|
||||
if (canTriggerSwipe) {
|
||||
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
|
||||
@@ -100,7 +98,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
if (dx == 0) {
|
||||
shouldTriggerSwipeFeedback = true;
|
||||
canTriggerSwipe = true;
|
||||
canTriggerSwipe = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,50 +120,50 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void setTouchListener(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx)
|
||||
{
|
||||
recyclerView.setOnTouchListener(new View.OnTouchListener() {
|
||||
private void setTouchListener(
|
||||
@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dx) {
|
||||
recyclerView.setOnTouchListener(
|
||||
new View.OnTouchListener() {
|
||||
|
||||
// This variable is necessary to make sure that the handleTouchActionUp() and therefore onSwiped() is called only once.
|
||||
// Otherwise, any subsequent little swipe would invoke onSwiped().
|
||||
// We can't call recyclerView.setOnTouchListener(null) because another ConversationItem might have set its own
|
||||
// on touch listener in the meantime and we don't want to cancel it
|
||||
private boolean listenerCalled = false;
|
||||
// This variable is necessary to make sure that the handleTouchActionUp() and therefore
|
||||
// onSwiped() is called only once.
|
||||
// Otherwise, any subsequent little swipe would invoke onSwiped().
|
||||
// We can't call recyclerView.setOnTouchListener(null) because another ConversationItem
|
||||
// might have set its own
|
||||
// on touch listener in the meantime and we don't want to cancel it
|
||||
private boolean listenerCalled = false;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
shouldTriggerSwipeFeedback = true;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!listenerCalled) {
|
||||
listenerCalled = true;
|
||||
ConversationItemSwipeCallback.this.handleTouchActionUp(recyclerView, viewHolder, dx);
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
shouldTriggerSwipeFeedback = true;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!listenerCalled) {
|
||||
listenerCalled = true;
|
||||
ConversationItemSwipeCallback.this.handleTouchActionUp(
|
||||
recyclerView, viewHolder, dx);
|
||||
}
|
||||
// fallthrough
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
swipeBack = true;
|
||||
shouldTriggerSwipeFeedback = false;
|
||||
// Sometimes the view does not go back correctly, so make sure that after 2s the
|
||||
// progress is reset:
|
||||
viewHolder.itemView.postDelayed(() -> resetProgress(viewHolder), 2000);
|
||||
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
|
||||
resetProgress(viewHolder);
|
||||
}
|
||||
break;
|
||||
}
|
||||
//fallthrough
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
swipeBack = true;
|
||||
shouldTriggerSwipeFeedback = false;
|
||||
// Sometimes the view does not go back correctly, so make sure that after 2s the progress is reset:
|
||||
viewHolder.itemView.postDelayed(() -> resetProgress(viewHolder), 2000);
|
||||
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
|
||||
resetProgress(viewHolder);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleTouchActionUp(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx)
|
||||
{
|
||||
private void handleTouchActionUp(
|
||||
@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dx) {
|
||||
if (dx > SWIPE_SUCCESS_DX) {
|
||||
canTriggerSwipe = false;
|
||||
onSwiped(viewHolder);
|
||||
@@ -177,17 +175,16 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
private static void resetProgress(RecyclerView.ViewHolder viewHolder) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
|
||||
0f,
|
||||
getSignFromDirection(viewHolder.itemView));
|
||||
ConversationSwipeAnimationHelper.update(
|
||||
(ConversationItem) viewHolder.itemView, 0f, getSignFromDirection(viewHolder.itemView));
|
||||
}
|
||||
|
||||
private boolean cannotSwipeViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (!(viewHolder.itemView instanceof ConversationItem)) return true;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) ||
|
||||
item.disallowSwipe(latestDownX, latestDownY);
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord())
|
||||
|| item.disallowSwipe(latestDownX, latestDownY);
|
||||
}
|
||||
|
||||
private void updateLatestDownCoordinate(float x, float y) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_URL;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageContent;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.getDirectSharingChatId;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.getSharedTitle;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.isDirectSharing;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.isForwarding;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.resetRelayingMessageContent;
|
||||
|
||||
@@ -45,14 +45,16 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.view.MenuCompat;
|
||||
|
||||
import chat.delta.rpc.types.SecurejoinSource;
|
||||
import chat.delta.rpc.types.SecurejoinUiPath;
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcContact;
|
||||
@@ -60,7 +62,8 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import org.thoughtcrime.securesms.components.AvatarView;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
@@ -77,45 +80,51 @@ import org.thoughtcrime.securesms.search.SearchFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ScreenLockUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.ScreenLockUtil;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import chat.delta.rpc.types.SecurejoinSource;
|
||||
import chat.delta.rpc.types.SecurejoinUiPath;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ConversationListFragment.ConversationSelectedListener
|
||||
{
|
||||
implements ConversationListFragment.ConversationSelectedListener {
|
||||
private static final String TAG = ConversationListActivity.class.getSimpleName();
|
||||
private static final String OPENPGP4FPR = "openpgp4fpr";
|
||||
private static final String NDK_ARCH_WARNED = "ndk_arch_warned";
|
||||
public static final String CLEAR_NOTIFICATIONS = "clear_notifications";
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
public static final String FROM_WELCOME = "from_welcome";
|
||||
private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1;
|
||||
public static final String FROM_WELCOME = "from_welcome";
|
||||
public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr";
|
||||
|
||||
private ConversationListFragment conversationListFragment;
|
||||
public TextView title;
|
||||
private AvatarView selfAvatar;
|
||||
private ImageView unreadIndicator;
|
||||
private SearchFragment searchFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private ViewGroup fragmentContainer;
|
||||
private ViewGroup selfAvatarContainer;
|
||||
public TextView title;
|
||||
private AvatarView selfAvatar;
|
||||
private ImageView unreadIndicator;
|
||||
private SearchFragment searchFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private ViewGroup fragmentContainer;
|
||||
private ViewGroup selfAvatarContainer;
|
||||
|
||||
/** used to store temporarily scanned QR to pass it back to QrCodeHandler when ScreenLockUtil is used */
|
||||
/**
|
||||
* used to store temporarily scanned QR to pass it back to QrCodeHandler when ScreenLockUtil is
|
||||
* used
|
||||
*/
|
||||
private String qrData = null;
|
||||
/** used to store temporarily profile ID to delete after authorization is granted via ScreenLockUtil */
|
||||
|
||||
private ActivityResultLauncher<Intent> relayLockLauncher;
|
||||
private ActivityResultLauncher<Intent> qrScannerLauncher;
|
||||
|
||||
/**
|
||||
* used to store temporarily profile ID to delete after authorization is granted via
|
||||
* ScreenLockUtil
|
||||
*/
|
||||
private int deleteProfileId = 0;
|
||||
|
||||
private ActivityResultLauncher<Intent> deleteProfileLockLauncher;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme = new DynamicNoActionBarTheme();
|
||||
@@ -124,6 +133,44 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
relayLockLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
// QrCodeHandler requested user authorization before adding a relay
|
||||
// and it was granted, so proceed to add the relay
|
||||
if (qrData != null) {
|
||||
new QrCodeHandler(this).addRelay(qrData);
|
||||
qrData = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
deleteProfileLockLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
if (deleteProfileId != 0) {
|
||||
deleteProfile(deleteProfileId);
|
||||
deleteProfileId = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
qrScannerLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
IntentResult scanResult =
|
||||
IntentIntegrator.parseActivityResult(result.getResultCode(), result.getData());
|
||||
qrData = scanResult.getContents();
|
||||
new QrCodeHandler(this)
|
||||
.handleQrData(
|
||||
qrData, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher);
|
||||
}
|
||||
});
|
||||
|
||||
addDeviceMessages(getIntent().getBooleanExtra(FROM_WELCOME, false));
|
||||
if (getIntent().getIntExtra(ACCOUNT_ID_EXTRA, -1) <= 0) {
|
||||
getIntent().putExtra(ACCOUNT_ID_EXTRA, DcHelper.getContext(this).getAccountId());
|
||||
@@ -134,72 +181,88 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
selfAvatar = findViewById(R.id.self_avatar);
|
||||
selfAvatarContainer = findViewById(R.id.self_avatar_container);
|
||||
unreadIndicator = findViewById(R.id.unread_indicator);
|
||||
title = findViewById(R.id.toolbar_title);
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
fragmentContainer = findViewById(R.id.fragment_container);
|
||||
selfAvatar = findViewById(R.id.self_avatar);
|
||||
selfAvatarContainer = findViewById(R.id.self_avatar_container);
|
||||
unreadIndicator = findViewById(R.id.unread_indicator);
|
||||
title = findViewById(R.id.toolbar_title);
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
fragmentContainer = findViewById(R.id.fragment_container);
|
||||
|
||||
// add margin to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsetsAsMargin(searchToolbar, true, true, true, false);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
conversationListFragment = initFragment(R.id.fragment_container, new ConversationListFragment(), bundle);
|
||||
conversationListFragment =
|
||||
initFragment(R.id.fragment_container, new ConversationListFragment(), bundle);
|
||||
|
||||
initializeSearchListener();
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (searchToolbar.isVisible()) {
|
||||
searchToolbar.collapse();
|
||||
} else {
|
||||
Activity activity = ConversationListActivity.this;
|
||||
if (isRelayingMessageContent(activity)) {
|
||||
int selectedAccId = DcHelper.getContext(activity).getAccountId();
|
||||
int initialAccId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId);
|
||||
if (initialAccId != selectedAccId) {
|
||||
// allowing to go back is dangerous, it could be activity on previously selected account,
|
||||
// instead of figuring out account rollback in onResume in each activity (conversation, gallery, media preview, webxdc, etc.)
|
||||
// just clear the back stack and stay in newly selected account
|
||||
finishAffinity();
|
||||
startActivity(new Intent(activity, ConversationListActivity.class));
|
||||
return;
|
||||
} else {
|
||||
handleResetRelaying();
|
||||
}
|
||||
}
|
||||
getOnBackPressedDispatcher()
|
||||
.addCallback(
|
||||
this,
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (searchToolbar.isVisible()) {
|
||||
searchToolbar.collapse();
|
||||
} else {
|
||||
Activity activity = ConversationListActivity.this;
|
||||
if (isRelayingMessageContent(activity)) {
|
||||
int selectedAccId = DcHelper.getContext(activity).getAccountId();
|
||||
int initialAccId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId);
|
||||
if (initialAccId != selectedAccId) {
|
||||
// allowing to go back is dangerous, it could be activity on previously
|
||||
// selected account,
|
||||
// instead of figuring out account rollback in onResume in each activity
|
||||
// (conversation, gallery, media preview, webxdc, etc.)
|
||||
// just clear the back stack and stay in newly selected account
|
||||
finishAffinity();
|
||||
startActivity(new Intent(activity, ConversationListActivity.class));
|
||||
return;
|
||||
} else {
|
||||
handleResetRelaying();
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TooltipCompat.setTooltipText(searchAction, getText(R.string.search_explain));
|
||||
|
||||
TooltipCompat.setTooltipText(selfAvatar, getText(R.string.switch_account));
|
||||
selfAvatar.setOnClickListener(v -> AccountManager.getInstance().showSwitchAccountMenu(this, false));
|
||||
findViewById(R.id.avatar_and_title).setOnClickListener(v -> {
|
||||
if (!isRelayingMessageContent(this)) {
|
||||
AccountManager.getInstance().showSwitchAccountMenu(this, false);
|
||||
}
|
||||
});
|
||||
selfAvatar.setOnClickListener(
|
||||
v -> AccountManager.getInstance().showSwitchAccountMenu(this, false));
|
||||
findViewById(R.id.avatar_and_title)
|
||||
.setOnClickListener(
|
||||
v -> {
|
||||
if (!isRelayingMessageContent(this)) {
|
||||
AccountManager.getInstance().showSwitchAccountMenu(this, false);
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (BuildConfig.DEBUG) checkNdkArchitecture();
|
||||
|
||||
DcHelper.maybeShowMigrationError(this);
|
||||
|
||||
String rawQrString = getIntent().getStringExtra(FROM_WELCOME_RAW_QR);
|
||||
// Launch chat directly, if coming from onboarding with a join chat/group QR
|
||||
if (rawQrString != null) {
|
||||
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
|
||||
qrCodeHandler.secureJoinByQr(rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the build script is invoked with a specific architecture (e.g.`ndk-make.sh arm64-v8a`), it
|
||||
* will compile the core only for this arch. This method checks if the arch was correct.
|
||||
*
|
||||
* In order to do this, `ndk-make.sh` writes its argument into the file `ndkArch`.
|
||||
* <p>In order to do this, `ndk-make.sh` writes its argument into the file `ndkArch`.
|
||||
* `getNdkArch()` in `build.gradle` then reads this file and its content is assigned to
|
||||
* `BuildConfig.NDK_ARCH`.
|
||||
*/
|
||||
@@ -212,7 +275,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
String arch;
|
||||
|
||||
// armv8l is 32 bit mode in 64 bit CPU:
|
||||
if (archProperty.startsWith("armv7") || archProperty.startsWith("armv8l")) arch = "armeabi-v7a";
|
||||
if (archProperty.startsWith("armv7") || archProperty.startsWith("armv8l"))
|
||||
arch = "armeabi-v7a";
|
||||
else if (archProperty.equals("aarch64")) arch = "arm64-v8a";
|
||||
else if (archProperty.equals("i686")) arch = "x86";
|
||||
else if (archProperty.equals("x86_64")) arch = "x86_64";
|
||||
@@ -226,23 +290,35 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
|
||||
String message;
|
||||
if (arch.equals("")) {
|
||||
message = "This phone has the unknown architecture " + archProperty + ".\n\n"+
|
||||
"Please open an issue at https://github.com/deltachat/deltachat-android/issues.";
|
||||
message =
|
||||
"This phone has the unknown architecture "
|
||||
+ archProperty
|
||||
+ ".\n\n"
|
||||
+ "Please open an issue at https://github.com/deltachat/deltachat-android/issues.";
|
||||
} else {
|
||||
message = "Apparently you used `ndk-make.sh " + BuildConfig.NDK_ARCH + "`, but this device is " + arch + ".\n\n" +
|
||||
"You can use the app, but changes you made to the Rust code were not applied.\n\n" +
|
||||
"To compile in your changes, you can:\n" +
|
||||
"- Either run `ndk-make.sh " + arch + "` to build only for " + arch + " in debug mode\n" +
|
||||
"- Or run `ndk-make.sh` without argument to build for all architectures in release mode\n\n" +
|
||||
"If something doesn't work, please open an issue at https://github.com/deltachat/deltachat-android/issues!!";
|
||||
message =
|
||||
"Apparently you used `ndk-make.sh "
|
||||
+ BuildConfig.NDK_ARCH
|
||||
+ "`, but this device is "
|
||||
+ arch
|
||||
+ ".\n\n"
|
||||
+ "You can use the app, but changes you made to the Rust code were not applied.\n\n"
|
||||
+ "To compile in your changes, you can:\n"
|
||||
+ "- Either run `ndk-make.sh "
|
||||
+ arch
|
||||
+ "` to build only for "
|
||||
+ arch
|
||||
+ " in debug mode\n"
|
||||
+ "- Or run `ndk-make.sh` without argument to build for all architectures in release mode\n\n"
|
||||
+ "If something doesn't work, please open an issue at https://github.com/deltachat/deltachat-android/issues!!";
|
||||
}
|
||||
Log.e(TAG, message);
|
||||
|
||||
if (!Prefs.getBooleanPreference(this, NDK_ARCH_WARNED, false)) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
Prefs.setBooleanPreference(this, NDK_ARCH_WARNED, true);
|
||||
}
|
||||
}
|
||||
@@ -303,7 +379,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} else {
|
||||
title.setText(DcHelper.getContext(this).getName());
|
||||
// refreshTitle is called by ConversationListFragment when connectivity changes so update connectivity dot here
|
||||
// refreshTitle is called by ConversationListFragment when connectivity changes so update
|
||||
// connectivity dot here
|
||||
selfAvatar.setConnectivity(DcHelper.getContext(this).getConnectivity());
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
@@ -339,23 +416,26 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
}
|
||||
|
||||
if(unreadCount == 0) {
|
||||
if (unreadCount == 0) {
|
||||
unreadIndicator.setVisibility(View.GONE);
|
||||
} else {
|
||||
boolean isDarkTheme = DynamicTheme.isDarkTheme(this);
|
||||
int badgeColor = Color.WHITE;
|
||||
if (isDarkTheme) {
|
||||
final TypedArray attrs = obtainStyledAttributes(new int[] {
|
||||
R.attr.conversation_list_item_unreadcount_color,
|
||||
});
|
||||
final TypedArray attrs =
|
||||
obtainStyledAttributes(
|
||||
new int[] {
|
||||
R.attr.conversation_list_item_unreadcount_color,
|
||||
});
|
||||
badgeColor = attrs.getColor(0, Color.BLACK);
|
||||
}
|
||||
String label = unreadCount>99? "99+" : String.valueOf(unreadCount);
|
||||
unreadIndicator.setImageDrawable(TextDrawable.builder()
|
||||
String label = unreadCount > 99 ? "99+" : String.valueOf(unreadCount);
|
||||
unreadIndicator.setImageDrawable(
|
||||
TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.width(ViewUtil.dpToPx(this, 20))
|
||||
.height(ViewUtil.dpToPx(this, 20))
|
||||
.textColor(isDarkTheme? Color.WHITE : Color.BLACK)
|
||||
.textColor(isDarkTheme ? Color.WHITE : Color.BLACK)
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRound(label, badgeColor));
|
||||
@@ -378,9 +458,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
|
||||
if (isRelayingMessageContent(this)) {
|
||||
inflater.inflate(R.menu.forwarding_menu, menu);
|
||||
menu.findItem(R.id.menu_export_attachment).setVisible(
|
||||
ShareUtil.isFromWebxdc(this) && ShareUtil.getSharedUris(this).size() == 1
|
||||
);
|
||||
menu.findItem(R.id.menu_export_attachment)
|
||||
.setVisible(ShareUtil.isFromWebxdc(this) && ShareUtil.getSharedUris(this).size() == 1);
|
||||
} else {
|
||||
inflater.inflate(R.menu.text_secure_normal, menu);
|
||||
menu.findItem(R.id.menu_global_map).setVisible(Prefs.isLocationStreamingEnabled(this));
|
||||
@@ -389,7 +468,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
proxyItem.setVisible(false);
|
||||
} else {
|
||||
boolean proxyEnabled = DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1;
|
||||
proxyItem.setIcon(proxyEnabled? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24);
|
||||
proxyItem.setIcon(
|
||||
proxyEnabled ? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24);
|
||||
proxyItem.setVisible(true);
|
||||
}
|
||||
MenuCompat.setGroupDividerEnabled(menu, true);
|
||||
@@ -400,42 +480,42 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
|
||||
private void initializeSearchListener() {
|
||||
searchAction.setOnClickListener(v -> {
|
||||
searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
|
||||
searchAction.getY() + (searchAction.getHeight() / 2));
|
||||
});
|
||||
searchAction.setOnClickListener(
|
||||
v -> {
|
||||
searchToolbar.display(
|
||||
searchAction.getX() + (searchAction.getWidth() / 2),
|
||||
searchAction.getY() + (searchAction.getHeight() / 2));
|
||||
});
|
||||
|
||||
searchToolbar.setListener(new SearchToolbar.SearchListener() {
|
||||
@Override
|
||||
public void onSearchTextChange(String text) {
|
||||
String trimmed = text.trim();
|
||||
searchToolbar.setListener(
|
||||
new SearchToolbar.SearchListener() {
|
||||
@Override
|
||||
public void onSearchTextChange(String text) {
|
||||
String trimmed = text.trim();
|
||||
|
||||
if (trimmed.length() > 0) {
|
||||
if (searchFragment == null) {
|
||||
searchFragment = SearchFragment.newInstance();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, searchFragment, null)
|
||||
.commit();
|
||||
if (trimmed.length() > 0) {
|
||||
if (searchFragment == null) {
|
||||
searchFragment = SearchFragment.newInstance();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(R.id.fragment_container, searchFragment, null)
|
||||
.commit();
|
||||
}
|
||||
searchFragment.updateSearchQuery(trimmed);
|
||||
} else if (searchFragment != null) {
|
||||
getSupportFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
searchFragment = null;
|
||||
}
|
||||
}
|
||||
searchFragment.updateSearchQuery(trimmed);
|
||||
} else if (searchFragment != null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.remove(searchFragment)
|
||||
.commit();
|
||||
searchFragment = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchClosed() {
|
||||
if (searchFragment != null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.remove(searchFragment)
|
||||
.commit();
|
||||
searchFragment = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onSearchClosed() {
|
||||
if (searchFragment != null) {
|
||||
getSupportFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
searchFragment = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -450,7 +530,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
startActivity(new Intent(this, ApplicationPreferencesActivity.class));
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_qr) {
|
||||
new IntentIntegrator(this).setCaptureActivity(QrActivity.class).initiateScan();
|
||||
Intent intent =
|
||||
new IntentIntegrator(this).setCaptureActivity(QrActivity.class).createScanIntent();
|
||||
qrScannerLauncher.launch(intent);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_global_map) {
|
||||
WebxdcActivity.openMaps(this, 0);
|
||||
@@ -475,38 +557,40 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
|
||||
private void handleSaveAttachment() {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore(this)) {
|
||||
performSave();
|
||||
return;
|
||||
}
|
||||
SaveAttachmentTask.showWarningDialog(
|
||||
this,
|
||||
(dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore(this)) {
|
||||
performSave();
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.alwaysGrantOnSdk30()
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||
.onAllGranted(this::performSave)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void performSave() {
|
||||
ArrayList<Uri> uriList = ShareUtil.getSharedUris(this);
|
||||
ArrayList<Uri> uriList = ShareUtil.getSharedUris(this);
|
||||
Uri uri = uriList.get(0);
|
||||
String mimeType = PersistentBlobProvider.getMimeType(this, uri);
|
||||
String fileName = PersistentBlobProvider.getFileName(this, uri);
|
||||
SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[]{
|
||||
new SaveAttachmentTask.Attachment(uri, mimeType, new Date().getTime(), fileName)
|
||||
};
|
||||
SaveAttachmentTask.Attachment[] attachments =
|
||||
new SaveAttachmentTask.Attachment[] {
|
||||
new SaveAttachmentTask.Attachment(uri, mimeType, new Date().getTime(), fileName)
|
||||
};
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(this);
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
|
||||
private void handleOpenpgp4fpr() {
|
||||
if (getIntent() != null &&
|
||||
Intent.ACTION_VIEW.equals(getIntent().getAction())) {
|
||||
if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getAction())) {
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri == null) {
|
||||
return;
|
||||
@@ -539,7 +623,11 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
int fwdAccId = getForwardedMessageAccountId(this);
|
||||
if (fwdAccId == dcContext.getAccountId() && dcContext.getChat(chatId).isSelfTalk()) {
|
||||
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
|
||||
Toast.makeText(this, DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(
|
||||
this,
|
||||
DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
handleResetRelaying();
|
||||
finish();
|
||||
} else {
|
||||
@@ -584,7 +672,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
|
||||
private void addDeviceMessages(boolean fromWelcome) {
|
||||
// update messages - for new messages, do not reuse or modify strings but create new ones.
|
||||
// it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted.
|
||||
// it is not needed to keep all past update messages, however, when deleted, also the strings
|
||||
// should be deleted.
|
||||
try {
|
||||
DcContext dcContext = DcHelper.getContext(this);
|
||||
final String deviceMsgLabel = "update_2_33_1_android";
|
||||
@@ -593,7 +682,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
if (!getIntent().getBooleanExtra(FROM_WELCOME, false)) {
|
||||
msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
|
||||
// InputStream inputStream = getResources().getAssets().open("device-messages/green-checkmark.jpg");
|
||||
// InputStream inputStream =
|
||||
// getResources().getAssets().open("device-messages/green-checkmark.jpg");
|
||||
// String outputFile = DcHelper.getBlobdirFile(dcContext, "green-checkmark", ".jpg");
|
||||
// Util.copy(inputStream, new FileOutputStream(outputFile));
|
||||
// msg.setFile(outputFile, "image/jpeg");
|
||||
@@ -602,7 +692,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
dcContext.addDeviceMsg(deviceMsgLabel, msg);
|
||||
|
||||
if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "").equals(deviceMsgLabel)) {
|
||||
if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "")
|
||||
.equals(deviceMsgLabel)) {
|
||||
int deviceChatId = dcContext.getChatIdByContactId(DcContact.DC_CONTACT_ID_DEVICE);
|
||||
if (deviceChatId != 0) {
|
||||
dcContext.marknoticedChat(deviceChatId);
|
||||
@@ -611,7 +702,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
Prefs.setStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, deviceMsgLabel);
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -621,12 +712,17 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
refreshAvatar();
|
||||
refreshUnreadIndicator();
|
||||
refreshTitle();
|
||||
conversationListFragment.loadChatlist();
|
||||
conversationListFragment.loadChatlistAsync();
|
||||
}
|
||||
|
||||
public void onDeleteProfile(int profileId) {
|
||||
deleteProfileId = profileId;
|
||||
boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.delete_account), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE);
|
||||
boolean result =
|
||||
ScreenLockUtil.applyScreenLock(
|
||||
this,
|
||||
getString(R.string.delete_account),
|
||||
getString(R.string.enter_system_secret_to_continue),
|
||||
deleteProfileLockLauncher);
|
||||
if (!result) {
|
||||
deleteProfile(profileId);
|
||||
}
|
||||
@@ -654,38 +750,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode != RESULT_OK) return;
|
||||
|
||||
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
|
||||
switch (requestCode) {
|
||||
case IntentIntegrator.REQUEST_CODE:
|
||||
IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data);
|
||||
qrData = scanResult.getContents();
|
||||
qrCodeHandler.handleQrData(qrData, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon);
|
||||
break;
|
||||
case ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS:
|
||||
// QrCodeHandler requested user authorization before adding a relay
|
||||
// and it was granted, so proceed to add the relay
|
||||
if (qrData != null) {
|
||||
qrCodeHandler.addRelay(qrData);
|
||||
qrData = null;
|
||||
}
|
||||
break;
|
||||
case REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE:
|
||||
if (deleteProfileId != 0) {
|
||||
deleteProfile(deleteProfileId);
|
||||
deleteProfileId = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,48 +20,44 @@ import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcChatlist;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* A CursorAdapter for building a list of conversation threads.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
class ConversationListAdapter extends BaseConversationListAdapter<ConversationListAdapter.ViewHolder> {
|
||||
class ConversationListAdapter
|
||||
extends BaseConversationListAdapter<ConversationListAdapter.ViewHolder> {
|
||||
|
||||
private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1;
|
||||
private static final int MESSAGE_TYPE_THREAD = 2;
|
||||
private static final int MESSAGE_TYPE_INBOX_ZERO = 3;
|
||||
private static final int MESSAGE_TYPE_THREAD = 2;
|
||||
private static final int MESSAGE_TYPE_INBOX_ZERO = 3;
|
||||
|
||||
private final WeakReference<Context> context;
|
||||
private @NonNull DcContext dcContext;
|
||||
private @NonNull DcChatlist dcChatlist;
|
||||
private final @NonNull GlideRequests glideRequests;
|
||||
private final @NonNull LayoutInflater inflater;
|
||||
private final @Nullable ItemClickListener clickListener;
|
||||
private final WeakReference<Context> context;
|
||||
private @NonNull DcContext dcContext;
|
||||
private @NonNull DcChatlist dcChatlist;
|
||||
private final @NonNull GlideRequests glideRequests;
|
||||
private final @NonNull LayoutInflater inflater;
|
||||
private final @Nullable ItemClickListener clickListener;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
|
||||
{
|
||||
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public BindableConversationListItem getItem() {
|
||||
return (BindableConversationListItem)itemView;
|
||||
return (BindableConversationListItem) itemView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,17 +71,17 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
return dcChatlist.getChatId(i);
|
||||
}
|
||||
|
||||
ConversationListAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable ItemClickListener clickListener)
|
||||
{
|
||||
ConversationListAdapter(
|
||||
@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable ItemClickListener clickListener) {
|
||||
super();
|
||||
this.context = new WeakReference<>(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.dcContext = DcHelper.getContext(context);
|
||||
this.dcChatlist = new DcChatlist(0, 0);
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.clickListener = clickListener;
|
||||
this.context = new WeakReference<>(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.dcContext = DcHelper.getContext(context);
|
||||
this.dcChatlist = new DcChatlist(0, 0);
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.clickListener = clickListener;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@@ -93,29 +89,37 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == MESSAGE_TYPE_SWITCH_ARCHIVE) {
|
||||
final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view, parent, false);
|
||||
final ConversationListItem item =
|
||||
(ConversationListItem)
|
||||
inflater.inflate(R.layout.conversation_list_item_view, parent, false);
|
||||
item.getLayoutParams().height = ViewUtil.dpToPx(54);
|
||||
item.findViewById(R.id.subject).setVisibility(View.GONE);
|
||||
item.findViewById(R.id.date).setVisibility(View.GONE);
|
||||
item.setOnClickListener(v -> {
|
||||
if (clickListener != null) clickListener.onSwitchToArchive();
|
||||
});
|
||||
item.setOnClickListener(
|
||||
v -> {
|
||||
if (clickListener != null) clickListener.onSwitchToArchive();
|
||||
});
|
||||
|
||||
return new ViewHolder(item);
|
||||
} else if (viewType == MESSAGE_TYPE_INBOX_ZERO) {
|
||||
return new ViewHolder((ConversationListItemInboxZero)inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false));
|
||||
return new ViewHolder(
|
||||
(ConversationListItemInboxZero)
|
||||
inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false));
|
||||
} else {
|
||||
final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
|
||||
parent, false);
|
||||
final ConversationListItem item =
|
||||
(ConversationListItem)
|
||||
inflater.inflate(R.layout.conversation_list_item_view, parent, false);
|
||||
|
||||
item.setOnClickListener(view -> {
|
||||
if (clickListener != null) clickListener.onItemClick(item);
|
||||
});
|
||||
item.setOnClickListener(
|
||||
view -> {
|
||||
if (clickListener != null) clickListener.onItemClick(item);
|
||||
});
|
||||
|
||||
item.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) clickListener.onItemLongClick(item);
|
||||
return true;
|
||||
});
|
||||
item.setOnLongClickListener(
|
||||
view -> {
|
||||
if (clickListener != null) clickListener.onItemLongClick(item);
|
||||
return true;
|
||||
});
|
||||
|
||||
return new ViewHolder(item);
|
||||
}
|
||||
@@ -130,7 +134,15 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
|
||||
DcChat chat = dcContext.getChat(dcChatlist.getChatId(i));
|
||||
DcLot summary = dcChatlist.getSummary(i, chat);
|
||||
viewHolder.getItem().bind(DcHelper.getThreadRecord(context, summary, chat), dcChatlist.getMsgId(i), summary, glideRequests, batchSet, batchMode);
|
||||
viewHolder
|
||||
.getItem()
|
||||
.bind(
|
||||
DcHelper.getThreadRecord(context, summary, chat),
|
||||
dcChatlist.getMsgId(i),
|
||||
summary,
|
||||
glideRequests,
|
||||
batchSet,
|
||||
batchMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,7 +151,7 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
|
||||
if (chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK) {
|
||||
return MESSAGE_TYPE_SWITCH_ARCHIVE;
|
||||
} else if(chatId == DcChat.DC_CHAT_ID_ALLDONE_HINT) {
|
||||
} else if (chatId == DcChat.DC_CHAT_ID_ALLDONE_HINT) {
|
||||
return MESSAGE_TYPE_INBOX_ZERO;
|
||||
} else {
|
||||
return MESSAGE_TYPE_THREAD;
|
||||
@@ -159,7 +171,9 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
|
||||
interface ItemClickListener {
|
||||
void onItemClick(ConversationListItem item);
|
||||
|
||||
void onItemLongClick(ConversationListItem item);
|
||||
|
||||
void onSwitchToArchive();
|
||||
}
|
||||
|
||||
@@ -169,7 +183,7 @@ class ConversationListAdapter extends BaseConversationListAdapter<ConversationLi
|
||||
return;
|
||||
}
|
||||
if (chatlist == null) {
|
||||
dcChatlist = new DcChatlist(0, 0);
|
||||
dcChatlist = new DcChatlist(0, 0);
|
||||
} else {
|
||||
dcChatlist = chatlist;
|
||||
dcContext = DcHelper.getContext(context);
|
||||
|
||||
@@ -11,22 +11,19 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
public class ConversationListArchiveActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ConversationListFragment.ConversationSelectedListener
|
||||
{
|
||||
implements ConversationListFragment.ConversationSelectedListener {
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
setContentView(R.layout.activity_conversation_list_archive);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
if (isRelayingMessageContent(this)) {
|
||||
getSupportActionBar().setTitle(isSharing(this) ? R.string.chat_share_with_title : R.string.forward_to);
|
||||
getSupportActionBar()
|
||||
.setTitle(isSharing(this) ? R.string.chat_share_with_title : R.string.forward_to);
|
||||
getSupportActionBar().setSubtitle(R.string.chat_archived_label);
|
||||
} else {
|
||||
getSupportActionBar().setTitle(R.string.chat_archived_label);
|
||||
@@ -36,20 +33,25 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
|
||||
bundle.putBoolean(ConversationListFragment.ARCHIVE, true);
|
||||
initFragment(R.id.fragment, new ConversationListFragment(), bundle);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (!isRelayingMessageContent(ConversationListArchiveActivity.this)) {
|
||||
// Load the ConversationListActivity in case it's not existent for some reason
|
||||
Intent intent = new Intent(ConversationListArchiveActivity.this, ConversationListActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
}
|
||||
getOnBackPressedDispatcher()
|
||||
.addCallback(
|
||||
this,
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (!isRelayingMessageContent(ConversationListArchiveActivity.this)) {
|
||||
// Load the ConversationListActivity in case it's not existent for some reason
|
||||
Intent intent =
|
||||
new Intent(
|
||||
ConversationListArchiveActivity.this, ConversationListActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
});
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,16 +29,15 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcChatlist;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
|
||||
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
|
||||
@@ -52,25 +51,21 @@ import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
|
||||
public class ConversationListFragment extends BaseConversationListFragment
|
||||
implements ItemClickListener, DcEventCenter.DcEventDelegate {
|
||||
implements ItemClickListener, DcEventCenter.DcEventDelegate {
|
||||
public static final String ARCHIVE = "archive";
|
||||
public static final String RELOAD_LIST = "reload_list";
|
||||
|
||||
private static final String TAG = ConversationListFragment.class.getSimpleName();
|
||||
|
||||
private RecyclerView list;
|
||||
private View emptyState;
|
||||
private TextView emptySearch;
|
||||
private final String queryFilter = "";
|
||||
private boolean archive;
|
||||
private Timer reloadTimer;
|
||||
private boolean chatlistJustLoaded;
|
||||
private boolean reloadTimerInstantly;
|
||||
private RecyclerView list;
|
||||
private View emptyState;
|
||||
private TextView emptySearch;
|
||||
private final String queryFilter = "";
|
||||
private boolean archive;
|
||||
private Timer reloadTimer;
|
||||
private boolean chatlistJustLoaded;
|
||||
private boolean reloadTimerInstantly;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
@@ -103,10 +98,10 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false);
|
||||
|
||||
list = ViewUtil.findById(view, R.id.list);
|
||||
fab = ViewUtil.findById(view, R.id.fab);
|
||||
emptyState = ViewUtil.findById(view, R.id.empty_state);
|
||||
emptySearch = ViewUtil.findById(view, R.id.empty_search);
|
||||
list = ViewUtil.findById(view, R.id.list);
|
||||
fab = ViewUtil.findById(view, R.id.fab);
|
||||
emptyState = ViewUtil.findById(view, R.id.empty_state);
|
||||
emptySearch = ViewUtil.findById(view, R.id.empty_search);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(list, true, archive, true, true);
|
||||
@@ -145,22 +140,25 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
updateReminders();
|
||||
|
||||
if (requireActivity().getIntent().getIntExtra(RELOAD_LIST, 0) == 1
|
||||
&& !chatlistJustLoaded) {
|
||||
if (requireActivity().getIntent().getIntExtra(RELOAD_LIST, 0) == 1 && !chatlistJustLoaded) {
|
||||
loadChatlist();
|
||||
reloadTimerInstantly = false;
|
||||
}
|
||||
chatlistJustLoaded = false;
|
||||
|
||||
reloadTimer = new Timer();
|
||||
reloadTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Util.runOnMain(() -> {
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
});
|
||||
}
|
||||
}, reloadTimerInstantly? 0 : 60 * 1000, 60 * 1000);
|
||||
reloadTimer.scheduleAtFixedRate(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
});
|
||||
}
|
||||
},
|
||||
reloadTimerInstantly ? 0 : 60 * 1000,
|
||||
60 * 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -184,7 +182,8 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
@SuppressLint({"StaticFieldLeak", "NewApi"})
|
||||
private void updateReminders() {
|
||||
// by the time onPostExecute() is asynchronously run, getActivity() might return null, so get the activity here:
|
||||
// by the time onPostExecute() is asynchronously run, getActivity() might return null, so get
|
||||
// the activity here:
|
||||
Activity activity = requireActivity();
|
||||
new AsyncTask<Context, Void, Void>() {
|
||||
@Override
|
||||
@@ -204,22 +203,29 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (!Prefs.getBooleanPreference(activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) {
|
||||
if (!Prefs.getBooleanPreference(
|
||||
activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) {
|
||||
Prefs.setBooleanPreference(activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true);
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
DozeReminder.maybeAskDirectly(activity);
|
||||
})
|
||||
.onAnyDenied(() -> {
|
||||
final DcContext dcContext = DcHelper.getContext(activity);
|
||||
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
msg.setText("\uD83D\uDC49 "+activity.getString(R.string.notifications_disabled)+" \uD83D\uDC48\n\n"
|
||||
+activity.getString(R.string.perm_explain_access_to_notifications_denied));
|
||||
dcContext.addDeviceMsg("android.notifications-disabled", msg);
|
||||
})
|
||||
.execute();
|
||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(
|
||||
() -> {
|
||||
DozeReminder.maybeAskDirectly(activity);
|
||||
})
|
||||
.onAnyDenied(
|
||||
() -> {
|
||||
final DcContext dcContext = DcHelper.getContext(activity);
|
||||
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
msg.setText(
|
||||
"\uD83D\uDC49 "
|
||||
+ activity.getString(R.string.notifications_disabled)
|
||||
+ " \uD83D\uDC48\n\n"
|
||||
+ activity.getString(
|
||||
R.string.perm_explain_access_to_notifications_denied));
|
||||
dcContext.addDeviceMsg("android.notifications-disabled", msg);
|
||||
})
|
||||
.execute();
|
||||
} else {
|
||||
DozeReminder.maybeAskDirectly(activity);
|
||||
}
|
||||
@@ -233,7 +239,8 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
private final Object loadChatlistLock = new Object();
|
||||
private boolean inLoadChatlist;
|
||||
private boolean needsAnotherLoad;
|
||||
private void loadChatlistAsync() {
|
||||
|
||||
public void loadChatlistAsync() {
|
||||
synchronized (loadChatlistLock) {
|
||||
needsAnotherLoad = true;
|
||||
if (inLoadChatlist) {
|
||||
@@ -243,24 +250,25 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
inLoadChatlist = true;
|
||||
}
|
||||
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
while(true) {
|
||||
synchronized (loadChatlistLock) {
|
||||
if (!needsAnotherLoad) {
|
||||
inLoadChatlist = false;
|
||||
return;
|
||||
}
|
||||
needsAnotherLoad = false;
|
||||
}
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
while (true) {
|
||||
synchronized (loadChatlistLock) {
|
||||
if (!needsAnotherLoad) {
|
||||
inLoadChatlist = false;
|
||||
return;
|
||||
}
|
||||
needsAnotherLoad = false;
|
||||
}
|
||||
|
||||
Log.i(TAG, "executing debounced chatlist loading");
|
||||
loadChatlist();
|
||||
Util.sleep(100);
|
||||
}
|
||||
});
|
||||
Log.i(TAG, "executing debounced chatlist loading");
|
||||
loadChatlist();
|
||||
Util.sleep(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadChatlist() {
|
||||
private void loadChatlist() {
|
||||
int listflags = 0;
|
||||
if (archive) {
|
||||
listflags |= DcContext.DC_GCL_ARCHIVED_ONLY;
|
||||
@@ -272,32 +280,36 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
// can't load chat list at this time, see: https://github.com/deltachat/deltachat-android/issues/2012
|
||||
// can't load chat list at this time, see:
|
||||
// https://github.com/deltachat/deltachat-android/issues/2012
|
||||
Log.w(TAG, "Ignoring call to loadChatlist()");
|
||||
return;
|
||||
}
|
||||
DcChatlist chatlist = DcHelper.getContext(context).getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0);
|
||||
DcChatlist chatlist =
|
||||
DcHelper.getContext(context)
|
||||
.getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0);
|
||||
|
||||
Util.runOnMain(() -> {
|
||||
if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
emptySearch.setVisibility(View.INVISIBLE);
|
||||
fab.startPulse(3 * 1000);
|
||||
} else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
emptySearch.setVisibility(View.VISIBLE);
|
||||
emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter));
|
||||
} else {
|
||||
list.setVisibility(View.VISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
emptySearch.setVisibility(View.INVISIBLE);
|
||||
fab.stopPulse();
|
||||
}
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
emptySearch.setVisibility(View.INVISIBLE);
|
||||
fab.startPulse(3 * 1000);
|
||||
} else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
emptySearch.setVisibility(View.VISIBLE);
|
||||
emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter));
|
||||
} else {
|
||||
list.setVisibility(View.VISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
emptySearch.setVisibility(View.INVISIBLE);
|
||||
fab.stopPulse();
|
||||
}
|
||||
|
||||
((ConversationListAdapter)list.getAdapter()).changeData(chatlist);
|
||||
});
|
||||
((ConversationListAdapter) list.getAdapter()).changeData(chatlist);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -307,12 +319,12 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
@Override
|
||||
protected void setFabVisibility(boolean isActionMode) {
|
||||
fab.setVisibility((isActionMode || !archive)? View.VISIBLE : View.GONE);
|
||||
fab.setVisibility((isActionMode || !archive) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(ConversationListItem item) {
|
||||
onItemClick(item.getChatId());
|
||||
onItemClick(item.getChatId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -322,14 +334,15 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
@Override
|
||||
public void onSwitchToArchive() {
|
||||
((ConversationSelectedListener)requireActivity()).onSwitchToArchive();
|
||||
((ConversationSelectedListener) requireActivity()).onSwitchToArchive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
final int accId = event.getAccountId();
|
||||
if (event.getId() == DcContext.DC_EVENT_CHAT_DELETED) {
|
||||
DcHelper.getNotificationCenter(requireActivity()).removeNotifications(accId, event.getData1Int());
|
||||
DcHelper.getNotificationCenter(requireActivity())
|
||||
.removeNotifications(accId, event.getData1Int());
|
||||
} else if (accId != DcHelper.getContext(requireActivity()).getAccountId()) {
|
||||
Activity activity = getActivity();
|
||||
if (activity instanceof ConversationListActivity) {
|
||||
@@ -352,7 +365,4 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
loadChatlistAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,17 +30,16 @@ import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.components.AvatarView;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
@@ -53,25 +52,22 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationListItem extends RelativeLayout
|
||||
implements BindableConversationListItem, Unbindable
|
||||
{
|
||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
implements BindableConversationListItem, Unbindable {
|
||||
private static final Typeface BOLD_TYPEFACE =
|
||||
Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private static final Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
private Set<Long> selectedThreads;
|
||||
private long chatId;
|
||||
private int msgId;
|
||||
private TextView subjectView;
|
||||
private FromTextView fromView;
|
||||
private TextView dateView;
|
||||
private TextView archivedBadgeView;
|
||||
private TextView requestBadgeView;
|
||||
private Set<Long> selectedThreads;
|
||||
private long chatId;
|
||||
private int msgId;
|
||||
private TextView subjectView;
|
||||
private FromTextView fromView;
|
||||
private TextView dateView;
|
||||
private TextView archivedBadgeView;
|
||||
private TextView requestBadgeView;
|
||||
private DeliveryStatusView deliveryStatusIndicator;
|
||||
private ImageView unreadIndicator;
|
||||
private ImageView unreadIndicator;
|
||||
|
||||
private AvatarView avatar;
|
||||
|
||||
@@ -86,69 +82,72 @@ public class ConversationListItem extends RelativeLayout
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
this.subjectView = findViewById(R.id.subject);
|
||||
this.fromView = findViewById(R.id.from_text);
|
||||
this.dateView = findViewById(R.id.date);
|
||||
this.subjectView = findViewById(R.id.subject);
|
||||
this.fromView = findViewById(R.id.from_text);
|
||||
this.dateView = findViewById(R.id.date);
|
||||
this.deliveryStatusIndicator = new DeliveryStatusView(findViewById(R.id.delivery_indicator));
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.archivedBadgeView = findViewById(R.id.archived_badge);
|
||||
this.requestBadgeView = findViewById(R.id.request_badge);
|
||||
this.unreadIndicator = findViewById(R.id.unread_indicator);
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.archivedBadgeView = findViewById(R.id.archived_badge);
|
||||
this.requestBadgeView = findViewById(R.id.request_badge);
|
||||
this.unreadIndicator = findViewById(R.id.unread_indicator);
|
||||
|
||||
ViewUtil.setTextViewGravityStart(this.fromView, getContext());
|
||||
ViewUtil.setTextViewGravityStart(this.subjectView, getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode)
|
||||
{
|
||||
public void bind(
|
||||
@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode) {
|
||||
bind(thread, msgId, dcSummary, glideRequests, selectedThreads, batchMode, null);
|
||||
}
|
||||
|
||||
public void bind(@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode,
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
this.selectedThreads = selectedThreads;
|
||||
Recipient recipient = thread.getRecipient();
|
||||
this.chatId = thread.getThreadId();
|
||||
this.msgId = msgId;
|
||||
public void bind(
|
||||
@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode,
|
||||
@Nullable String highlightSubstring) {
|
||||
this.selectedThreads = selectedThreads;
|
||||
Recipient recipient = thread.getRecipient();
|
||||
this.chatId = thread.getThreadId();
|
||||
this.msgId = msgId;
|
||||
|
||||
int state = dcSummary.getState();
|
||||
int state = dcSummary.getState();
|
||||
|
||||
if (highlightSubstring != null) {
|
||||
this.fromView.setText(getHighlightedSpan(recipient.getName(), highlightSubstring));
|
||||
} else {
|
||||
this.fromView.setText(recipient, state!=DcMsg.DC_STATE_IN_FRESH);
|
||||
this.fromView.setText(recipient, state != DcMsg.DC_STATE_IN_FRESH);
|
||||
}
|
||||
|
||||
subjectView.setVisibility(chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK? GONE : VISIBLE);
|
||||
subjectView.setVisibility(chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK ? GONE : VISIBLE);
|
||||
this.subjectView.setText(thread.getDisplayBody());
|
||||
this.subjectView.setTypeface(state==DcMsg.DC_STATE_IN_FRESH ? BOLD_TYPEFACE : LIGHT_TYPEFACE);
|
||||
this.subjectView.setTextColor(state==DcMsg.DC_STATE_IN_FRESH ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color));
|
||||
this.subjectView.setTypeface(state == DcMsg.DC_STATE_IN_FRESH ? BOLD_TYPEFACE : LIGHT_TYPEFACE);
|
||||
this.subjectView.setTextColor(
|
||||
state == DcMsg.DC_STATE_IN_FRESH
|
||||
? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color));
|
||||
|
||||
if (thread.getDate() > 0) {
|
||||
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), thread.getDate());
|
||||
dateView.setText(date);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dateView.setText("");
|
||||
}
|
||||
|
||||
dateView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
thread.isSendingLocations()? R.drawable.ic_location_chatlist : 0, 0,
|
||||
thread.getVisibility()==DcChat.DC_CHAT_VISIBILITY_PINNED? R.drawable.ic_pinned_chatlist : 0, 0
|
||||
);
|
||||
thread.isSendingLocations() ? R.drawable.ic_location_chatlist : 0, 0,
|
||||
thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_PINNED
|
||||
? R.drawable.ic_pinned_chatlist
|
||||
: 0,
|
||||
0);
|
||||
|
||||
setStatusIcons(thread, state);
|
||||
setBatchState(batchMode);
|
||||
@@ -159,22 +158,22 @@ public class ConversationListItem extends RelativeLayout
|
||||
DcContact contact = recipient.getDcContact();
|
||||
avatar.setSeenRecently(contact != null && contact.wasSeenRecently());
|
||||
|
||||
DcChat dcChat = DcHelper.getContext(getContext()).getChat((int)chatId);
|
||||
DcChat dcChat = DcHelper.getContext(getContext()).getChat((int) chatId);
|
||||
boolean isProtected = dcChat.isDeviceTalk() || dcChat.isSelfTalk();
|
||||
|
||||
fromView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
thread.isMuted()? R.drawable.ic_volume_off_grey600_18dp : 0,
|
||||
thread.isMuted() ? R.drawable.ic_volume_off_grey600_18dp : 0,
|
||||
0,
|
||||
isProtected? R.drawable.ic_verified : 0,
|
||||
isProtected ? R.drawable.ic_verified : 0,
|
||||
0);
|
||||
}
|
||||
|
||||
public void bind(@NonNull DcContact contact,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
public void bind(
|
||||
@NonNull DcContact contact,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable String highlightSubstring) {
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
Recipient recipient = new Recipient(getContext(), contact);
|
||||
Recipient recipient = new Recipient(getContext(), contact);
|
||||
|
||||
fromView.setText(getHighlightedSpan(contact.getDisplayName(), highlightSubstring));
|
||||
fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
@@ -191,14 +190,14 @@ public class ConversationListItem extends RelativeLayout
|
||||
avatar.setSeenRecently(contact.wasSeenRecently());
|
||||
}
|
||||
|
||||
public void bind(@NonNull DcMsg messageResult,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable String highlightSubstring)
|
||||
{
|
||||
public void bind(
|
||||
@NonNull DcMsg messageResult,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable String highlightSubstring) {
|
||||
DcContext dcContext = DcHelper.getContext(getContext());
|
||||
DcContact sender = dcContext.getContact(messageResult.getFromId());
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
Recipient recipient = new Recipient(getContext(), sender);
|
||||
Recipient recipient = new Recipient(getContext(), sender);
|
||||
|
||||
fromView.setText(recipient, true);
|
||||
fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
@@ -206,10 +205,10 @@ public class ConversationListItem extends RelativeLayout
|
||||
subjectView.setText(getHighlightedSpan(messageResult.getSummarytext(512), highlightSubstring));
|
||||
|
||||
long timestamp = messageResult.getTimestamp();
|
||||
if(timestamp>0) {
|
||||
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), messageResult.getTimestamp()));
|
||||
}
|
||||
else {
|
||||
if (timestamp > 0) {
|
||||
dateView.setText(
|
||||
DateUtils.getBriefRelativeTimeSpanString(getContext(), messageResult.getTimestamp()));
|
||||
} else {
|
||||
dateView.setText("");
|
||||
}
|
||||
dateView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
@@ -224,8 +223,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind() {
|
||||
}
|
||||
public void unbind() {}
|
||||
|
||||
private void setBatchState(boolean batch) {
|
||||
setSelected(batch && selectedThreads.contains(chatId));
|
||||
@@ -240,19 +238,15 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
private void setStatusIcons(ThreadRecord thread, int state) {
|
||||
if (thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
{
|
||||
if (thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED) {
|
||||
archivedBadgeView.setVisibility(View.VISIBLE);
|
||||
requestBadgeView.setVisibility(thread.isContactRequest() ? View.VISIBLE : View.GONE);
|
||||
deliveryStatusIndicator.setNone();
|
||||
}
|
||||
else if (thread.isContactRequest()) {
|
||||
} else if (thread.isContactRequest()) {
|
||||
requestBadgeView.setVisibility(View.VISIBLE);
|
||||
archivedBadgeView.setVisibility(View.GONE);
|
||||
deliveryStatusIndicator.setNone();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
requestBadgeView.setVisibility(View.GONE);
|
||||
archivedBadgeView.setVisibility(View.GONE);
|
||||
|
||||
@@ -271,52 +265,53 @@ public class ConversationListItem extends RelativeLayout
|
||||
} else {
|
||||
deliveryStatusIndicator.setNone();
|
||||
}
|
||||
|
||||
if (state == DcMsg.DC_STATE_OUT_FAILED) {
|
||||
deliveryStatusIndicator.setTint(Color.RED);
|
||||
} else {
|
||||
deliveryStatusIndicator.resetTint();
|
||||
}
|
||||
}
|
||||
|
||||
int unreadCount = thread.getUnreadCount();
|
||||
if(unreadCount==0 || thread.isContactRequest()) {
|
||||
if (unreadCount == 0 || thread.isContactRequest()) {
|
||||
unreadIndicator.setVisibility(View.GONE);
|
||||
} else {
|
||||
final int color;
|
||||
if(thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK){
|
||||
color = getResources().getColor(ThemeUtil.isDarkTheme(getContext()) ? R.color.unread_count_muted_dark : R.color.unread_count_muted);
|
||||
if (thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK) {
|
||||
color =
|
||||
getResources()
|
||||
.getColor(
|
||||
ThemeUtil.isDarkTheme(getContext())
|
||||
? R.color.unread_count_muted_dark
|
||||
: R.color.unread_count_muted);
|
||||
} else {
|
||||
final TypedArray attrs = getContext().obtainStyledAttributes(new int[] {
|
||||
R.attr.conversation_list_item_unreadcount_color,
|
||||
});
|
||||
final TypedArray attrs =
|
||||
getContext()
|
||||
.obtainStyledAttributes(
|
||||
new int[] {
|
||||
R.attr.conversation_list_item_unreadcount_color,
|
||||
});
|
||||
color = attrs.getColor(0, Color.BLACK);
|
||||
}
|
||||
unreadIndicator.setImageDrawable(TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.width(ViewUtil.dpToPx(getContext(), 24))
|
||||
.height(ViewUtil.dpToPx(getContext(), 24))
|
||||
.textColor(Color.WHITE)
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRound(String.valueOf(unreadCount), color));
|
||||
unreadIndicator.setImageDrawable(
|
||||
TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.width(ViewUtil.dpToPx(getContext(), 24))
|
||||
.height(ViewUtil.dpToPx(getContext(), 24))
|
||||
.textColor(Color.WHITE)
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRound(String.valueOf(unreadCount), color));
|
||||
unreadIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBgColor(ThreadRecord thread) {
|
||||
int bg = R.attr.conversation_list_item_background;
|
||||
if (thread!=null && thread.getVisibility()==DcChat.DC_CHAT_VISIBILITY_PINNED) {
|
||||
bg = R.attr.pinned_list_item_background;
|
||||
if (thread != null && thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_PINNED) {
|
||||
bg = R.attr.pinned_list_item_background;
|
||||
}
|
||||
try (TypedArray ta = getContext().obtainStyledAttributes(new int[]{bg})) {
|
||||
try (TypedArray ta = getContext().obtainStyledAttributes(new int[] {bg})) {
|
||||
ViewUtil.setBackground(this, ta.getDrawable(0));
|
||||
}
|
||||
}
|
||||
|
||||
private Spanned getHighlightedSpan(@Nullable String value,
|
||||
@Nullable String highlight)
|
||||
{
|
||||
private Spanned getHighlightedSpan(@Nullable String value, @Nullable String highlight) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return new SpannableString("");
|
||||
}
|
||||
@@ -327,11 +322,11 @@ public class ConversationListItem extends RelativeLayout
|
||||
return new SpannableString(value);
|
||||
}
|
||||
|
||||
String normalizedValue = value.toLowerCase(Util.getLocale());
|
||||
String normalizedTest = highlight.toLowerCase(Util.getLocale());
|
||||
String normalizedValue = value.toLowerCase(Util.getLocale());
|
||||
String normalizedTest = highlight.toLowerCase(Util.getLocale());
|
||||
|
||||
Spannable spanned = new SpannableString(value);
|
||||
int searchStartIndex = 0;
|
||||
Spannable spanned = new SpannableString(value);
|
||||
int searchStartIndex = 0;
|
||||
|
||||
for (String token : normalizedTest.split(" ")) {
|
||||
if (token.trim().isEmpty()) continue;
|
||||
@@ -343,7 +338,8 @@ public class ConversationListItem extends RelativeLayout
|
||||
|
||||
if (start >= 0) {
|
||||
int end = Math.min(start + token.length(), spanned.length());
|
||||
spanned.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
spanned.setSpan(
|
||||
new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
searchStartIndex = end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.b44t.messenger.DcLot;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem{
|
||||
public class ConversationListItemInboxZero extends LinearLayout
|
||||
implements BindableConversationListItem {
|
||||
public ConversationListItemInboxZero(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -24,21 +20,25 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ConversationListItemInboxZero(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
public ConversationListItemInboxZero(
|
||||
Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public ConversationListItemInboxZero(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public ConversationListItemInboxZero(
|
||||
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind() {
|
||||
|
||||
}
|
||||
public void unbind() {}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ThreadRecord thread, int msgId, @NonNull DcLot dcSummary, @NonNull GlideRequests glideRequests, @NonNull Set<Long> selectedThreads, boolean batchMode) {
|
||||
|
||||
}
|
||||
public void bind(
|
||||
@NonNull ThreadRecord thread,
|
||||
int msgId,
|
||||
@NonNull DcLot dcSummary,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<Long> selectedThreads,
|
||||
boolean batchMode) {}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,24 @@ import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* "Relaying" means "Forwarding or Sharing".
|
||||
*
|
||||
* When forwarding or sharing, we show the ConversationListActivity to the user.
|
||||
* However, ConversationListActivity has `launchMode="singleTask"`, which means that this will
|
||||
* destroy the existing ConversationListActivity.
|
||||
* <p>When forwarding or sharing, we show the ConversationListActivity to the user. However,
|
||||
* ConversationListActivity has `launchMode="singleTask"`, which means that this will destroy the
|
||||
* existing ConversationListActivity.
|
||||
*
|
||||
* In API 20-29, `startActivityForResult()` could be used instead of `startActivity()`
|
||||
* to override this behavior and get two instances of ConversationListActivity.
|
||||
* <p>In API 20-29, `startActivityForResult()` could be used instead of `startActivity()` to
|
||||
* override this behavior and get two instances of ConversationListActivity.
|
||||
*
|
||||
* As this is not possible anymore starting with API 30, we needed another solution, and created
|
||||
* <p>As this is not possible anymore starting with API 30, we needed another solution, and created
|
||||
* this activity here.
|
||||
*
|
||||
* See https://github.com/deltachat/deltachat-android/issues/1704.
|
||||
* <p>See https://github.com/deltachat/deltachat-android/issues/1704.
|
||||
*/
|
||||
|
||||
public class ConversationListRelayingActivity extends ConversationListActivity {
|
||||
static WeakReference<ConversationListRelayingActivity> INSTANCE = null;
|
||||
|
||||
@@ -42,7 +39,8 @@ public class ConversationListRelayingActivity extends ConversationListActivity {
|
||||
|
||||
// =================== Static Methods ===================
|
||||
public static void start(Fragment fragment, Intent intent) {
|
||||
intent.setComponent(new ComponentName(fragment.getContext(), ConversationListRelayingActivity.class));
|
||||
intent.setComponent(
|
||||
new ComponentName(fragment.getContext(), ConversationListRelayingActivity.class));
|
||||
fragment.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,30 +4,32 @@ import android.animation.ValueAnimator;
|
||||
import android.content.res.Resources;
|
||||
import android.view.View;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
final class ConversationSwipeAnimationHelper {
|
||||
|
||||
static final float TRIGGER_DX = dpToPx(64);
|
||||
static final float MAX_DX = dpToPx(96);
|
||||
static final float MAX_DX = dpToPx(96);
|
||||
|
||||
private static final float REPLY_SCALE_OVERSHOOT = 1.8f;
|
||||
private static final float REPLY_SCALE_MAX = 1.2f;
|
||||
private static final float REPLY_SCALE_MIN = 1f;
|
||||
private static final long REPLY_SCALE_OVERSHOOT_DURATION = 200;
|
||||
private static final float REPLY_SCALE_OVERSHOOT = 1.8f;
|
||||
private static final float REPLY_SCALE_MAX = 1.2f;
|
||||
private static final float REPLY_SCALE_MIN = 1f;
|
||||
private static final long REPLY_SCALE_OVERSHOOT_DURATION = 200;
|
||||
|
||||
private static final Interpolator BUBBLE_INTERPOLATOR = new BubblePositionInterpolator(0f, TRIGGER_DX, MAX_DX);
|
||||
private static final Interpolator REPLY_ALPHA_INTERPOLATOR = new ClampingLinearInterpolator(0f, 1f, 1f);
|
||||
private static final Interpolator REPLY_TRANSITION_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(10));
|
||||
private static final Interpolator AVATAR_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(8));
|
||||
private static final Interpolator REPLY_SCALE_INTERPOLATOR = new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX);
|
||||
private static final Interpolator BUBBLE_INTERPOLATOR =
|
||||
new BubblePositionInterpolator(0f, TRIGGER_DX, MAX_DX);
|
||||
private static final Interpolator REPLY_ALPHA_INTERPOLATOR =
|
||||
new ClampingLinearInterpolator(0f, 1f, 1f);
|
||||
private static final Interpolator REPLY_TRANSITION_INTERPOLATOR =
|
||||
new ClampingLinearInterpolator(0f, dpToPx(10));
|
||||
private static final Interpolator AVATAR_INTERPOLATOR =
|
||||
new ClampingLinearInterpolator(0f, dpToPx(8));
|
||||
private static final Interpolator REPLY_SCALE_INTERPOLATOR =
|
||||
new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX);
|
||||
|
||||
private ConversationSwipeAnimationHelper() {
|
||||
}
|
||||
private ConversationSwipeAnimationHelper() {}
|
||||
|
||||
public static void update(@NonNull ConversationItem conversationItem, float dx, float sign) {
|
||||
float progress = dx / TRIGGER_DX;
|
||||
@@ -46,11 +48,13 @@ final class ConversationSwipeAnimationHelper {
|
||||
bodyBubble.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
|
||||
}
|
||||
|
||||
private static void updateReactionsTransition(@NonNull View reactionsContainer, float dx, float sign) {
|
||||
private static void updateReactionsTransition(
|
||||
@NonNull View reactionsContainer, float dx, float sign) {
|
||||
reactionsContainer.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
|
||||
}
|
||||
|
||||
private static void updateReplyIconTransition(@NonNull View replyIcon, float dx, float progress, float sign) {
|
||||
private static void updateReplyIconTransition(
|
||||
@NonNull View replyIcon, float dx, float progress, float sign) {
|
||||
if (progress > 0.05f) {
|
||||
replyIcon.setAlpha(REPLY_ALPHA_INTERPOLATOR.getInterpolation(progress));
|
||||
} else replyIcon.setAlpha(0f);
|
||||
@@ -64,21 +68,21 @@ final class ConversationSwipeAnimationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateContactPhotoHolderTransition(@Nullable View contactPhotoHolder,
|
||||
float progress,
|
||||
float sign)
|
||||
{
|
||||
private static void updateContactPhotoHolderTransition(
|
||||
@Nullable View contactPhotoHolder, float progress, float sign) {
|
||||
if (contactPhotoHolder == null) return;
|
||||
contactPhotoHolder.setTranslationX(AVATAR_INTERPOLATOR.getInterpolation(progress) * sign);
|
||||
}
|
||||
|
||||
private static void triggerReplyIcon(@NonNull View replyIcon) {
|
||||
ValueAnimator animator = ValueAnimator.ofFloat(REPLY_SCALE_MAX, REPLY_SCALE_OVERSHOOT, REPLY_SCALE_MAX);
|
||||
ValueAnimator animator =
|
||||
ValueAnimator.ofFloat(REPLY_SCALE_MAX, REPLY_SCALE_OVERSHOOT, REPLY_SCALE_MAX);
|
||||
animator.setDuration(REPLY_SCALE_OVERSHOOT_DURATION);
|
||||
animator.addUpdateListener(animation -> {
|
||||
replyIcon.setScaleX((float) animation.getAnimatedValue());
|
||||
replyIcon.setScaleY((float) animation.getAnimatedValue());
|
||||
});
|
||||
animator.addUpdateListener(
|
||||
animation -> {
|
||||
replyIcon.setScaleX((float) animation.getAnimatedValue());
|
||||
replyIcon.setScaleY((float) animation.getAnimatedValue());
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@@ -93,9 +97,9 @@ final class ConversationSwipeAnimationHelper {
|
||||
private final float end;
|
||||
|
||||
private BubblePositionInterpolator(float start, float middle, float end) {
|
||||
this.start = start;
|
||||
this.start = start;
|
||||
this.middle = middle;
|
||||
this.end = end;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,11 +109,11 @@ final class ConversationSwipeAnimationHelper {
|
||||
} else if (input < middle) {
|
||||
return input;
|
||||
} else {
|
||||
float segmentLength = end - middle;
|
||||
float segmentTraveled = input - middle;
|
||||
float segmentLength = end - middle;
|
||||
float segmentTraveled = input - middle;
|
||||
float segmentCompletion = segmentTraveled / segmentLength;
|
||||
float scaleDownFactor = middle / (input * 2);
|
||||
float output = middle + (segmentLength * segmentCompletion * scaleDownFactor);
|
||||
float scaleDownFactor = middle / (input * 2);
|
||||
float output = middle + (segmentLength * segmentCompletion * scaleDownFactor);
|
||||
|
||||
return Math.min(output, end);
|
||||
}
|
||||
@@ -128,10 +132,10 @@ final class ConversationSwipeAnimationHelper {
|
||||
}
|
||||
|
||||
ClampingLinearInterpolator(float start, float end, float scale) {
|
||||
slope = (end - start) * scale;
|
||||
slope = (end - start) * scale;
|
||||
yIntercept = start;
|
||||
max = Math.max(start, end);
|
||||
min = Math.min(start, end);
|
||||
max = Math.max(start, end);
|
||||
min = Math.min(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,5 +143,4 @@ final class ConversationSwipeAnimationHelper {
|
||||
return Util.clamp(slope * input + yIntercept, min, max);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -8,14 +7,11 @@ import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AvatarView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -24,16 +20,14 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
private View content;
|
||||
private ImageView back;
|
||||
private AvatarView avatar;
|
||||
private TextView title;
|
||||
private TextView subtitle;
|
||||
private ImageView ephemeralIcon;
|
||||
private View content;
|
||||
private ImageView back;
|
||||
private AvatarView avatar;
|
||||
private TextView title;
|
||||
private TextView subtitle;
|
||||
private ImageView ephemeralIcon;
|
||||
|
||||
public ConversationTitleView(Context context) {
|
||||
this(context, null);
|
||||
@@ -41,18 +35,17 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
public ConversationTitleView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
this.back = ViewUtil.findById(this, R.id.up_button);
|
||||
this.content = ViewUtil.findById(this, R.id.content);
|
||||
this.title = ViewUtil.findById(this, R.id.title);
|
||||
this.subtitle = ViewUtil.findById(this, R.id.subtitle);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.back = ViewUtil.findById(this, R.id.up_button);
|
||||
this.content = ViewUtil.findById(this, R.id.content);
|
||||
this.title = ViewUtil.findById(this, R.id.title);
|
||||
this.subtitle = ViewUtil.findById(this, R.id.subtitle);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.ephemeralIcon = ViewUtil.findById(this, R.id.ephemeral_icon);
|
||||
|
||||
ViewUtil.setTextViewGravityStart(this.title, getContext());
|
||||
@@ -75,21 +68,25 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
} else if (dcChat.isInBroadcast()) {
|
||||
subtitleStr = context.getString(R.string.channel);
|
||||
} else if (dcChat.isOutBroadcast()) {
|
||||
subtitleStr = context.getResources().getQuantityString(R.plurals.n_recipients, chatContacts.length, chatContacts.length);
|
||||
} else if( dcChat.isMultiUser() ) {
|
||||
subtitleStr =
|
||||
context
|
||||
.getResources()
|
||||
.getQuantityString(R.plurals.n_recipients, chatContacts.length, chatContacts.length);
|
||||
} else if (dcChat.isMultiUser()) {
|
||||
if (chatContacts.length > 1 || Util.contains(chatContacts, DcContact.DC_CONTACT_ID_SELF)) {
|
||||
subtitleStr = context.getResources().getQuantityString(R.plurals.n_members, chatContacts.length, chatContacts.length);
|
||||
subtitleStr =
|
||||
context
|
||||
.getResources()
|
||||
.getQuantityString(R.plurals.n_members, chatContacts.length, chatContacts.length);
|
||||
} else {
|
||||
subtitleStr = "…";
|
||||
}
|
||||
} else if( chatContacts.length>=1 ) {
|
||||
if( dcChat.isSelfTalk() ) {
|
||||
} else if (chatContacts.length >= 1) {
|
||||
if (dcChat.isSelfTalk()) {
|
||||
subtitleStr = context.getString(R.string.chat_self_talk_subtitle);
|
||||
}
|
||||
else if( dcChat.isDeviceTalk() ) {
|
||||
} else if (dcChat.isDeviceTalk()) {
|
||||
subtitleStr = context.getString(R.string.device_talk_subtitle);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
DcContact dcContact = dcContext.getContact(chatContacts[0]);
|
||||
isOnline = dcContact.wasSeenRecently();
|
||||
if (!dcChat.isEncrypted()) {
|
||||
@@ -101,7 +98,9 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
} else {
|
||||
long timestamp = dcContact.getLastSeen();
|
||||
if (timestamp >= 0) {
|
||||
subtitleStr = context.getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, timestamp));
|
||||
subtitleStr =
|
||||
context.getString(
|
||||
R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, timestamp));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,8 +108,8 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
avatar.setAvatar(glideRequests, new Recipient(getContext(), dcChat), false);
|
||||
avatar.setSeenRecently(isOnline);
|
||||
int imgLeft = dcChat.isMuted()? R.drawable.ic_volume_off_white_18dp : 0;
|
||||
int imgRight = dcChat.isSelfTalk() || dcChat.isDeviceTalk()? R.drawable.ic_verified : 0;
|
||||
int imgLeft = dcChat.isMuted() ? R.drawable.ic_volume_off_white_18dp : 0;
|
||||
int imgRight = dcChat.isSelfTalk() || dcChat.isDeviceTalk() ? R.drawable.ic_verified : 0;
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(imgLeft, 0, imgRight, 0);
|
||||
if (!TextUtils.isEmpty(subtitleStr)) {
|
||||
subtitle.setText(subtitleStr);
|
||||
@@ -119,7 +118,7 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
subtitle.setVisibility(View.GONE);
|
||||
}
|
||||
boolean isEphemeral = dcContext.getChatEphemeralTimer(chatId) != 0;
|
||||
ephemeralIcon.setVisibility(isEphemeral? View.VISIBLE : View.GONE);
|
||||
ephemeralIcon.setVisibility(isEphemeral ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void setTitle(@NonNull GlideRequests glideRequests, @NonNull DcContact contact) {
|
||||
|
||||
@@ -5,28 +5,25 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Set;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationUpdateItem extends BaseConversationItem
|
||||
{
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private AppCompatImageView appIcon;
|
||||
private int textColor;
|
||||
public class ConversationUpdateItem extends BaseConversationItem {
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
private AppCompatImageView appIcon;
|
||||
private int textColor;
|
||||
|
||||
public ConversationUpdateItem(Context context) {
|
||||
this(context, null);
|
||||
@@ -42,10 +39,9 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
|
||||
initializeAttributes();
|
||||
|
||||
bodyText = findViewById(R.id.conversation_update_body);
|
||||
bodyText = findViewById(R.id.conversation_update_body);
|
||||
deliveryStatusView = new DeliveryStatusView(findViewById(R.id.delivery_indicator));
|
||||
appIcon = findViewById(R.id.app_icon);
|
||||
|
||||
appIcon = findViewById(R.id.app_icon);
|
||||
|
||||
bodyText.setOnLongClickListener(passthroughClickListener);
|
||||
bodyText.setOnClickListener(passthroughClickListener);
|
||||
@@ -56,22 +52,25 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean pulseUpdate)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
public void bind(
|
||||
@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean pulseUpdate,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener) {
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
setGenericInfoRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void initializeAttributes() {
|
||||
final int[] attributes = new int[] {
|
||||
R.attr.conversation_item_update_text_color,
|
||||
};
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
final int[] attributes =
|
||||
new int[] {
|
||||
R.attr.conversation_item_update_text_color,
|
||||
};
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
|
||||
textColor = attrs.getColor(0, Color.WHITE);
|
||||
attrs.recycle();
|
||||
@@ -114,20 +113,13 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
bodyText.setText(messageRecord.getDisplayBody());
|
||||
bodyText.setVisibility(VISIBLE);
|
||||
|
||||
if (messageRecord.isFailed()) deliveryStatusView.setFailed();
|
||||
else if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
||||
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
|
||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
||||
else deliveryStatusView.setNone();
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
deliveryStatusView.setTint(Color.RED);
|
||||
} else {
|
||||
deliveryStatusView.setTint(textColor);
|
||||
}
|
||||
if (messageRecord.isFailed()) deliveryStatusView.setFailed();
|
||||
else if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
|
||||
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
|
||||
else if (messageRecord.isPending()) deliveryStatusView.setPending();
|
||||
else deliveryStatusView.setNone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind() {
|
||||
}
|
||||
public void unbind() {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@@ -15,22 +14,19 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import org.thoughtcrime.securesms.components.AvatarSelector;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -43,11 +39,6 @@ import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
@@ -55,10 +46,10 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final int REQUEST_CODE_AVATAR = 1;
|
||||
|
||||
private InputAwareLayout container;
|
||||
private ImageView avatar;
|
||||
private EditText name;
|
||||
private EditText statusView;
|
||||
private InputAwareLayout container;
|
||||
private ImageView avatar;
|
||||
private EditText name;
|
||||
private EditText statusView;
|
||||
|
||||
private boolean avatarChanged;
|
||||
private boolean imageLoaded;
|
||||
@@ -66,7 +57,6 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
private Bitmap avatarBmp;
|
||||
private AttachmentManager attachmentManager;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -84,17 +74,20 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
initializeProfileAvatar();
|
||||
initializeStatusText();
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (container.isInputOpen()) {
|
||||
container.hideCurrentInput(name);
|
||||
} else {
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
getOnBackPressedDispatcher()
|
||||
.addCallback(
|
||||
this,
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (container.isInputOpen()) {
|
||||
container.hideCurrentInput(name);
|
||||
} else {
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,7 +112,8 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@@ -128,12 +122,12 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_AVATAR:
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
onFileSelected(inputFile);
|
||||
break;
|
||||
|
||||
@@ -145,26 +139,28 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
private void setAvatarView(Uri output) {
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(
|
||||
new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
public void onResourceReady(
|
||||
@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
avatarChanged = true;
|
||||
imageLoaded = true;
|
||||
avatarBmp = resource;
|
||||
}
|
||||
});
|
||||
GlideApp.with(this)
|
||||
.load(output)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
.load(output)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
private void onFileSelected(Uri inputFile) {
|
||||
@@ -176,11 +172,11 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
TextView loginSuccessText = ViewUtil.findById(this, R.id.login_success_text);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.name = ViewUtil.findById(this, R.id.name_text);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.statusView = ViewUtil.findById(this, R.id.status_text);
|
||||
TextView loginSuccessText = ViewUtil.findById(this, R.id.login_success_text);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.name = ViewUtil.findById(this, R.id.name_text);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.statusView = ViewUtil.findById(this, R.id.status_text);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(container);
|
||||
@@ -189,7 +185,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
}
|
||||
|
||||
private void initializeProfileName() {
|
||||
String profileName = DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME);
|
||||
String profileName = DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME);
|
||||
if (!TextUtils.isEmpty(profileName)) {
|
||||
name.setText(profileName);
|
||||
name.setSelection(profileName.length(), profileName.length());
|
||||
@@ -200,18 +196,21 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
File avatarFile = AvatarHelper.getSelfAvatarFile(this);
|
||||
if (avatarFile.exists() && avatarFile.length() > 0) {
|
||||
imageLoaded = true;
|
||||
GlideApp.with(this)
|
||||
.load(avatarFile)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
GlideApp.with(this).load(avatarFile).circleCrop().into(avatar);
|
||||
} else {
|
||||
imageLoaded = false;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp)
|
||||
.asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
}
|
||||
avatar.setOnClickListener(view ->
|
||||
new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded)
|
||||
.show(this, avatar)
|
||||
);
|
||||
avatar.setOnClickListener(
|
||||
view ->
|
||||
new AvatarSelector(
|
||||
this,
|
||||
LoaderManager.getInstance(this),
|
||||
new AvatarSelectedListener(),
|
||||
imageLoaded)
|
||||
.show(this, avatar));
|
||||
}
|
||||
|
||||
private void initializeStatusText() {
|
||||
@@ -229,7 +228,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = CreateProfileActivity.this;
|
||||
Context context = CreateProfileActivity.this;
|
||||
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
|
||||
setStatusText();
|
||||
|
||||
@@ -253,7 +252,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
if (result) {
|
||||
attachmentManager.cleanup();
|
||||
finish();
|
||||
} else {
|
||||
} else {
|
||||
Toast.makeText(CreateProfileActivity.this, R.string.error, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
@@ -276,7 +275,10 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
avatarBmp = null;
|
||||
imageLoaded = false;
|
||||
avatarChanged = true;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(CreateProfileActivity.this, getResources().getColor(R.color.grey_400)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp)
|
||||
.asDrawable(
|
||||
CreateProfileActivity.this, getResources().getColor(R.color.grey_400)));
|
||||
break;
|
||||
case AvatarSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(CreateProfileActivity.this, REQUEST_CODE_AVATAR);
|
||||
|
||||
@@ -4,7 +4,8 @@ import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
// dummy activity that just pushes the app to foreground when fired.
|
||||
// can also be used to work around android bug https://code.google.com/p/android/issues/detail?id=53313
|
||||
// can also be used to work around android bug
|
||||
// https://code.google.com/p/android/issues/detail?id=53313
|
||||
public class DummyActivity extends Activity {
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
|
||||
@@ -6,99 +6,120 @@ import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class EphemeralMessagesDialog {
|
||||
|
||||
private final static String TAG = EphemeralMessagesDialog.class.getSimpleName();
|
||||
private static final String TAG = EphemeralMessagesDialog.class.getSimpleName();
|
||||
|
||||
public static void show(final Context context, int currentSelectedTime, final @NonNull EphemeralMessagesInterface listener) {
|
||||
CharSequence[] choices = context.getResources().getStringArray(R.array.ephemeral_message_durations);
|
||||
int preselected = getPreselection(currentSelectedTime);
|
||||
final int[] selectedChoice = new int[]{preselected};
|
||||
public static void show(
|
||||
final Context context,
|
||||
int currentSelectedTime,
|
||||
final @NonNull EphemeralMessagesInterface listener) {
|
||||
CharSequence[] choices =
|
||||
context.getResources().getStringArray(R.array.ephemeral_message_durations);
|
||||
int preselected = getPreselection(currentSelectedTime);
|
||||
final int[] selectedChoice = new int[] {preselected};
|
||||
|
||||
View dialogView = View.inflate(context, R.layout.dialog_extended_options, null);
|
||||
RadioGroup container = dialogView.findViewById(R.id.optionsContainer);
|
||||
for (CharSequence choice : choices) {
|
||||
View dialogView = View.inflate(context, R.layout.dialog_extended_options, null);
|
||||
RadioGroup container = dialogView.findViewById(R.id.optionsContainer);
|
||||
for (CharSequence choice : choices) {
|
||||
|
||||
RadioButton radioButton = new RadioButton(context);
|
||||
radioButton.setText(choice);
|
||||
TextViewCompat.setTextAppearance(radioButton, android.R.style.TextAppearance_Medium);
|
||||
RadioButton radioButton = new RadioButton(context);
|
||||
radioButton.setText(choice);
|
||||
TextViewCompat.setTextAppearance(radioButton, android.R.style.TextAppearance_Medium);
|
||||
|
||||
RadioGroup.LayoutParams params = new RadioGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(0, 0, 0, ViewUtil.dpToPx(context, 8));
|
||||
radioButton.setLayoutParams(params);
|
||||
container.addView(radioButton);
|
||||
}
|
||||
RadioGroup.LayoutParams params =
|
||||
new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(0, 0, 0, ViewUtil.dpToPx(context, 8));
|
||||
radioButton.setLayoutParams(params);
|
||||
container.addView(radioButton);
|
||||
}
|
||||
|
||||
container.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
int childCount = group.getChildCount();
|
||||
for (int x = 0; x < childCount; x++) {
|
||||
RadioButton btn = (RadioButton) group.getChildAt(x);
|
||||
if (btn.getId() == checkedId) {
|
||||
selectedChoice[0] = x;
|
||||
}
|
||||
container.setOnCheckedChangeListener(
|
||||
(group, checkedId) -> {
|
||||
int childCount = group.getChildCount();
|
||||
for (int x = 0; x < childCount; x++) {
|
||||
RadioButton btn = (RadioButton) group.getChildAt(x);
|
||||
if (btn.getId() == checkedId) {
|
||||
selectedChoice[0] = x;
|
||||
}
|
||||
}
|
||||
});
|
||||
container.check(container.getChildAt(preselected).getId());
|
||||
container.check(container.getChildAt(preselected).getId());
|
||||
|
||||
TextView messageView = dialogView.findViewById(R.id.description);
|
||||
messageView.setText(context.getString(R.string.ephemeral_messages_hint));
|
||||
TextView messageView = dialogView.findViewById(R.id.description);
|
||||
messageView.setText(context.getString(R.string.ephemeral_messages_hint));
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.ephemeral_messages)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
final long burnAfter;
|
||||
switch (selectedChoice[0]) {
|
||||
case 1: burnAfter = TimeUnit.MINUTES.toSeconds(5); break;
|
||||
case 2: burnAfter = TimeUnit.HOURS.toSeconds(1); break;
|
||||
case 3: burnAfter = TimeUnit.DAYS.toSeconds(1); break;
|
||||
case 4: burnAfter = TimeUnit.DAYS.toSeconds(7); break;
|
||||
case 5: burnAfter = TimeUnit.DAYS.toSeconds(35); break;
|
||||
case 6: burnAfter = TimeUnit.DAYS.toSeconds(365); break;
|
||||
default: burnAfter = 0; break;
|
||||
}
|
||||
listener.onTimeSelected(burnAfter);
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.ephemeral_messages)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(
|
||||
R.string.ok,
|
||||
(dialog, which) -> {
|
||||
final long burnAfter;
|
||||
switch (selectedChoice[0]) {
|
||||
case 1:
|
||||
burnAfter = TimeUnit.MINUTES.toSeconds(5);
|
||||
break;
|
||||
case 2:
|
||||
burnAfter = TimeUnit.HOURS.toSeconds(1);
|
||||
break;
|
||||
case 3:
|
||||
burnAfter = TimeUnit.DAYS.toSeconds(1);
|
||||
break;
|
||||
case 4:
|
||||
burnAfter = TimeUnit.DAYS.toSeconds(7);
|
||||
break;
|
||||
case 5:
|
||||
burnAfter = TimeUnit.DAYS.toSeconds(35);
|
||||
break;
|
||||
case 6:
|
||||
burnAfter = TimeUnit.DAYS.toSeconds(365);
|
||||
break;
|
||||
default:
|
||||
burnAfter = 0;
|
||||
break;
|
||||
}
|
||||
listener.onTimeSelected(burnAfter);
|
||||
})
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> DcHelper.openHelp(context, "#ephemeralmsgs"));
|
||||
builder.show();
|
||||
}
|
||||
.setNeutralButton(
|
||||
R.string.learn_more, (d, w) -> DcHelper.openHelp(context, "#ephemeralmsgs"));
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public interface EphemeralMessagesInterface {
|
||||
void onTimeSelected(long duration);
|
||||
}
|
||||
public interface EphemeralMessagesInterface {
|
||||
void onTimeSelected(long duration);
|
||||
}
|
||||
|
||||
private static int getPreselection(int timespan) {
|
||||
if (timespan == 0) {
|
||||
return 0; // off
|
||||
}
|
||||
// Choose timespan close to the current one out of available options.
|
||||
if (timespan < TimeUnit.HOURS.toSeconds(1)) {
|
||||
return 1; // 5 minutes
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(1)) {
|
||||
return 2; // 1 hour
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(7)) {
|
||||
return 3; // 1 day
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(35)) {
|
||||
return 4; // 1 week
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(365)) {
|
||||
return 5; // 5 weeks
|
||||
}
|
||||
return 6; // 1 year
|
||||
private static int getPreselection(int timespan) {
|
||||
if (timespan == 0) {
|
||||
return 0; // off
|
||||
}
|
||||
|
||||
// Choose timespan close to the current one out of available options.
|
||||
if (timespan < TimeUnit.HOURS.toSeconds(1)) {
|
||||
return 1; // 5 minutes
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(1)) {
|
||||
return 2; // 1 hour
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(7)) {
|
||||
return 3; // 1 day
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(35)) {
|
||||
return 4; // 1 week
|
||||
}
|
||||
if (timespan < TimeUnit.DAYS.toSeconds(365)) {
|
||||
return 5; // 5 weeks
|
||||
}
|
||||
return 6; // 1 year
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,23 @@ import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.types.HttpResponse;
|
||||
import com.b44t.messenger.DcContext;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.types.HttpResponse;
|
||||
|
||||
public class FullMsgActivity extends WebViewActivity
|
||||
{
|
||||
public class FullMsgActivity extends WebViewActivity {
|
||||
public static final String MSG_ID_EXTRA = "msg_id";
|
||||
public static final String BLOCK_LOADING_REMOTE = "block_loading_remote";
|
||||
private String imageUrl;
|
||||
@@ -91,7 +85,8 @@ public class FullMsgActivity extends WebViewActivity
|
||||
WebView.HitTestResult result = ((WebView) v).getHitTestResult();
|
||||
if (result != null) {
|
||||
int type = result.getType();
|
||||
if (type == WebView.HitTestResult.IMAGE_TYPE || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
if (type == WebView.HitTestResult.IMAGE_TYPE
|
||||
|| type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
imageUrl = result.getExtra();
|
||||
if (!imageUrl.startsWith("data:")) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
@@ -120,24 +115,30 @@ public class FullMsgActivity extends WebViewActivity
|
||||
}
|
||||
|
||||
private static void loadHtmlAsync(final WeakReference<FullMsgActivity> activityReference) {
|
||||
Util.runOnBackground(() -> {
|
||||
try {
|
||||
FullMsgActivity activity = activityReference.get();
|
||||
String html = activity.dcContext.getMsgHtml(activity.msgId);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
try {
|
||||
// a base URL is needed, otherwise clicking links that reference document sections will not jump to sections
|
||||
activityReference.get().webView.loadDataWithBaseURL("file://index.html", html, "text/html", null, null);
|
||||
FullMsgActivity activity = activityReference.get();
|
||||
String html = activity.dcContext.getMsgHtml(activity.msgId);
|
||||
|
||||
activity.runOnUiThread(
|
||||
() -> {
|
||||
try {
|
||||
// a base URL is needed, otherwise clicking links that reference document
|
||||
// sections will not jump to sections
|
||||
activityReference
|
||||
.get()
|
||||
.webView
|
||||
.loadDataWithBaseURL("file://index.html", html, "text/html", null, null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,17 +160,21 @@ public class FullMsgActivity extends WebViewActivity
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.load_remote_content) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.load_remote_content)
|
||||
.setMessage(R.string.load_remote_content_ask);
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.load_remote_content)
|
||||
.setMessage(R.string.load_remote_content_ask);
|
||||
|
||||
// we are using the buttons "[Always] [Never][Once]" in that order.
|
||||
// 1. Checkmarks before [Always] and [Never] show the current state.
|
||||
// 2. [Once] is also shown in always-mode and disables always-mode if selected
|
||||
// (there was the idea to hide [Once] in always mode, but that looks more like a bug in the end)
|
||||
// (maybe a usual Always-Checkbox and "[Cancel][OK]" buttons are an alternative, however, a [Once]
|
||||
// (there was the idea to hide [Once] in always mode, but that looks more like a bug in the
|
||||
// end)
|
||||
// (maybe a usual Always-Checkbox and "[Cancel][OK]" buttons are an alternative, however, a
|
||||
// [Once]
|
||||
// would be required as well - probably as the leftmost button which is not that usable in
|
||||
// not-always-mode where the dialog is used more often. Or [Ok] would mean "Once" as well as "Change checkbox setting",
|
||||
// not-always-mode where the dialog is used more often. Or [Ok] would mean "Once" as well as
|
||||
// "Change checkbox setting",
|
||||
// which is also a bit weird. Anyway, let's give the three buttons a try :)
|
||||
final String checkmark = DynamicTheme.getCheckmarkEmoji(this) + " ";
|
||||
String alwaysCheckmark = "";
|
||||
@@ -184,10 +189,16 @@ public class FullMsgActivity extends WebViewActivity
|
||||
}
|
||||
|
||||
if (!blockLoadingRemote) {
|
||||
builder.setNeutralButton(alwaysCheckmark + getString(R.string.always), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ALWAYS));
|
||||
builder.setNeutralButton(
|
||||
alwaysCheckmark + getString(R.string.always),
|
||||
(dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ALWAYS));
|
||||
}
|
||||
builder.setNegativeButton(neverCheckmark + getString(blockLoadingRemote ? R.string.no : R.string.never), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.NEVER));
|
||||
builder.setPositiveButton(onceCheckmark + getString(R.string.once), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ONCE));
|
||||
builder.setNegativeButton(
|
||||
neverCheckmark + getString(blockLoadingRemote ? R.string.no : R.string.never),
|
||||
(dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.NEVER));
|
||||
builder.setPositiveButton(
|
||||
onceCheckmark + getString(R.string.once),
|
||||
(dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ONCE));
|
||||
|
||||
builder.show();
|
||||
return true;
|
||||
@@ -234,10 +245,15 @@ public class FullMsgActivity extends WebViewActivity
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
byte[] blob = JsonUtils.decodeBase64(httpResponse.blob);
|
||||
res = new WebResourceResponse(mimeType, httpResponse.encoding, new ByteArrayInputStream(blob));
|
||||
res =
|
||||
new WebResourceResponse(mimeType, httpResponse.encoding, new ByteArrayInputStream(blob));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
res = new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes()));
|
||||
res =
|
||||
new WebResourceResponse(
|
||||
"text/plain",
|
||||
"UTF-8",
|
||||
new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes()));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -15,18 +16,20 @@ import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import org.thoughtcrime.securesms.components.AvatarSelector;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
@@ -40,16 +43,10 @@ import org.thoughtcrime.securesms.util.SelectedContactsAdapter.ItemClickListener
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ItemClickListener
|
||||
{
|
||||
implements ItemClickListener {
|
||||
|
||||
private static final String TAG = GroupCreateActivity.class.getSimpleName();
|
||||
public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id";
|
||||
public static final String CREATE_BROADCAST = "create_broadcast";
|
||||
public static final String UNENCRYPTED = "unencrypted";
|
||||
@@ -62,14 +59,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private boolean unencrypted;
|
||||
private boolean broadcast;
|
||||
private EditText groupName;
|
||||
private ListView lv;
|
||||
private ImageView avatar;
|
||||
private Bitmap avatarBmp;
|
||||
private int groupChatId;
|
||||
private boolean isEdit;
|
||||
private boolean avatarChanged;
|
||||
private boolean imageLoaded;
|
||||
private EditText groupName;
|
||||
private EditText chatDescription;
|
||||
private ListView lv;
|
||||
private ImageView avatar;
|
||||
private Bitmap avatarBmp;
|
||||
private int groupChatId;
|
||||
private boolean isEdit;
|
||||
private boolean avatarChanged;
|
||||
private boolean imageLoaded;
|
||||
private AttachmentManager attachmentManager;
|
||||
|
||||
@Override
|
||||
@@ -87,7 +85,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// groupChatId may be set during creation,
|
||||
// so always check isEdit()
|
||||
if(groupChatId !=0) {
|
||||
if (groupChatId != 0) {
|
||||
isEdit = true;
|
||||
DcChat dcChat = dcContext.getChat(groupChatId);
|
||||
broadcast = dcChat.isOutBroadcast();
|
||||
@@ -96,7 +94,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
int chatId = getIntent().getIntExtra(CLONE_CHAT_EXTRA, 0);
|
||||
if (chatId != 0) {
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
broadcast = dcChat.isOutBroadcast();
|
||||
unencrypted = !dcChat.isEncrypted();
|
||||
}
|
||||
@@ -121,26 +119,24 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
groupName.setEnabled(true);
|
||||
|
||||
String title;
|
||||
if(isEdit()) {
|
||||
if (isEdit()) {
|
||||
title = getString(R.string.global_menu_edit_desktop);
|
||||
}
|
||||
else if(broadcast) {
|
||||
} else if (broadcast) {
|
||||
title = getString(R.string.new_channel);
|
||||
}
|
||||
else if(unencrypted) {
|
||||
} else if (unencrypted) {
|
||||
title = getString(R.string.new_email);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
title = getString(R.string.menu_new_group);
|
||||
}
|
||||
getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
|
||||
avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
groupName = ViewUtil.findById(this, R.id.group_name);
|
||||
TextView chatHints = ViewUtil.findById(this, R.id.chat_hints);
|
||||
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
|
||||
avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
groupName = ViewUtil.findById(this, R.id.group_name);
|
||||
chatDescription = ViewUtil.findById(this, R.id.chat_description);
|
||||
TextView chatHints = ViewUtil.findById(this, R.id.chat_hints);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(lv, false, false, false, true);
|
||||
@@ -149,7 +145,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
initializeAvatarView();
|
||||
|
||||
SelectedContactsAdapter adapter = new SelectedContactsAdapter(this, GlideApp.with(this), broadcast);
|
||||
SelectedContactsAdapter adapter =
|
||||
new SelectedContactsAdapter(this, GlideApp.with(this), broadcast);
|
||||
adapter.setItemClickListener(this);
|
||||
lv.setAdapter(adapter);
|
||||
|
||||
@@ -178,14 +175,23 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
} else if (unencrypted) {
|
||||
avatar.setVisibility(View.GONE);
|
||||
groupName.setHint(R.string.subject);
|
||||
findViewById(R.id.chat_description_container).setVisibility(View.GONE);
|
||||
chatHints.setVisibility(View.GONE);
|
||||
} else {
|
||||
chatHints.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(isEdit()) {
|
||||
if (isEdit()) {
|
||||
groupName.setText(dcContext.getChat(groupChatId).getName());
|
||||
lv.setVisibility(View.GONE);
|
||||
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
try {
|
||||
String description = rpc.getChatDescription(rpc.getSelectedAccountId(), groupChatId);
|
||||
chatDescription.setText(description);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,19 +202,22 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
File avatarFile = new File(avatarPath);
|
||||
if (avatarFile.exists()) {
|
||||
imageLoaded = true;
|
||||
GlideApp.with(this)
|
||||
.load(avatarFile)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
GlideApp.with(this).load(avatarFile).circleCrop().into(avatar);
|
||||
}
|
||||
}
|
||||
if (!imageLoaded) {
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_white_24dp).asDrawable(this, ThemeUtil.getDummyContactColor(this)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_group_white_24dp)
|
||||
.asDrawable(this, ThemeUtil.getDummyContactColor(this)));
|
||||
}
|
||||
avatar.setOnClickListener(view ->
|
||||
new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded)
|
||||
.show(this, avatar)
|
||||
);
|
||||
avatar.setOnClickListener(
|
||||
view ->
|
||||
new AvatarSelector(
|
||||
this,
|
||||
LoaderManager.getInstance(this),
|
||||
new AvatarSelectedListener(),
|
||||
imageLoaded)
|
||||
.show(this, avatar));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,28 +270,31 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void createGroup(String groupName) {
|
||||
if (broadcast) {
|
||||
try {
|
||||
groupChatId = DcHelper.getRpc(this).createBroadcast(dcContext.getAccountId(), groupName);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
int accId;
|
||||
try {
|
||||
accId = rpc.getSelectedAccountId();
|
||||
if (broadcast) {
|
||||
groupChatId = rpc.createBroadcast(accId, groupName);
|
||||
} else if (unencrypted) {
|
||||
groupChatId = rpc.createGroupChatUnencrypted(accId, groupName);
|
||||
} else {
|
||||
groupChatId = rpc.createGroupChat(accId, groupName, false);
|
||||
}
|
||||
} else if (unencrypted) {
|
||||
try {
|
||||
groupChatId = DcHelper.getRpc(this).createGroupChatUnencrypted(dcContext.getAccountId(), groupName);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
|
||||
String description = getChatDescription();
|
||||
if (!description.isEmpty()) {
|
||||
rpc.setChatDescription(accId, groupChatId, description);
|
||||
}
|
||||
} else {
|
||||
groupChatId = dcContext.createGroupChat(groupName);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int contactId : getAdapter().getContacts()) {
|
||||
dcContext.addContactToChat(groupChatId, contactId);
|
||||
}
|
||||
if (avatarBmp!=null) {
|
||||
if (avatarBmp != null) {
|
||||
AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
|
||||
}
|
||||
|
||||
@@ -294,8 +306,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private boolean showGroupNameEmptyToast(String groupName) {
|
||||
if(groupName == null) {
|
||||
Toast.makeText(this, getString(R.string.group_please_enter_group_name), Toast.LENGTH_LONG).show();
|
||||
if (groupName == null) {
|
||||
Toast.makeText(this, getString(R.string.group_please_enter_group_name), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -307,6 +320,14 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
dcContext.setChatName(groupChatId, groupName);
|
||||
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
String description = getChatDescription();
|
||||
try {
|
||||
rpc.setChatDescription(rpc.getSelectedAccountId(), groupChatId, description);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC error", e);
|
||||
}
|
||||
|
||||
if (avatarChanged) AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
|
||||
|
||||
attachmentManager.cleanup();
|
||||
@@ -317,22 +338,27 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private SelectedContactsAdapter getAdapter() {
|
||||
return (SelectedContactsAdapter)lv.getAdapter();
|
||||
return (SelectedContactsAdapter) lv.getAdapter();
|
||||
}
|
||||
|
||||
private @Nullable String getGroupName() {
|
||||
String ret = groupName.getText() != null ? groupName.getText().toString() : null;
|
||||
if(ret!=null) {
|
||||
if (ret != null) {
|
||||
ret = ret.trim();
|
||||
if(ret.isEmpty()) {
|
||||
if (ret.isEmpty()) {
|
||||
ret = null;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private String getChatDescription() {
|
||||
return chatDescription.getText() != null ? chatDescription.getText().toString().trim() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
@@ -341,19 +367,20 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
|
||||
if (resultCode != Activity.RESULT_OK)
|
||||
return;
|
||||
if (resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
switch (reqCode) {
|
||||
case REQUEST_CODE_AVATAR:
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
onFileSelected(inputFile);
|
||||
break;
|
||||
|
||||
case PICK_CONTACT:
|
||||
ArrayList<Integer> contactIds = new ArrayList<>();
|
||||
for (Integer contactId : Objects.requireNonNull(data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA))) {
|
||||
if(contactId != null) {
|
||||
for (Integer contactId :
|
||||
Objects.requireNonNull(
|
||||
data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA))) {
|
||||
if (contactId != null) {
|
||||
contactIds.add(contactId);
|
||||
}
|
||||
}
|
||||
@@ -368,15 +395,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void setAvatarView(Uri output) {
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(
|
||||
new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
public void onResourceReady(
|
||||
@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
setAvatar(output, resource);
|
||||
}
|
||||
|
||||
@@ -390,18 +419,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
avatarChanged = true;
|
||||
imageLoaded = true;
|
||||
GlideApp.with(this)
|
||||
.load(model)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
.load(model)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
private boolean isEdit() {
|
||||
return isEdit;
|
||||
}
|
||||
|
||||
|
||||
private class AvatarSelectedListener implements AvatarSelector.AttachmentClickedListener {
|
||||
@Override
|
||||
public void onClick(int type) {
|
||||
@@ -413,7 +441,11 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
avatarBmp = null;
|
||||
imageLoaded = false;
|
||||
avatarChanged = true;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_white_24dp).asDrawable(GroupCreateActivity.this, ThemeUtil.getDummyContactColor(GroupCreateActivity.this)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_group_white_24dp)
|
||||
.asDrawable(
|
||||
GroupCreateActivity.this,
|
||||
ThemeUtil.getDummyContactColor(GroupCreateActivity.this)));
|
||||
break;
|
||||
case AvatarSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(GroupCreateActivity.this, REQUEST_CODE_AVATAR);
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_URL;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@@ -12,7 +11,6 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -24,12 +22,12 @@ import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcLot;
|
||||
@@ -38,7 +36,12 @@ import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.thoughtcrime.securesms.components.AvatarSelector;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -59,15 +62,8 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class InstantOnboardingActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
|
||||
public class InstantOnboardingActivity extends BaseActionBarActivity
|
||||
implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
private static final String TAG = InstantOnboardingActivity.class.getSimpleName();
|
||||
private static final String DCACCOUNT = "dcaccount";
|
||||
@@ -79,6 +75,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private ImageView avatar;
|
||||
private EditText name;
|
||||
private TextView invitationText;
|
||||
private TextView privacyPolicyBtn;
|
||||
private Button signUpBtn;
|
||||
|
||||
@@ -86,7 +83,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
private boolean imageLoaded;
|
||||
private String providerHost;
|
||||
private String providerQrData;
|
||||
private String rawQrData;
|
||||
private DcLot parsedQrData;
|
||||
private boolean isDcLogin;
|
||||
private boolean isContactInvitation;
|
||||
private boolean isGroupInvitation;
|
||||
|
||||
private AttachmentManager attachmentManager;
|
||||
private Bitmap avatarBmp;
|
||||
@@ -96,20 +97,24 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private DcContext dcContext;
|
||||
|
||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
setContentView(R.layout.instant_onboarding_activity);
|
||||
|
||||
Objects.requireNonNull(getSupportActionBar()).setTitle(R.string.onboarding_create_instant_account);
|
||||
Objects.requireNonNull(getSupportActionBar())
|
||||
.setTitle(R.string.onboarding_create_instant_account);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
boolean configured = DcHelper.getContext(this).isConfigured() == 1;
|
||||
boolean configured = DcHelper.getContext(this).isConfigured() == 1;
|
||||
|
||||
if (configured) {
|
||||
// if account is configured it means we didn't come from Welcome screen nor from QR scanner,
|
||||
// instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new relay
|
||||
// instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new
|
||||
// relay
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri != null) {
|
||||
Intent intent = new Intent(this, RelayListActivity.class);
|
||||
@@ -148,7 +153,8 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
proxyItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
} else {
|
||||
boolean proxyEnabled = DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1;
|
||||
proxyItem.setIcon(proxyEnabled? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24);
|
||||
proxyItem.setIcon(
|
||||
proxyEnabled ? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24);
|
||||
proxyItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
@@ -178,12 +184,12 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_AVATAR:
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
onFileSelected(inputFile);
|
||||
break;
|
||||
|
||||
@@ -206,22 +212,37 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private void setProviderFromQr(String rawQr) {
|
||||
DcLot qrParsed = dcContext.checkQr(rawQr);
|
||||
boolean isDcLogin = qrParsed.getState() == DcContext.DC_QR_LOGIN;
|
||||
if (isDcLogin || qrParsed.getState() == DcContext.DC_QR_ACCOUNT) {
|
||||
this.isDcLogin = isDcLogin;
|
||||
providerHost = qrParsed.getText1();
|
||||
providerQrData = rawQr;
|
||||
updateProvider();
|
||||
} else {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
switch (qrParsed.getState()) {
|
||||
case DcContext.DC_QR_LOGIN:
|
||||
isDcLogin = true; // Intentional fall-through
|
||||
case DcContext.DC_QR_ACCOUNT:
|
||||
providerHost = qrParsed.getText1();
|
||||
providerQrData = rawQr;
|
||||
updateProvider();
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_VERIFYCONTACT:
|
||||
isContactInvitation = true;
|
||||
rawQrData = rawQr;
|
||||
parsedQrData = qrParsed;
|
||||
updateProvider();
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_VERIFYGROUP:
|
||||
isGroupInvitation = true;
|
||||
rawQrData = rawQr;
|
||||
parsedQrData = qrParsed;
|
||||
updateProvider();
|
||||
break;
|
||||
default:
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
@@ -231,12 +252,15 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
super.onPause();
|
||||
|
||||
// Save display name and avatar in the unconfigured profile.
|
||||
// If the currently selected profile is configured, then this means that rollbackAccountCreation()
|
||||
// was called (see handleOnBackPressed() above), i.e. the newly created profile was removed already
|
||||
// If the currently selected profile is configured, then this means that
|
||||
// rollbackAccountCreation()
|
||||
// was called (see handleOnBackPressed() above), i.e. the newly created profile was removed
|
||||
// already
|
||||
// and we can't save the display name & avatar.
|
||||
if (DcHelper.getContext(this).isConfigured() == 0) {
|
||||
final String displayName = name.getText().toString();
|
||||
DcHelper.set(this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName) ? null : displayName);
|
||||
DcHelper.set(
|
||||
this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName) ? null : displayName);
|
||||
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
@@ -260,6 +284,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
private void handleIntent() {
|
||||
@@ -267,7 +292,8 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri == null) return;
|
||||
|
||||
if (uri.getScheme().equalsIgnoreCase(DCACCOUNT) || uri.getScheme().equalsIgnoreCase(DCLOGIN)) {
|
||||
if (uri.getScheme().equalsIgnoreCase(DCACCOUNT)
|
||||
|| uri.getScheme().equalsIgnoreCase(DCLOGIN)) {
|
||||
setProviderFromQr(uri.toString());
|
||||
}
|
||||
}
|
||||
@@ -275,29 +301,31 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
private void setAvatarView(Uri output) {
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
avatarChanged = true;
|
||||
imageLoaded = true;
|
||||
avatarBmp = resource;
|
||||
}
|
||||
.asBitmap()
|
||||
.load(output)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE)
|
||||
.into(
|
||||
new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(
|
||||
@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
avatarChanged = true;
|
||||
imageLoaded = true;
|
||||
avatarBmp = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {}
|
||||
});
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {}
|
||||
});
|
||||
GlideApp.with(this)
|
||||
.load(output)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
.load(output)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
private void onFileSelected(Uri inputFile) {
|
||||
@@ -309,34 +337,43 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.name = findViewById(R.id.name_text);
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.name = findViewById(R.id.name_text);
|
||||
this.invitationText = findViewById(R.id.invitation_label);
|
||||
this.privacyPolicyBtn = findViewById(R.id.privacy_policy_button);
|
||||
this.signUpBtn = findViewById(R.id.signup_button);
|
||||
this.signUpBtn = findViewById(R.id.signup_button);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.container));
|
||||
|
||||
privacyPolicyBtn.setOnClickListener(view -> {
|
||||
if (!isDcLogin) {
|
||||
IntentUtils.showInBrowser(this, "https://" + providerHost + "/privacy.html");
|
||||
}
|
||||
});
|
||||
privacyPolicyBtn.setOnClickListener(
|
||||
view -> {
|
||||
if (!isDcLogin) {
|
||||
IntentUtils.showInBrowser(this, "https://" + providerHost + "/privacy.html");
|
||||
}
|
||||
});
|
||||
|
||||
signUpBtn.setOnClickListener(view -> createProfile());
|
||||
|
||||
Button otherServerButton = findViewById(R.id.use_other_server);
|
||||
otherServerButton.setText(
|
||||
TextUtil.markAsExternal(getString(R.string.instant_onboarding_other_server)));
|
||||
otherServerButton.setOnClickListener((v) -> {
|
||||
IntentUtils.showInBrowser(this, INSTANCES_URL);
|
||||
});
|
||||
findViewById(R.id.login_button).setOnClickListener((v) -> {
|
||||
startActivity(new Intent(this, EditRelayActivity.class));
|
||||
});
|
||||
findViewById(R.id.scan_qr_button).setOnClickListener((v) -> {
|
||||
new IntentIntegrator(this).setCaptureActivity(RegistrationQrActivity.class).initiateScan();
|
||||
});
|
||||
TextUtil.markAsExternal(getString(R.string.instant_onboarding_other_server)));
|
||||
otherServerButton.setOnClickListener(
|
||||
(v) -> {
|
||||
IntentUtils.showInBrowser(this, INSTANCES_URL);
|
||||
});
|
||||
findViewById(R.id.login_button)
|
||||
.setOnClickListener(
|
||||
(v) -> {
|
||||
startActivity(new Intent(this, EditRelayActivity.class));
|
||||
});
|
||||
findViewById(R.id.scan_qr_button)
|
||||
.setOnClickListener(
|
||||
(v) -> {
|
||||
new IntentIntegrator(this)
|
||||
.setCaptureActivity(RegistrationQrActivity.class)
|
||||
.initiateScan();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateProvider() {
|
||||
@@ -347,16 +384,30 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
} else {
|
||||
signUpBtn.setText(R.string.instant_onboarding_create);
|
||||
|
||||
try (TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.colorAccent})) {
|
||||
try (TypedArray typedArray = obtainStyledAttributes(new int[] {R.attr.colorAccent})) {
|
||||
privacyPolicyBtn.setTextColor(typedArray.getColor(0, Color.BLACK));
|
||||
}
|
||||
|
||||
if (DEFAULT_CHATMAIL_HOST.equals(providerHost)) {
|
||||
privacyPolicyBtn.setText(TextUtil.markAsExternal(
|
||||
getString(R.string.instant_onboarding_agree_default2, providerHost)));
|
||||
privacyPolicyBtn.setText(
|
||||
TextUtil.markAsExternal(
|
||||
getString(R.string.instant_onboarding_agree_default2, providerHost)));
|
||||
} else {
|
||||
privacyPolicyBtn.setText(TextUtil.markAsExternal(
|
||||
getString(R.string.instant_onboarding_agree_instance, providerHost)));
|
||||
privacyPolicyBtn.setText(
|
||||
TextUtil.markAsExternal(
|
||||
getString(R.string.instant_onboarding_agree_instance, providerHost)));
|
||||
}
|
||||
|
||||
if (parsedQrData != null) {
|
||||
if (isContactInvitation) {
|
||||
String name = dcContext.getContact(parsedQrData.getId()).getDisplayName();
|
||||
invitationText.setText(this.getString(R.string.instant_onboarding_contact_info, name));
|
||||
invitationText.setVisibility(View.VISIBLE);
|
||||
} else if (isGroupInvitation) {
|
||||
String groupName = parsedQrData.getText1();
|
||||
invitationText.setText(this.getString(R.string.instant_onboarding_group_info, groupName));
|
||||
invitationText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,11 +419,18 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
GlideApp.with(this).load(avatarFile).circleCrop().into(avatar);
|
||||
} else {
|
||||
imageLoaded = false;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp)
|
||||
.asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
}
|
||||
avatar.setOnClickListener(view ->
|
||||
new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded).show(this, avatar)
|
||||
);
|
||||
avatar.setOnClickListener(
|
||||
view ->
|
||||
new AvatarSelector(
|
||||
this,
|
||||
LoaderManager.getInstance(this),
|
||||
new AvatarSelectedListener(),
|
||||
imageLoaded)
|
||||
.show(this, avatar));
|
||||
|
||||
name.setText(DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME));
|
||||
}
|
||||
@@ -389,14 +447,15 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
if (eventId == DcContext.DC_EVENT_CONFIGURE_PROGRESS) {
|
||||
long progress = event.getData1Int();
|
||||
progressUpdate((int)progress);
|
||||
progressUpdate((int) progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void progressUpdate(int progress) {
|
||||
int percent = progress / 10;
|
||||
if (progressDialog != null) {
|
||||
progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent));
|
||||
progressDialog.setMessage(
|
||||
getResources().getString(R.string.one_moment) + String.format(" %d%%", percent));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,11 +478,14 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
|
||||
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
|
||||
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
|
||||
if (isContactInvitation || isGroupInvitation) {
|
||||
intent.putExtra(ConversationListActivity.FROM_WELCOME_RAW_QR, rawQrData);
|
||||
}
|
||||
|
||||
startActivity(intent);
|
||||
finishAffinity();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void createProfile() {
|
||||
if (TextUtils.isEmpty(this.name.getText())) {
|
||||
Toast.makeText(this, R.string.please_enter_name, Toast.LENGTH_LONG).show();
|
||||
@@ -431,41 +493,38 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
final String name = this.name.getText().toString();
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = InstantOnboardingActivity.this;
|
||||
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
|
||||
executor.execute(
|
||||
() -> {
|
||||
Context context = InstantOnboardingActivity.this;
|
||||
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
|
||||
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
boolean result = true;
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(
|
||||
InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
if (result) {
|
||||
attachmentManager.cleanup();
|
||||
startQrAccountCreation(providerQrData);
|
||||
} else {
|
||||
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
boolean finalResult = result;
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
if (finalResult) {
|
||||
attachmentManager.cleanup();
|
||||
startQrAccountCreation(providerQrData);
|
||||
} else {
|
||||
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void startQrAccountCreation(String qrCode)
|
||||
{
|
||||
private void startQrAccountCreation(String qrCode) {
|
||||
if (progressDialog != null) {
|
||||
progressDialog.dismiss();
|
||||
progressDialog = null;
|
||||
@@ -477,27 +536,32 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
progressDialog.setMessage(getResources().getString(R.string.one_moment));
|
||||
progressDialog.setCanceledOnTouchOutside(false);
|
||||
progressDialog.setCancelable(false);
|
||||
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> {
|
||||
cancelled = true;
|
||||
dcContext.stopOngoingProcess();
|
||||
});
|
||||
progressDialog.setButton(
|
||||
DialogInterface.BUTTON_NEGATIVE,
|
||||
getResources().getString(android.R.string.cancel),
|
||||
(dialog, which) -> {
|
||||
cancelled = true;
|
||||
dcContext.stopOngoingProcess();
|
||||
});
|
||||
progressDialog.show();
|
||||
|
||||
DcHelper.getEventCenter(this).captureNextError();
|
||||
|
||||
new Thread(() -> {
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
try {
|
||||
rpc.addTransportFromQr(dcContext.getAccountId(), qrCode);
|
||||
DcHelper.getEventCenter(this).endCaptureNextError();
|
||||
progressSuccess();
|
||||
} catch (RpcException e) {
|
||||
DcHelper.getEventCenter(this).endCaptureNextError();
|
||||
if (!cancelled) {
|
||||
Util.runOnMain(() -> progressError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
new Thread(
|
||||
() -> {
|
||||
Rpc rpc = DcHelper.getRpc(this);
|
||||
try {
|
||||
rpc.addTransportFromQr(dcContext.getAccountId(), qrCode);
|
||||
DcHelper.getEventCenter(this).endCaptureNextError();
|
||||
progressSuccess();
|
||||
} catch (RpcException e) {
|
||||
DcHelper.getEventCenter(this).endCaptureNextError();
|
||||
if (!cancelled) {
|
||||
Util.runOnMain(() -> progressError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
private class AvatarSelectedListener implements AvatarSelector.AttachmentClickedListener {
|
||||
@@ -511,7 +575,10 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
avatarBmp = null;
|
||||
imageLoaded = false;
|
||||
avatarChanged = true;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(InstantOnboardingActivity.this, getResources().getColor(R.color.grey_400)));
|
||||
avatar.setImageDrawable(
|
||||
new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp)
|
||||
.asDrawable(
|
||||
InstantOnboardingActivity.this, getResources().getColor(R.color.grey_400)));
|
||||
break;
|
||||
case AvatarSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(InstantOnboardingActivity.this, REQUEST_CODE_AVATAR);
|
||||
@@ -524,5 +591,4 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
onFileSelected(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,21 +4,19 @@ import android.content.res.AssetManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
import org.thoughtcrime.securesms.util.TextUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocalHelpActivity extends WebViewActivity
|
||||
{
|
||||
public class LocalHelpActivity extends WebViewActivity {
|
||||
public static final String SECTION_EXTRA = "section_extra";
|
||||
|
||||
@Override
|
||||
protected boolean allowInLockedMode() { return true; }
|
||||
protected boolean allowInLockedMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
@@ -35,31 +33,37 @@ public class LocalHelpActivity extends WebViewActivity
|
||||
String appCountry = locale.getCountry();
|
||||
if (assetExists(helpPath.replace("LANG", appLang))) {
|
||||
helpLang = appLang;
|
||||
} else if (assetExists(helpPath.replace("LANG", appLang+"_"+appCountry))) {
|
||||
helpLang = appLang+"_"+appCountry;
|
||||
} else if (assetExists(helpPath.replace("LANG", appLang + "_" + appCountry))) {
|
||||
helpLang = appLang + "_" + appCountry;
|
||||
} else {
|
||||
appLang = appLang.substring(0, 2);
|
||||
if (assetExists(helpPath.replace("LANG", appLang))) {
|
||||
helpLang = appLang;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
getOnBackPressedDispatcher()
|
||||
.addCallback(
|
||||
this,
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
setEnabled(false);
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl("file:///android_asset/" + helpPath.replace("LANG", helpLang) + (section!=null? section : ""));
|
||||
webView.loadUrl(
|
||||
"file:///android_asset/"
|
||||
+ helpPath.replace("LANG", helpLang)
|
||||
+ (section != null ? section : ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,7 +128,7 @@ public class LocalHelpActivity extends WebViewActivity
|
||||
InputStream is = assetManager.open(fileName);
|
||||
exists = true;
|
||||
is.close();
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
; // a non-existent asset is no error, the function's purpose is to check exactly that.
|
||||
}
|
||||
return exists;
|
||||
|
||||
@@ -9,16 +9,13 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import java.io.File;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class LogViewActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = LogViewActivity.class.getSimpleName();
|
||||
@@ -60,18 +57,20 @@ public class LogViewActivity extends BaseActionBarActivity {
|
||||
return true;
|
||||
} else if (itemId == R.id.save_log) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.alwaysGrantOnSdk30()
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
File outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
boolean success = logViewFragment.saveLogFile(outputDir) != null;
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(success ? R.string.pref_saved_log : R.string.pref_save_log_failed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
})
|
||||
.execute();
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.alwaysGrantOnSdk30()
|
||||
.ifNecessary()
|
||||
.onAllGranted(
|
||||
() -> {
|
||||
File outputDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
boolean success = logViewFragment.saveLogFile(outputDir) != null;
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(success ? R.string.pref_saved_log : R.string.pref_save_log_failed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
})
|
||||
.execute();
|
||||
return true;
|
||||
} else if (itemId == R.id.share_log) {
|
||||
shareLog();
|
||||
@@ -110,7 +109,8 @@ public class LogViewActivity extends BaseActionBarActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,17 +32,11 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.PermissionChecker;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
@@ -53,15 +47,16 @@ import java.lang.ref.WeakReference;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class LogViewFragment extends Fragment {
|
||||
private EditText logPreview;
|
||||
|
||||
public LogViewFragment() {
|
||||
}
|
||||
public LogViewFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@@ -69,8 +64,8 @@ public class LogViewFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_view_log, container, false);
|
||||
}
|
||||
|
||||
@@ -78,19 +73,22 @@ public class LogViewFragment extends Fragment {
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
logPreview = (EditText) getView().findViewById(R.id.log_preview);
|
||||
logPreview = (EditText) getView().findViewById(R.id.log_preview);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(getView().findViewById(R.id.content_container), true, false, true, true);
|
||||
ViewUtil.applyWindowInsets(
|
||||
getView().findViewById(R.id.content_container), true, false, true, true);
|
||||
|
||||
new PopulateLogcatAsyncTask(this).execute();
|
||||
}
|
||||
|
||||
public String getLogText() {
|
||||
return logPreview==null? "null" : logPreview.getText().toString();
|
||||
return logPreview == null ? "null" : logPreview.getText().toString();
|
||||
}
|
||||
|
||||
public Float getLogTextSize() { return logPreview.getTextSize(); }
|
||||
public Float getLogTextSize() {
|
||||
return logPreview.getTextSize();
|
||||
}
|
||||
|
||||
public void setLogTextSize(Float textSize) {
|
||||
logPreview.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
|
||||
@@ -107,16 +105,16 @@ public class LogViewFragment extends Fragment {
|
||||
}
|
||||
|
||||
public File saveLogFile(File outputDir) {
|
||||
File logFile = null;
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
Date now = new Date();
|
||||
String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt";
|
||||
File logFile = null;
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||
Date now = new Date();
|
||||
String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt";
|
||||
|
||||
try {
|
||||
String logText = logPreview.getText().toString();
|
||||
if(!logText.trim().equals("")){
|
||||
String logText = logPreview.getText().toString();
|
||||
if (!logText.trim().equals("")) {
|
||||
logFile = new File(outputDir + "/" + logFileName);
|
||||
if(!logFile.exists()) logFile.createNewFile();
|
||||
if (!logFile.exists()) logFile.createNewFile();
|
||||
|
||||
FileWriter logFileWriter = new FileWriter(logFile, false);
|
||||
BufferedWriter logFileBufferWriter = new BufferedWriter(logFileWriter);
|
||||
@@ -129,13 +127,14 @@ public class LogViewFragment extends Fragment {
|
||||
return logFile;
|
||||
}
|
||||
|
||||
private static String grabLogcat(LogViewFragment fragment) {
|
||||
public static String grabLogcat() {
|
||||
String command = "logcat -v threadtime -d -t 10000 *:I";
|
||||
try {
|
||||
final Process process = Runtime.getRuntime().exec(command);
|
||||
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
final StringBuilder log = new StringBuilder();
|
||||
final String separator = System.getProperty("line.separator");
|
||||
final Process process = Runtime.getRuntime().exec(command);
|
||||
final BufferedReader bufferedReader =
|
||||
new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
final StringBuilder log = new StringBuilder();
|
||||
final String separator = System.getProperty("line.separator");
|
||||
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
@@ -152,7 +151,7 @@ public class LogViewFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private class PopulateLogcatAsyncTask extends AsyncTask<Void,Void,String> {
|
||||
private class PopulateLogcatAsyncTask extends AsyncTask<Void, Void, String> {
|
||||
private final WeakReference<LogViewFragment> weakFragment;
|
||||
|
||||
public PopulateLogcatAsyncTask(LogViewFragment fragment) {
|
||||
@@ -164,8 +163,10 @@ public class LogViewFragment extends Fragment {
|
||||
LogViewFragment fragment = weakFragment.get();
|
||||
if (fragment == null) return null;
|
||||
|
||||
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" +
|
||||
buildDescription(fragment) + "\n" + grabLogcat(fragment);
|
||||
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n"
|
||||
+ buildDescription(fragment)
|
||||
+ "\n"
|
||||
+ grabLogcat();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,7 +180,8 @@ public class LogViewFragment extends Fragment {
|
||||
super.onPostExecute(logcat);
|
||||
if (TextUtils.isEmpty(logcat)) {
|
||||
// the log is in english, so it is fine if some of explaining strings are in english as well
|
||||
logPreview.setText("Could not read the log on your device. You can still use ADB to get a debug log instead.");
|
||||
logPreview.setText(
|
||||
"Could not read the log on your device. You can still use ADB to get a debug log instead.");
|
||||
return;
|
||||
}
|
||||
logPreview.setText(logcat);
|
||||
@@ -192,15 +194,18 @@ public class LogViewFragment extends Fragment {
|
||||
|
||||
public static String getMemoryUsage(Context context) {
|
||||
Runtime info = Runtime.getRuntime();
|
||||
return String.format(Locale.ENGLISH, "%dM (%.2f%% free, %dM max)",
|
||||
asMegs(info.totalMemory()),
|
||||
(float)info.freeMemory() / info.totalMemory() * 100f,
|
||||
asMegs(info.maxMemory()));
|
||||
return String.format(
|
||||
Locale.ENGLISH,
|
||||
"%dM (%.2f%% free, %dM max)",
|
||||
asMegs(info.totalMemory()),
|
||||
(float) info.freeMemory() / info.totalMemory() * 100f,
|
||||
asMegs(info.maxMemory()));
|
||||
}
|
||||
|
||||
public static String getMemoryClass(Context context) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
String lowMem = "";
|
||||
ActivityManager activityManager =
|
||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
String lowMem = "";
|
||||
|
||||
if (activityManager.isLowRamDevice()) {
|
||||
lowMem = ", low-mem device";
|
||||
@@ -211,17 +216,26 @@ public class LogViewFragment extends Fragment {
|
||||
private static String buildDescription(LogViewFragment fragment) {
|
||||
Context context = fragment.getActivity();
|
||||
|
||||
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("device=")
|
||||
.append(Build.MANUFACTURER).append(" ")
|
||||
.append(Build.MODEL).append(" (")
|
||||
.append(Build.PRODUCT).append(")\n");
|
||||
builder.append("android=").append(VERSION.RELEASE).append(" (")
|
||||
.append(VERSION.INCREMENTAL).append(", ")
|
||||
.append(Build.DISPLAY).append(")\n");
|
||||
builder
|
||||
.append("device=")
|
||||
.append(Build.MANUFACTURER)
|
||||
.append(" ")
|
||||
.append(Build.MODEL)
|
||||
.append(" (")
|
||||
.append(Build.PRODUCT)
|
||||
.append(")\n");
|
||||
builder
|
||||
.append("android=")
|
||||
.append(VERSION.RELEASE)
|
||||
.append(" (")
|
||||
.append(VERSION.INCREMENTAL)
|
||||
.append(", ")
|
||||
.append(Build.DISPLAY)
|
||||
.append(")\n");
|
||||
builder.append("sdk=").append(Build.VERSION.SDK_INT).append("\n");
|
||||
builder.append("memory=").append(getMemoryUsage(context)).append("\n");
|
||||
builder.append("memoryClass=").append(getMemoryClass(context)).append("\n");
|
||||
@@ -229,25 +243,29 @@ public class LogViewFragment extends Fragment {
|
||||
builder.append("applicationId=").append(BuildConfig.APPLICATION_ID).append("\n");
|
||||
builder.append("app=");
|
||||
try {
|
||||
builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0)))
|
||||
.append(" ")
|
||||
.append(pm.getPackageInfo(context.getPackageName(), 0).versionName)
|
||||
.append("-")
|
||||
.append(BuildConfig.FLAVOR)
|
||||
.append(BuildConfig.DEBUG? "-debug" : "")
|
||||
.append("\n");
|
||||
builder.append("versionCode=")
|
||||
.append(pm.getPackageInfo(context.getPackageName(), 0).versionCode)
|
||||
.append("\n");
|
||||
builder.append("installer=")
|
||||
.append(pm.getInstallerPackageName(context.getPackageName()))
|
||||
.append("\n");
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
builder.append("ignoreBatteryOptimizations=").append(
|
||||
powerManager.isIgnoringBatteryOptimizations(context.getPackageName())).append("\n");
|
||||
builder
|
||||
.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0)))
|
||||
.append(" ")
|
||||
.append(pm.getPackageInfo(context.getPackageName(), 0).versionName)
|
||||
.append("-")
|
||||
.append(BuildConfig.FLAVOR)
|
||||
.append(BuildConfig.DEBUG ? "-debug" : "")
|
||||
.append("\n");
|
||||
builder
|
||||
.append("versionCode=")
|
||||
.append(pm.getPackageInfo(context.getPackageName(), 0).versionCode)
|
||||
.append("\n");
|
||||
builder
|
||||
.append("installer=")
|
||||
.append(pm.getInstallerPackageName(context.getPackageName()))
|
||||
.append("\n");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
builder
|
||||
.append("ignoreBatteryOptimizations=")
|
||||
.append(powerManager.isIgnoringBatteryOptimizations(context.getPackageName()))
|
||||
.append("\n");
|
||||
}
|
||||
builder.append("reliableService=").append(
|
||||
Prefs.reliableService(context)).append("\n");
|
||||
builder.append("reliableService=").append(Prefs.reliableService(context)).append("\n");
|
||||
|
||||
Locale locale = Util.getLocale();
|
||||
builder.append("lang=").append(locale.toString()).append("\n");
|
||||
@@ -255,7 +273,9 @@ public class LogViewFragment extends Fragment {
|
||||
builder.append("rtl=").append(isRtl).append("\n");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
boolean notifPermGranted = PermissionChecker.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PermissionChecker.PERMISSION_GRANTED;
|
||||
boolean notifPermGranted =
|
||||
PermissionChecker.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PermissionChecker.PERMISSION_GRANTED;
|
||||
builder.append("post-notifications-granted=").append(notifPermGranted).append("\n");
|
||||
} else {
|
||||
builder.append("post-notifications-granted=<not needed>").append("\n");
|
||||
|
||||
@@ -34,7 +34,6 @@ import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -42,12 +41,12 @@ import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMediaGalleryElement;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.WeakHashMap;
|
||||
import org.thoughtcrime.securesms.components.MediaView;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -66,24 +65,19 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
/** Activity for displaying media attachments in-app */
|
||||
public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
implements RecipientModifiedListener, LoaderManager.LoaderCallbacks<DcMediaGalleryElement> {
|
||||
|
||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
private static final String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
|
||||
public static final String ACTIVITY_TITLE_EXTRA = "activity_title";
|
||||
public static final String EDIT_AVATAR_CHAT_ID = "avatar_for_chat_id";
|
||||
public static final String ADDRESS_EXTRA = "address";
|
||||
public static final String OUTGOING_EXTRA = "outgoing";
|
||||
public static final String EDIT_AVATAR_CHAT_ID = "avatar_for_chat_id";
|
||||
public static final String ADDRESS_EXTRA = "address";
|
||||
public static final String OUTGOING_EXTRA = "outgoing";
|
||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
public static final String DC_MSG_ID = "dc_msg_id";
|
||||
public static final String OPENED_FROM_PROFILE = "opened_from_profile";
|
||||
public static final String DC_MSG_ID = "dc_msg_id";
|
||||
public static final String OPENED_FROM_PROFILE = "opened_from_profile";
|
||||
|
||||
/** USE ONLY IF YOU HAVE NO MESSAGE ID! */
|
||||
public static final String DATE_EXTRA = "date";
|
||||
@@ -91,13 +85,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
/** USE ONLY IF YOU HAVE NO MESSAGE ID! */
|
||||
public static final String SIZE_EXTRA = "size";
|
||||
|
||||
@Nullable
|
||||
private DcMsg messageRecord;
|
||||
@Nullable private DcMsg messageRecord;
|
||||
private DcContext dcContext;
|
||||
private MediaItem initialMedia;
|
||||
private ViewPager mediaPager;
|
||||
private Recipient conversationRecipient;
|
||||
private boolean leftIsRecent;
|
||||
private boolean leftIsRecent;
|
||||
|
||||
private int restartItem = -1;
|
||||
|
||||
@@ -105,12 +98,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme = new DynamicTheme() {
|
||||
public void onCreate(Activity activity) {
|
||||
activity.setTheme(R.style.TextSecure_DarkTheme); // force dark theme
|
||||
}
|
||||
public void onResume(Activity activity) {}
|
||||
};
|
||||
dynamicTheme =
|
||||
new DynamicTheme() {
|
||||
public void onCreate(Activity activity) {
|
||||
activity.setTheme(R.style.TextSecure_DarkTheme); // force dark theme
|
||||
}
|
||||
|
||||
public void onResume(Activity activity) {}
|
||||
};
|
||||
super.onPreCreate();
|
||||
}
|
||||
|
||||
@@ -118,15 +113,16 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setFullscreenIfPossible();
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow()
|
||||
.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setContentView(R.layout.media_preview_activity);
|
||||
|
||||
editAvatarChatId = getIntent().getIntExtra(EDIT_AVATAR_CHAT_ID, 0);
|
||||
@Nullable String title = getIntent().getStringExtra(ACTIVITY_TITLE_EXTRA);
|
||||
if (title!=null) {
|
||||
if (title != null) {
|
||||
getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
||||
@@ -135,7 +131,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@@ -161,8 +158,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
relativeTimeSpan = getString(R.string.draft);
|
||||
}
|
||||
|
||||
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.self));
|
||||
else {
|
||||
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.self));
|
||||
else {
|
||||
int fromId = dcContext.getMsg(mediaItem.msgId).getFromId();
|
||||
getSupportActionBar().setTitle(dcContext.getContact(fromId).getDisplayName());
|
||||
}
|
||||
@@ -203,12 +200,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
this.dcContext = DcHelper.getContext(context);
|
||||
final int msgId = getIntent().getIntExtra(DC_MSG_ID, DcMsg.DC_MSG_NO_ID);
|
||||
|
||||
if(msgId == DcMsg.DC_MSG_NO_ID) {
|
||||
if (msgId == DcMsg.DC_MSG_NO_ID) {
|
||||
messageRecord = null;
|
||||
long date = getIntent().getLongExtra(DATE_EXTRA, 0);
|
||||
long size = getIntent().getLongExtra(SIZE_EXTRA, 0);
|
||||
initialMedia = new MediaItem(null, getIntent().getData(), null, getIntent().getType(),
|
||||
DcMsg.DC_MSG_NO_ID, date, size, false);
|
||||
initialMedia =
|
||||
new MediaItem(
|
||||
null,
|
||||
getIntent().getData(),
|
||||
null,
|
||||
getIntent().getType(),
|
||||
DcMsg.DC_MSG_NO_ID,
|
||||
date,
|
||||
size,
|
||||
false);
|
||||
|
||||
if (address != null) {
|
||||
conversationRecipient = Recipient.from(context, address);
|
||||
@@ -217,14 +222,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
} else {
|
||||
messageRecord = dcContext.getMsg(msgId);
|
||||
initialMedia = new MediaItem(Recipient.fromChat(context, msgId), Uri.fromFile(messageRecord.getFileAsFile()),
|
||||
messageRecord.getFilename(), messageRecord.getFilemime(), messageRecord.getId(), messageRecord.getDateReceived(),
|
||||
messageRecord.getFilebytes(), messageRecord.isOutgoing());
|
||||
initialMedia =
|
||||
new MediaItem(
|
||||
Recipient.fromChat(context, msgId),
|
||||
Uri.fromFile(messageRecord.getFileAsFile()),
|
||||
messageRecord.getFilename(),
|
||||
messageRecord.getFilemime(),
|
||||
messageRecord.getId(),
|
||||
messageRecord.getDateReceived(),
|
||||
messageRecord.getFilebytes(),
|
||||
messageRecord.isOutgoing());
|
||||
conversationRecipient = Recipient.fromChat(context, msgId);
|
||||
}
|
||||
leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
|
||||
leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
private void initializeMedia() {
|
||||
@@ -235,8 +246,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
if (messageRecord != null) {
|
||||
getSupportLoaderManager().restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this),
|
||||
getWindow(), initialMedia.uri, initialMedia.name, initialMedia.type, initialMedia.size));
|
||||
mediaPager.setAdapter(
|
||||
new SingleItemPagerAdapter(
|
||||
this,
|
||||
GlideApp.with(this),
|
||||
getWindow(),
|
||||
initialMedia.uri,
|
||||
initialMedia.name,
|
||||
initialMedia.type,
|
||||
initialMedia.size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,21 +274,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
finish(); // avoid the need to update the enlarged-avatar
|
||||
}
|
||||
|
||||
|
||||
private void showOverview() {
|
||||
if (getIntent().getBooleanExtra(OPENED_FROM_PROFILE, false)) {
|
||||
finish();
|
||||
}
|
||||
else if(conversationRecipient.getAddress().isDcChat()) {
|
||||
} else if (conversationRecipient.getAddress().isDcChat()) {
|
||||
Intent intent = new Intent(this, AllMediaActivity.class);
|
||||
intent.putExtra(AllMediaActivity.CHAT_ID_EXTRA, conversationRecipient.getAddress().getDcChatId());
|
||||
intent.putExtra(
|
||||
AllMediaActivity.CHAT_ID_EXTRA, conversationRecipient.getAddress().getDcChatId());
|
||||
intent.putExtra(AllMediaActivity.FORCE_GALLERY, true);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
else if(conversationRecipient.getAddress().isDcContact()) {
|
||||
} else if (conversationRecipient.getAddress().isDcContact()) {
|
||||
Intent intent = new Intent(this, AllMediaActivity.class);
|
||||
intent.putExtra(AllMediaActivity.CONTACT_ID_EXTRA, conversationRecipient.getAddress().getDcContactId());
|
||||
intent.putExtra(
|
||||
AllMediaActivity.CONTACT_ID_EXTRA, conversationRecipient.getAddress().getDcContactId());
|
||||
intent.putExtra(AllMediaActivity.FORCE_GALLERY, true);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
@@ -290,29 +307,35 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore(this)) {
|
||||
performSavetoDisk(mediaItem);
|
||||
return;
|
||||
}
|
||||
SaveAttachmentTask.showWarningDialog(
|
||||
this,
|
||||
(dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore(this)) {
|
||||
performSavetoDisk(mediaItem);
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.alwaysGrantOnSdk30()
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||
.onAllGranted(() -> {
|
||||
performSavetoDisk(mediaItem);
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.alwaysGrantOnSdk30()
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.perm_explain_access_to_storage_denied))
|
||||
.onAllGranted(
|
||||
() -> {
|
||||
performSavetoDisk(mediaItem);
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, mediaItem.name));
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(
|
||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
new Attachment(mediaItem.uri, mediaItem.type, saveDate, mediaItem.name));
|
||||
}
|
||||
|
||||
private void showInChat() {
|
||||
@@ -330,7 +353,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId());
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, dcContext));
|
||||
intent.putExtra(
|
||||
ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, dcContext));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@@ -344,24 +368,28 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
DcMsg dcMsg = dcContext.getMsg(mediaItem.msgId);
|
||||
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
||||
|
||||
String text = getResources().getQuantityString(R.plurals.ask_delete_messages,1, 1);
|
||||
String text = getResources().getQuantityString(R.plurals.ask_delete_messages, 1, 1);
|
||||
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
|
||||
final int[] messageIds = new int[]{mediaItem.msgId};
|
||||
final int[] messageIds = new int[] {mediaItem.msgId};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(text);
|
||||
builder.setCancelable(true);
|
||||
builder.setNeutralButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(positiveBtnLabel, (dialogInterface, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
finish();
|
||||
});
|
||||
builder.setPositiveButton(
|
||||
positiveBtnLabel,
|
||||
(dialogInterface, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
finish();
|
||||
});
|
||||
|
||||
if(dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk() && dcMsg.isOutgoing()) {
|
||||
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
|
||||
finish();
|
||||
});
|
||||
if (dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk() && dcMsg.isOutgoing()) {
|
||||
builder.setNegativeButton(
|
||||
R.string.delete_for_everyone,
|
||||
(d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
|
||||
finish();
|
||||
});
|
||||
AlertDialog dialog = builder.show();
|
||||
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
|
||||
Util.redPositiveButton(dialog);
|
||||
@@ -387,8 +415,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
menu.findItem(R.id.show_in_chat).setVisible(false);
|
||||
}
|
||||
|
||||
if (editAvatarChatId==0) {
|
||||
menu.findItem(R.id.media_preview__edit).setVisible(false);
|
||||
if (editAvatarChatId == 0) {
|
||||
menu.findItem(R.id.media_preview__edit).setVisible(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -430,7 +458,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private @Nullable MediaItem getCurrentMediaItem() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
||||
@@ -449,23 +477,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<DcMediaGalleryElement> loader, @Nullable DcMediaGalleryElement data) {
|
||||
public void onLoadFinished(
|
||||
Loader<DcMediaGalleryElement> loader, @Nullable DcMediaGalleryElement data) {
|
||||
if (data != null) {
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
DcMediaPagerAdapter adapter = new DcMediaPagerAdapter(this, GlideApp.with(this),
|
||||
getWindow(), data, leftIsRecent);
|
||||
DcMediaPagerAdapter adapter =
|
||||
new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition());
|
||||
else mediaPager.setCurrentItem(restartItem);
|
||||
else mediaPager.setCurrentItem(restartItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<DcMediaGalleryElement> loader) {
|
||||
|
||||
}
|
||||
public void onLoaderReset(Loader<DcMediaGalleryElement> loader) {}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
|
||||
@@ -473,7 +500,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
@@ -483,10 +510,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPageUnselected(int position) {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
try {
|
||||
@@ -503,25 +529,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final Window window;
|
||||
private final Uri uri;
|
||||
private final String name;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
private final Window window;
|
||||
private final Uri uri;
|
||||
private final String name;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
SingleItemPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull Window window, @NonNull Uri uri, @Nullable String name, @NonNull String mediaType,
|
||||
long size)
|
||||
{
|
||||
SingleItemPagerAdapter(
|
||||
@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Window window,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String name,
|
||||
@NonNull String mediaType,
|
||||
long size) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.window = window;
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.window = window;
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -536,7 +566,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
|
||||
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
|
||||
try {
|
||||
@@ -552,10 +582,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view);
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
container.removeView((FrameLayout)object);
|
||||
container.removeView((FrameLayout) object);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -564,33 +594,33 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(int position) {
|
||||
|
||||
}
|
||||
public void pause(int position) {}
|
||||
}
|
||||
|
||||
private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
|
||||
|
||||
private final Context context;
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Window window;
|
||||
private final Window window;
|
||||
private final DcMediaGalleryElement gallery;
|
||||
private final boolean leftIsRecent;
|
||||
private final boolean leftIsRecent;
|
||||
|
||||
private boolean active;
|
||||
private int autoPlayPosition;
|
||||
private int autoPlayPosition;
|
||||
|
||||
DcMediaPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull Window window, @NonNull DcMediaGalleryElement gallery,
|
||||
boolean leftIsRecent)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.glideRequests = glideRequests;
|
||||
this.window = window;
|
||||
this.gallery = gallery;
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
DcMediaPagerAdapter(
|
||||
@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Window window,
|
||||
@NonNull DcMediaGalleryElement gallery,
|
||||
boolean leftIsRecent) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.glideRequests = glideRequests;
|
||||
this.window = window;
|
||||
this.gallery = gallery;
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
this.autoPlayPosition = gallery.getPosition();
|
||||
}
|
||||
|
||||
@@ -602,7 +632,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (!active) return 0;
|
||||
else return gallery.getCount();
|
||||
else return gallery.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -612,10 +642,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView = LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
boolean autoplay = position == autoPlayPosition;
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
View itemView =
|
||||
LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
boolean autoplay = position == autoPlayPosition;
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
autoPlayPosition = -1;
|
||||
|
||||
@@ -625,8 +656,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
mediaView.set(glideRequests, window, Uri.fromFile(msg.getFileAsFile()), msg.getFilename(),
|
||||
msg.getFilemime(), msg.getFilebytes(), autoplay);
|
||||
mediaView.set(
|
||||
glideRequests,
|
||||
window,
|
||||
Uri.fromFile(msg.getFileAsFile()),
|
||||
msg.getFilename(),
|
||||
msg.getFilemime(),
|
||||
msg.getFilebytes(),
|
||||
autoplay);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
@@ -639,27 +676,28 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view);
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
mediaViews.remove(position);
|
||||
container.removeView((FrameLayout)object);
|
||||
container.removeView((FrameLayout) object);
|
||||
}
|
||||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
gallery.moveToPosition(getCursorPosition(position));
|
||||
DcMsg msg = gallery.getMessage();
|
||||
DcMsg msg = gallery.getMessage();
|
||||
|
||||
if (msg.getFile() == null) throw new AssertionError();
|
||||
|
||||
return new MediaItem(Recipient.fromChat(context, msg.getId()),
|
||||
Uri.fromFile(msg.getFileAsFile()),
|
||||
msg.getFilename(),
|
||||
msg.getFilemime(),
|
||||
msg.getId(),
|
||||
msg.getDateReceived(),
|
||||
msg.getFilebytes(),
|
||||
msg.isOutgoing());
|
||||
return new MediaItem(
|
||||
Recipient.fromChat(context, msg.getId()),
|
||||
Uri.fromFile(msg.getFileAsFile()),
|
||||
msg.getFilename(),
|
||||
msg.getFilemime(),
|
||||
msg.getId(),
|
||||
msg.getDateReceived(),
|
||||
msg.getFilebytes(),
|
||||
msg.isOutgoing());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -670,42 +708,43 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
if (leftIsRecent) return position;
|
||||
else return gallery.getCount() - 1 - position;
|
||||
else return gallery.getCount() - 1 - position;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MediaItem {
|
||||
private final @Nullable Recipient recipient;
|
||||
private final @NonNull Uri uri;
|
||||
private final @Nullable String name;
|
||||
private final @NonNull String type;
|
||||
private final int msgId;
|
||||
private final long date;
|
||||
private final long size;
|
||||
private final boolean outgoing;
|
||||
private final @Nullable Recipient recipient;
|
||||
private final @NonNull Uri uri;
|
||||
private final @Nullable String name;
|
||||
private final @NonNull String type;
|
||||
private final int msgId;
|
||||
private final long date;
|
||||
private final long size;
|
||||
private final boolean outgoing;
|
||||
|
||||
private MediaItem(@Nullable Recipient recipient,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String name,
|
||||
@NonNull String type,
|
||||
int msgId,
|
||||
long date,
|
||||
long size,
|
||||
boolean outgoing)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.msgId = msgId;
|
||||
this.date = date;
|
||||
this.size = size;
|
||||
this.outgoing = outgoing;
|
||||
private MediaItem(
|
||||
@Nullable Recipient recipient,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String name,
|
||||
@NonNull String type,
|
||||
int msgId,
|
||||
long date,
|
||||
long size,
|
||||
boolean outgoing) {
|
||||
this.recipient = recipient;
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.msgId = msgId;
|
||||
this.date = date;
|
||||
this.size = size;
|
||||
this.outgoing = outgoing;
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
|
||||
void pause(int position);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user