Compare commits

...

90 Commits

Author SHA1 Message Date
adbenitez d68c32ff26 fix ProgressDialog 2026-03-11 20:16:29 +01:00
adbenitez b67e9d795e Merge remote-tracking branch 'upstream/main' into adb/test 2026-03-11 20:02:57 +01:00
adbenitez ae7be8a841 apply code formatter 2026-03-11 18:53:16 +01:00
adb 1894425ad2 Merge pull request #4271 from deltachat/adb/apply-code-format4
apply code formatter, part 4
2026-03-11 17:29:32 +01:00
adbenitez 3460e7e405 apply code formatter, part 4 2026-03-11 17:10:29 +01:00
adb ca40ae7869 Merge pull request #4270 from deltachat/adb/apply-code-format3
apply code formatter part 3
2026-03-11 17:01:17 +01:00
adbenitez cb05abb85f apply code formatter part 3 2026-03-11 16:33:21 +01:00
adb 8a76f99701 Merge pull request #4269 from deltachat/adb/apply-code-format2
apply code formatter part 2
2026-03-11 14:34:13 +01:00
adbenitez fd5a3b9f8e apply code formatter part 2 2026-03-11 14:16:44 +01:00
adb 6cab1d1cf4 Merge pull request #4268 from deltachat/adb/apply-code-format1
apply code format part 1
2026-03-11 14:06:27 +01:00
biörn be8009c950 fix: leave and delete finishes activity (#4266)
* fix: leave and delete finishes activity

the chat is gone - otherwise, we end up in ghost chat zero :)

moreover, the fix removes shortcuts from the homescreen.

a toast is no longer needed, as there is enough visual feedback;
we're also not showing a toast on plain deletion.

* use consistent string and clarify deletion scope
2026-03-11 14:01:34 +01:00
adbenitez f6c1fa2f5c apply code format part 1 2026-03-11 13:40:09 +01:00
biörn 6949be6fd8 leave and delete (#4262)
* deleting groups/channels is available after leaving only

* update CHANGELOG

* Update src/main/java/org/thoughtcrime/securesms/ConversationActivity.java

Co-authored-by: adb <adb@merlinux.eu>

* mailing lists are not encrypted

---------

Co-authored-by: adb <adb@merlinux.eu>
2026-03-11 00:40:04 +01:00
adb f95a6e8db7 Merge pull request #4264 from deltachat/adb/make-encr-info-selectable
make it possible to select/copy the fingerprints in encryption info dialog
2026-03-10 21:14:02 +01:00
biörn b81757d579 simplify 'call' wording (#4263) 2026-03-10 20:33:26 +01:00
adbenitez 30c0026840 make it possible to select/copy the fingerprints in encryption info dialog 2026-03-10 19:40:06 +01:00
adbenitez efb4bf0536 Merge remote-tracking branch 'upstream/main' 2026-03-10 19:08:40 +01:00
adb cd2fc0df0f Merge pull request #4230 from deltachat/adb/sticker-picker
add basic sticker picker
2026-03-10 14:49:17 +01:00
adbenitez 51380cf906 update changelog 2026-03-09 20:47:03 +01:00
adbenitez ded0c44ce3 improve StickerPickerView's code 2026-03-09 20:45:46 +01:00
adbenitez 61f5064929 move setupViews() to onFinishInflate() 2026-03-09 19:07:19 +01:00
adb f316ebea20 Merge branch 'main' into adb/sticker-picker 2026-03-09 18:18:24 +01:00
wchen342 dc1a029e4a Merge pull request #4250 from deltachat/wch423/remove-notification-msg-delete
Remove notification when message is deleted
2026-03-09 13:03:31 -04:00
wchen342 6bbacae54d Merge branch 'main' into wch423/remove-notification-msg-delete 2026-03-09 12:52:26 -04:00
wch423 972d372bd4 Add back exception handling for notify() 2026-03-09 17:17:28 +01:00
biörn fe7fb19adf add a hint about sending original images (#4259)
* add a hint about sending original images

* force items having needed size
2026-03-09 16:49:36 +01:00
biörn 5112389dae remove unused CustomDefaultPreference (#4257)
* remove unused CustomDefaultPreference

* remove now superfluous null check

* remove now unused resources
2026-03-09 16:48:17 +01:00
biörn c190a7b50a update some call translator notes (#4256) 2026-03-09 12:08:19 +01:00
biörn 7ce8ccff78 unify status line (#4255)
* less formal duration string

* use standard status footer

* move call icon to call information

* add duration line

* update CHANGELOG
2026-03-09 10:54:54 +01:00
biörn 7eb0a99fc7 add strings for the 'Calls' notification setting (#4249)
* add strings for the 'Calls' notification setting

* Update src/main/res/values/strings.xml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/res/values/strings.xml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 01:21:29 +01:00
adb cbdd8f19ed Merge branch 'main' into wch423/remove-notification-msg-delete 2026-03-06 17:15:28 +01:00
adb 5fdf319cda Merge pull request #4254 from deltachat/Hocuri-patch-1
CI: Change preview APK architecture from 32-bit to 64-bit
2026-03-06 14:55:48 +01:00
wch423 837e2313dd Make notifications rebuild up on message deletion 2026-03-06 00:26:59 +01:00
Hocuri e45fd2545e Change build target from armeabi-v7a to arm64-v8a 2026-03-05 18:27:08 +01:00
wch423 15537ea4b2 Remove notification when message is deleted 2026-03-05 00:40:10 +01:00
B. Petersen df97f4069a remove deprecated strings 2026-03-03 23:57:10 +01:00
B. Petersen dea8b2bed9 tweak 'view count' translator's hint 2026-03-02 16:54:56 +01:00
B. Petersen 0d130a4218 add view-count string 2026-03-01 13:24:38 +01:00
adbenitez b4ee89c1e8 Merge remote-tracking branch 'upstream/main' 2026-02-27 18:56:48 +01:00
adb b2e7b8fdb6 Merge branch 'main' into adb/sticker-picker 2026-02-27 18:51:14 +01:00
adb a494feb272 Merge pull request #4245 from deltachat/update-core-and-stuff-2026-02-27
Update core to 2.44.0
2026-02-27 18:49:20 +01:00
adbenitez 5efce4345a update RPC 2026-02-27 18:45:37 +01:00
adbenitez b7703ce71f update changelog 2026-02-27 17:25:24 +01:00
adbenitez c0ed32b645 update translations 2026-02-27 17:25:11 +01:00
adbenitez 7e4f408059 update deltachat-core-rust to 'chore(release): prepare for 2.44.0' of 'v2.44.0' 2026-02-27 17:18:33 +01:00
adbenitez a1993426de Merge remote-tracking branch 'upstream/main' 2026-02-24 01:30:11 +01:00
adbenitez bbb6ce8def fix ConversationFragment 2026-02-24 01:29:39 +01:00
adb c601595477 Merge pull request #4238 from deltachat/adb/avoid-deprecated-onActivityResult
migrate away from deprecated `Fragment.onActivityResult()`
2026-02-23 19:11:30 +01:00
adbenitez f909174119 tweak ConversationListActivity and RelayListActivity
don't relay in onActivityResult
2026-02-23 18:59:54 +01:00
adbenitez b0762a8b46 Merge remote-tracking branch 'upstream/main' 2026-02-22 13:00:11 +01:00
adb 545f987f31 Merge branch 'main' into adb/avoid-deprecated-onActivityResult 2026-02-21 23:48:11 +01:00
adb b625c57d2d Merge pull request #4240 from deltachat/adb/improve-channel-creation
don't set channel description on channel creation if it is empty
2026-02-21 23:47:57 +01:00
adbenitez 022d24bc5f Merge branch 'adb/improve-channel-creation' of https://github.com/deltachat/deltachat-android into adb/improve-channel-creation 2026-02-21 23:47:20 +01:00
adbenitez c5c09008a6 fix missing parenthesis 2026-02-21 23:47:03 +01:00
adb 7679cc8bba Merge branch 'main' into adb/avoid-deprecated-onActivityResult 2026-02-21 23:34:03 +01:00
adb 4807d05861 Merge branch 'main' into adb/improve-channel-creation 2026-02-21 23:33:20 +01:00
adb 34eedf5f07 Merge pull request #4239 from deltachat/adb/improve-description-changed-info-clicked
when "description changed by Foo" is clicked, open chat profile
2026-02-21 23:32:57 +01:00
adbenitez 941c186142 don't set channel description on channel creation if it is empty
when user creates a channel, only call rpc.setChatDescription if
there is actually some description to set, otherwise there will be a
"you changed the description" info-message when the user didn't set anything
2026-02-21 23:10:37 +01:00
adbenitez b54ac50ea5 when "description changed by Foo" is clicked, open chat profile 2026-02-21 22:59:19 +01:00
adb b99426b7c6 Merge pull request #118 from ArcaneChat/copilot/setup-copilot-instructions-again
Enhance Copilot instructions with build timing, CI/CD details, and troubleshooting
2026-02-19 05:33:38 +01:00
adbenitez de1eedc63f update .github/copilot-instructions.md 2026-02-19 05:32:04 +01:00
adbenitez c3dac043cb remove ScreenLockUtil.applyScreenLock(Activity, String, String, int) 2026-02-19 04:59:24 +01:00
adbenitez 00b52d4fe6 don't call registerForActivityResult in onActivityCreated 2026-02-19 03:34:05 +01:00
adbenitez b66bf595e2 avoid call to fragment.onActivityResult in ApplicationPreferencesActivity 2026-02-19 03:08:46 +01:00
adbenitez 7c24e836fe avoid deprecated ContactSelectionListFragment.onActivityResult() 2026-02-19 02:26:09 +01:00
adbenitez 694f3bf013 remove unused import in ListSummaryPreferenceFragment 2026-02-19 02:08:47 +01:00
adbenitez f6f42991e8 avoid deprecated NotificationsPreferenceFragment.onActivityResult() 2026-02-19 02:08:24 +01:00
adbenitez 23f6803be9 avoid deprecated ChatsPreferenceFragment.onActivityResult()
also remove now unused fields in `ListSummaryPreferenceFragment`
2026-02-19 00:05:39 +01:00
adbenitez cdcb7fab4b avoid deprecated AdvancedPreferenceFragment.onActivityResult() 2026-02-18 23:53:30 +01:00
adbenitez 5ee3607312 avoid deprecated ConversationFragment.onActivityResult() 2026-02-18 23:32:54 +01:00
adbenitez 4b9642f51f add more logging to ProfileFragment 2026-02-18 23:29:54 +01:00
adbenitez 4abbf3091f avoid deprecated in ProfileFragment.onActivityResult() 2026-02-18 22:58:21 +01:00
copilot-swe-agent[bot] ec4db3e58f Enhance Copilot instructions with detailed build, test, and CI/CD information
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 20:52:39 +00:00
copilot-swe-agent[bot] 0d2175b641 Initial plan 2026-02-18 20:49:42 +00:00
adb a821ee363a Merge branch 'main' into adb/sticker-picker 2026-02-18 19:59:07 +01:00
adb f43083acef Merge pull request #4214 from deltachat/adb/chat-description
allow to set chat description
2026-02-18 19:58:02 +01:00
adb 7aed78a935 Update src/main/res/values/strings.xml
Co-authored-by: biörn <r10s@b44t.com>
2026-02-18 19:34:59 +01:00
adbenitez 26c30fe3dc Merge branch 'adb/chat-description' of https://github.com/deltachat/deltachat-android into adb/chat-description 2026-02-18 19:32:49 +01:00
adbenitez 5550b8b1f5 update changelog 2026-02-18 19:32:00 +01:00
adb c94512fb24 Merge branch 'main' into adb/chat-description 2026-02-18 19:30:30 +01:00
B. Petersen d1db6e5a8a apply suggestion from @wchen342 2026-02-18 14:44:56 +01:00
B. Petersen d352583237 avoid 'recode', which is too technical as well 2026-02-18 14:44:56 +01:00
B. Petersen c8469be136 clearify, how to recode multiple videos 2026-02-18 14:44:56 +01:00
adb 97620b8a3c Merge pull request #114 from ArcaneChat/copilot/improve-attachment-selector
Convert attachment selector to horizontal scrollable row with uniform cell sizing
2026-02-18 04:40:13 +01:00
adbenitez 24317b38bf tweak src/main/res/layout/attachment_type_selector.xml 2026-02-18 04:35:01 +01:00
copilot-swe-agent[bot] 6b28ed15e3 Fix cell sizing: uniform width with ellipsized text
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 03:02:49 +00:00
copilot-swe-agent[bot] b8b203e517 Convert attachment selector from fixed grid to scrollable row
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-02-18 02:47:04 +00:00
copilot-swe-agent[bot] e5c1e477f0 Initial plan 2026-02-18 02:44:54 +00:00
adbenitez b0699ab9be sort stickers 2026-02-17 18:04:56 +01:00
adbenitez 499f4aafc8 add basic sticker picker 2026-02-17 04:14:16 +01:00
736 changed files with 27504 additions and 23884 deletions
+148 -9
View File
@@ -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
+2 -2
View File
@@ -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
+12
View File
@@ -1,5 +1,17 @@
# Delta Chat Android Changelog
## 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
@@ -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&amp;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> &lt;newline&gt; <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&amp;body=blub">no email, just subject&amp;body</a> - this should let you choose a chat and create a draft <code>bla</code> &lt;newline&gt; <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&amp;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> &lt;newline&gt; <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&amp;body=blub">no email, just subject&amp;body</a> - this
* should let you choose a chat and create a draft <code>bla</code> &lt;newline&gt; <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()));
@@ -1,17 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
<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>
<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>
@@ -1,6 +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"/>
<background android:drawable="@color/white" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +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"/>
<background android:drawable="@color/white" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+11 -9
View File
@@ -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;
}
}
+34 -26
View File
@@ -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 {
+2 -4
View File
@@ -1369,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">
+5
View File
@@ -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");
}
@@ -2,64 +2,92 @@ package com.b44t.messenger;
public class DcAccounts {
public DcAccounts(String dir, DcEventChannel channel) {
accountsCPtr = createAccountsCPtr(dir, channel);
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, 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;
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();
}
+92 -59
View File
@@ -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);
}
+68 -53
View File
@@ -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();
}
+376 -249
View File
@@ -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();
}
+24 -17
View File
@@ -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();
}
@@ -20,8 +20,11 @@ public class DcEventChannel {
}
// 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 ();
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();
}
+27 -20
View File
@@ -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;
}
+298 -232
View File
@@ -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
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,6 @@ 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;
@@ -20,14 +19,13 @@ 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;
@@ -35,38 +33,37 @@ import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
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;
@@ -82,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));
@@ -94,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);
@@ -123,47 +123,44 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
}
@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));
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) {
@@ -176,13 +173,13 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
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 {
@@ -216,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,12 +5,12 @@ 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.DocumentView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
@@ -22,31 +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 AudioPlaybackViewModel playbackViewModel;
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);
}
}
@@ -59,14 +55,12 @@ 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) {
@@ -78,25 +72,30 @@ class AllMediaDocumentsAdapter 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_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);
@@ -104,46 +103,60 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
viewHolder.audioView.setVisibility(View.VISIBLE);
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
viewHolder.audioView.setAudio((AudioSlide)slide);
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
@@ -184,6 +197,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
interface ItemClickListener {
void onMediaClicked(@NonNull DcMsg mediaRecord);
void onMediaLongClicked(DcMsg mediaRecord);
}
}
@@ -12,7 +12,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.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
@@ -20,25 +19,20 @@ 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";
@@ -50,7 +44,7 @@ public class AllMediaDocumentsFragment
private int viewtype1;
private int viewtype2;
protected int chatId;
protected int chatId;
@Override
public void onCreate(Bundle bundle) {
@@ -64,21 +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);
AllMediaDocumentsAdapter adapter = 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));
adapter.setPlaybackViewModel(
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@@ -109,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();
@@ -122,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);
@@ -137,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
@@ -168,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;
}
@@ -216,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);
}
@@ -243,20 +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);
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(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
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
@@ -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 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;
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) {
@@ -137,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,26 +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,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener);
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();
@@ -29,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,7 +28,6 @@ import java.util.List;
* Activity container for selecting a list of contacts.
*
* @author Moxie Marlinspike
*
*/
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
@@ -74,7 +72,8 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
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));
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,48 +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.HashSet;
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> deselectedContacts;
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
@@ -114,53 +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);
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
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;
}
@@ -192,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);
}
@@ -216,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);
}
@@ -247,15 +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();
deselectedContacts = new HashSet<>();
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
if(preselectedContacts!=null) {
ArrayList<Integer> preselectedContacts =
getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
if (preselectedContacts != null) {
selectedContacts.addAll(preselectedContacts);
}
recyclerView.setAdapter(adapter);
@@ -270,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
@@ -298,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);
@@ -314,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);
}
@@ -340,38 +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);
deselectedContacts.remove(contactId);
}
if (event.getId() == DcContext.DC_EVENT_CONTACTS_CHANGED) {
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
}
@@ -531,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()) {
@@ -732,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() {
@@ -753,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();
@@ -1456,6 +1460,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) {
@@ -23,16 +23,20 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import java.lang.ref.SoftReference;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
@@ -45,61 +49,50 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.lang.ref.SoftReference;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A DC adapter for a conversation thread. Ultimately
* used by ConversationActivity to display a conversation
* thread in a ListActivity.
* A DC adapter for a conversation thread. Ultimately used by ConversationActivity to display a
* conversation thread in a ListActivity.
*
* @author Moxie Marlinspike
*
*/
// FIXME: this breaks type checks, that is why there are so many casts.
public class ConversationAdapter <V extends View & BindableConversationItem>
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;
@@ -110,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() {
@@ -118,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;
}
@@ -151,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);
}
@@ -170,7 +161,7 @@ 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;
}
@@ -183,12 +174,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
this.audioPlayPauseListener = audioPlayPauseListener;
}
/**
* Returns the position of the message with msgId in the chat list, counted from the top
*/
/** Returns the position of the message with msgId in the chat list, counted from the top */
public int msgIdToPosition(int msgId) {
for(int i=0; i<dcMsgList.length; i++ ) {
if(dcMsgList[i]==msgId) {
for (int i = 0; i < dcMsgList.length; i++) {
if (dcMsgList[i] == msgId) {
return dcMsgList.length - 1 - i;
}
}
@@ -213,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;
@@ -231,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) {
@@ -248,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, playbackViewModel, audioPlayPauseListener);
holder
.getItem()
.bind(
getMsg(position),
dcChat,
glideRequests,
batchSelected,
recipient,
pulseHighlight,
playbackViewModel,
audioPlayPauseListener);
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder viewHolder) {
if (viewHolder.itemView instanceof ConversationItem) {
if (viewHolder.itemView instanceof ConversationItem) {
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
}
}
@@ -263,17 +264,19 @@ public class ConversationAdapter <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);
}
@@ -284,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");
}
}
@@ -301,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;
}
}
@@ -338,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();
@@ -361,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));
@@ -369,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;
@@ -400,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) {
@@ -417,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 {
@@ -429,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
@@ -807,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;
}
@@ -969,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);
}
}
}
@@ -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,46 +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";
public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr";
private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1;
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();
@@ -125,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());
@@ -135,59 +181,68 @@ 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();
@@ -207,7 +262,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
* 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`.
*/
@@ -220,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";
@@ -234,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);
}
}
@@ -311,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);
}
@@ -347,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));
@@ -386,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));
@@ -397,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);
@@ -408,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
@@ -458,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);
@@ -483,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;
@@ -547,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 {
@@ -592,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";
@@ -601,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");
@@ -610,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);
@@ -619,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();
}
}
@@ -634,7 +717,12 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
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);
}
@@ -662,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,6 +239,7 @@ public class ConversationListFragment extends BaseConversationListFragment
private final Object loadChatlistLock = new Object();
private boolean inLoadChatlist;
private boolean needsAnotherLoad;
public void loadChatlistAsync() {
synchronized (loadChatlistLock) {
needsAnotherLoad = true;
@@ -243,21 +250,22 @@ 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);
}
});
}
private void loadChatlist() {
@@ -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);
@@ -274,43 +268,50 @@ public class ConversationListItem extends RelativeLayout
}
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("");
}
@@ -321,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;
@@ -337,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,14 +5,13 @@ 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;
@@ -21,14 +20,10 @@ 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);
@@ -44,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);
@@ -58,24 +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,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
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();
@@ -118,14 +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.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;
}
@@ -16,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;
@@ -41,16 +43,8 @@ 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.Rpc;
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";
@@ -65,15 +59,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private boolean unencrypted;
private boolean broadcast;
private EditText groupName;
private EditText chatDescription;
private ListView lv;
private ImageView avatar;
private Bitmap avatarBmp;
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
@@ -91,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();
@@ -100,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();
}
@@ -125,27 +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);
chatDescription = ViewUtil.findById(this, R.id.chat_description);
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);
@@ -154,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);
@@ -189,7 +181,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
chatHints.setVisibility(View.GONE);
}
if(isEdit()) {
if (isEdit()) {
groupName.setText(dcContext.getChat(groupChatId).getName());
lv.setVisibility(View.GONE);
@@ -210,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
@@ -287,7 +282,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
groupChatId = rpc.createGroupChat(accId, groupName, false);
}
rpc.setChatDescription(accId, groupChatId, getChatDescription());
String description = getChatDescription();
if (!description.isEmpty()) {
rpc.setChatDescription(accId, groupChatId, description);
}
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
return;
@@ -296,7 +294,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
for (int contactId : getAdapter().getContacts()) {
dcContext.addContactToChat(groupChatId, contactId);
}
if (avatarBmp!=null) {
if (avatarBmp != null) {
AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
}
@@ -308,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;
@@ -339,26 +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 @Nullable String getChatDescription() {
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);
}
@@ -367,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);
}
}
@@ -394,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);
}
@@ -416,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) {
@@ -439,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,17 +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 java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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";
@@ -90,7 +84,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private String providerHost;
private String providerQrData;
private String rawQrData;
private DcLot parsedQrData;
private DcLot parsedQrData;
private boolean isDcLogin;
private boolean isContactInvitation;
private boolean isGroupInvitation;
@@ -111,14 +105,16 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
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);
@@ -157,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);
@@ -187,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;
@@ -217,7 +214,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
DcLot qrParsed = dcContext.checkQr(rawQr);
switch (qrParsed.getState()) {
case DcContext.DC_QR_LOGIN:
isDcLogin = true; // Intentional fall-through
isDcLogin = true; // Intentional fall-through
case DcContext.DC_QR_ACCOUNT:
providerHost = qrParsed.getText1();
providerQrData = rawQr;
@@ -237,14 +234,15 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
break;
default:
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
.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);
}
@@ -254,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 {
@@ -291,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());
}
}
@@ -299,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) {
@@ -333,35 +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.invitationText = findViewById(R.id.invitation_label);
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() {
@@ -372,16 +384,18 @@ 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) {
@@ -395,7 +409,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
invitationText.setVisibility(View.VISIBLE);
}
}
}
}
@@ -406,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));
}
@@ -427,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));
}
}
@@ -472,35 +493,38 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
}
final String name = this.name.getText().toString();
executor.execute(() -> {
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);
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;
}
}
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;
}
}
boolean finalResult = result;
runOnUiThread(() -> {
if (finalResult) {
attachmentManager.cleanup();
startQrAccountCreation(providerQrData);
} else {
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show();
}
});
});
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;
@@ -512,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 {
@@ -546,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);
@@ -559,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);
@@ -132,10 +130,11 @@ public class LogViewFragment extends 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();
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);
}
}
@@ -10,16 +10,14 @@ import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import java.util.Set;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -27,12 +25,8 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.Set;
public abstract class MessageSelectorFragment
extends Fragment
implements DcEventCenter.DcEventDelegate
{
public abstract class MessageSelectorFragment extends Fragment
implements DcEventCenter.DcEventDelegate {
protected ActionMode actionMode;
protected abstract void setCorrectMenuVisibility(Menu menu);
@@ -43,7 +37,7 @@ public abstract class MessageSelectorFragment
protected DcMsg getSelectedMessageRecord(Set<DcMsg> messageRecords) {
if (messageRecords.size() == 1) return messageRecords.iterator().next();
else throw new AssertionError();
else throw new AssertionError();
}
protected void handleDisplayDetails(DcMsg dcMsg) {
@@ -51,7 +45,8 @@ public abstract class MessageSelectorFragment
TextView detailsText = view.findViewById(R.id.details_text);
detailsText.setText(DcHelper.getContext(getContext()).getMsgInfo(dcMsg.getId()));
AlertDialog d = new AlertDialog.Builder(getActivity())
AlertDialog d =
new AlertDialog.Builder(getActivity())
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.create();
@@ -62,16 +57,28 @@ public abstract class MessageSelectorFragment
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
}
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
protected void handleDeleteMessages(
int chatId,
final Set<DcMsg> messageRecords,
Consumer<int[]> deleteForMeListenerExtra,
Consumer<int[]> deleteForAllListenerExtra) {
handleDeleteMessages(
chatId,
DcMsg.msgSetToIds(messageRecords),
deleteForMeListenerExtra,
deleteForAllListenerExtra);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
protected void handleDeleteMessages(
int chatId,
final int[] messageIds,
Consumer<int[]> deleteForMeListenerExtra,
Consumer<int[]> deleteForAllListenerExtra) {
DcContext dcContext = DcHelper.getContext(getContext());
DcChat dcChat = dcContext.getChat(chatId);
boolean canDeleteForAll = true;
if (dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk()) {
for(int msgId : messageIds) {
for (int msgId : messageIds) {
DcMsg msg = dcContext.getMsg(msgId);
if (!msg.isOutgoing() || msg.isInfo()) {
canDeleteForAll = false;
@@ -82,26 +89,32 @@ public abstract class MessageSelectorFragment
canDeleteForAll = false;
}
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
String text =
getActivity()
.getResources()
.getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
};
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
.setMessage(text)
.setCancelable(true)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
DialogInterface.OnClickListener deleteForMeListener =
(d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
};
AlertDialog.Builder builder =
new AlertDialog.Builder(requireActivity())
.setMessage(text)
.setCancelable(true)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
if(canDeleteForAll) {
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
};
if (canDeleteForAll) {
DialogInterface.OnClickListener deleteForAllListener =
(d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
};
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
AlertDialog dialog = builder.show();
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
@@ -113,29 +126,36 @@ public abstract class MessageSelectorFragment
}
protected void handleSaveAttachment(final Set<DcMsg> messageRecords) {
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore(getContext())) {
performSave(messageRecords);
return;
}
SaveAttachmentTask.showWarningDialog(
getContext(),
(dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore(getContext())) {
performSave(messageRecords);
return;
}
Permissions.with(getActivity())
Permissions.with(getActivity())
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.alwaysGrantOnSdk30()
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> performSave(messageRecords))
.execute();
});
});
}
private void performSave(Set<DcMsg> messageRecords) {
SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[messageRecords.size()];
SaveAttachmentTask.Attachment[] attachments =
new SaveAttachmentTask.Attachment[messageRecords.size()];
int index = 0;
for (DcMsg message : messageRecords) {
attachments[index] = new SaveAttachmentTask.Attachment(
Uri.fromFile(message.getFileAsFile()), message.getFilemime(), message.getDateReceived(), message.getFilename());
index++;
attachments[index] =
new SaveAttachmentTask.Attachment(
Uri.fromFile(message.getFileAsFile()),
message.getFilemime(),
message.getDateReceived(),
message.getFilename());
index++;
}
SaveAttachmentTask saveTask = new SaveAttachmentTask(getContext());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments);
@@ -144,7 +164,9 @@ public abstract class MessageSelectorFragment
protected void handleShowInChat(final DcMsg dcMsg) {
Intent intent = new Intent(getContext(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId());
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(getContext())));
intent.putExtra(
ConversationActivity.STARTING_POSITION_EXTRA,
DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(getContext())));
startActivity(intent);
}
@@ -155,22 +177,24 @@ public abstract class MessageSelectorFragment
protected void handleResendMessage(final Set<DcMsg> dcMsgsSet) {
int[] ids = DcMsg.msgSetToIds(dcMsgsSet);
DcContext dcContext = DcHelper.getContext(getContext());
Util.runOnAnyBackgroundThread(() -> {
boolean success = dcContext.resendMsgs(ids);
Util.runOnMain(() -> {
Activity activity = getActivity();
if (activity == null || activity.isFinishing()) return;
if (success) {
actionMode.finish();
Toast.makeText(getContext(), R.string.sending, Toast.LENGTH_SHORT).show();
} else {
new AlertDialog.Builder(activity)
.setMessage(dcContext.getLastError())
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
});
Util.runOnAnyBackgroundThread(
() -> {
boolean success = dcContext.resendMsgs(ids);
Util.runOnMain(
() -> {
Activity activity = getActivity();
if (activity == null || activity.isFinishing()) return;
if (success) {
actionMode.finish();
Toast.makeText(getContext(), R.string.sending, Toast.LENGTH_SHORT).show();
} else {
new AlertDialog.Builder(activity)
.setMessage(dcContext.getLastError())
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
});
}
}
@@ -1,10 +1,8 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import java.util.concurrent.TimeUnit;
public class MuteDialog {
@@ -13,21 +11,35 @@ public class MuteDialog {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.menu_mute);
builder.setNegativeButton(R.string.cancel, null);
builder.setItems(R.array.mute_durations, (dialog, which) -> {
final long muteUntil;
builder.setItems(
R.array.mute_durations,
(dialog, which) -> {
final long muteUntil;
// See https://c.delta.chat/classdc__context__t.html#a6460395925d49d2053bc95224bf5ce37.
switch (which) {
case 0: muteUntil = TimeUnit.HOURS.toSeconds(1); break;
case 1: muteUntil = TimeUnit.HOURS.toSeconds(8); break;
case 2: muteUntil = TimeUnit.DAYS.toSeconds(1); break;
case 3: muteUntil = TimeUnit.DAYS.toSeconds(7); break;
case 4: muteUntil = -1; break; // mute forever
default: muteUntil = 0; break;
}
// See https://c.delta.chat/classdc__context__t.html#a6460395925d49d2053bc95224bf5ce37.
switch (which) {
case 0:
muteUntil = TimeUnit.HOURS.toSeconds(1);
break;
case 1:
muteUntil = TimeUnit.HOURS.toSeconds(8);
break;
case 2:
muteUntil = TimeUnit.DAYS.toSeconds(1);
break;
case 3:
muteUntil = TimeUnit.DAYS.toSeconds(7);
break;
case 4:
muteUntil = -1;
break; // mute forever
default:
muteUntil = 0;
break;
}
listener.onMuted(muteUntil);
});
listener.onMuted(muteUntil);
});
builder.show();
}
@@ -35,5 +47,4 @@ public class MuteDialog {
public interface MuteSelectionListener {
void onMuted(long duration);
}
}
@@ -23,26 +23,21 @@ import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import chat.delta.rpc.types.SecurejoinSource;
import chat.delta.rpc.types.SecurejoinUiPath;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.qr.QrActivity;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import chat.delta.rpc.types.SecurejoinSource;
import chat.delta.rpc.types.SecurejoinUiPath;
/**
* Activity container for starting a new conversation.
*
* @author Moxie Marlinspike
*
*/
public class NewConversationActivity extends ContactSelectionActivity {
@@ -57,32 +52,34 @@ public class NewConversationActivity extends ContactSelectionActivity {
@Override
public void onContactSelected(int contactId) {
if(contactId == DcContact.DC_CONTACT_ID_NEW_GROUP) {
if (contactId == DcContact.DC_CONTACT_ID_NEW_GROUP) {
startActivity(new Intent(this, GroupCreateActivity.class));
} else if(contactId == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP) {
} else if (contactId == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP) {
Intent intent = new Intent(this, GroupCreateActivity.class);
intent.putExtra(GroupCreateActivity.UNENCRYPTED, true);
startActivity(intent);
} else if(contactId == DcContact.DC_CONTACT_ID_NEW_BROADCAST) {
} else if (contactId == DcContact.DC_CONTACT_ID_NEW_BROADCAST) {
Intent intent = new Intent(this, GroupCreateActivity.class);
intent.putExtra(GroupCreateActivity.CREATE_BROADCAST, true);
startActivity(intent);
} else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) {
new IntentIntegrator(this).setCaptureActivity(QrActivity.class).initiateScan();
}
else {
} else {
final DcContext dcContext = DcHelper.getContext(this);
if (dcContext.getChatIdByContactId(contactId)!=0) {
if (dcContext.getChatIdByContactId(contactId) != 0) {
openConversation(dcContext.getChatIdByContactId(contactId));
} else {
String name = dcContext.getContact(contactId).getDisplayName();
new AlertDialog.Builder(this)
.setMessage(getString(R.string.ask_start_chat_with, name))
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
.setMessage(getString(R.string.ask_start_chat_with, name))
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(
android.R.string.ok,
(dialog, which) -> {
openConversation(dcContext.createChatByContactId(contactId));
}).show();
})
.show();
}
}
}
@@ -96,7 +93,8 @@ public class NewConversationActivity extends ContactSelectionActivity {
case IntentIntegrator.REQUEST_CODE:
IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data);
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
qrCodeHandler.handleOnlySecureJoinQr(scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact);
qrCodeHandler.handleOnlySecureJoinQr(
scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact);
break;
default:
break;
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.GenericForegroundService;
@@ -50,7 +49,9 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
// The app is "locked" to an activity if you will.
// In "Locked Mode" the user should not leave that activity otherwise the state would be lost -
// so eg. tapping app icon or notifications MUST NOT replace activity stack.
// However, sometimes it is fine to allow to pushing activities in these situations,
// However, sometimes it is fine to allow to pushing activities in these situations,
// like to see the logs or offline help.
protected boolean allowInLockedMode() { return false; }
protected boolean allowInLockedMode() {
return false;
}
}
@@ -12,18 +12,19 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import java.io.File;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@@ -32,31 +33,25 @@ import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.File;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ProfileActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
implements DcEventCenter.DcEventDelegate {
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";
private static final int REQUEST_CODE_PICK_RINGTONE = 1;
private DcContext dcContext;
private DcContext dcContext;
private Rpc rpc;
private int chatId;
private boolean chatIsMultiUser;
private boolean chatIsDeviceTalk;
private boolean chatIsMailingList;
private boolean chatIsOutBroadcast;
private boolean chatIsInBroadcast;
private int contactId;
private boolean contactIsBot;
private Toolbar toolbar;
private int chatId;
private boolean chatIsMultiUser;
private boolean chatIsDeviceTalk;
private boolean chatIsMailingList;
private boolean chatIsOutBroadcast;
private boolean chatIsInBroadcast;
private int contactId;
private boolean contactIsBot;
private Toolbar toolbar;
@Override
protected void onPreCreate() {
@@ -110,7 +105,9 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
if (chatId != 0) {
DcChat dcChat = dcContext.getChat(chatId);
menu.findItem(R.id.menu_clone).setVisible(chatIsMultiUser && !chatIsInBroadcast && !chatIsOutBroadcast && !chatIsMailingList);
menu.findItem(R.id.menu_clone)
.setVisible(
chatIsMultiUser && !chatIsInBroadcast && !chatIsOutBroadcast && !chatIsMailingList);
if (chatIsDeviceTalk) {
menu.findItem(R.id.edit_name).setVisible(false);
menu.findItem(R.id.show_encr_info).setVisible(false);
@@ -120,9 +117,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
if (chatIsOutBroadcast) {
canReceive = false;
} else {
if (!dcChat.isEncrypted()
|| !dcChat.canSend()
|| chatIsMailingList) {
if (!dcChat.isEncrypted() || !dcChat.canSend() || chatIsMailingList) {
menu.findItem(R.id.edit_name).setVisible(false);
}
}
@@ -155,14 +150,18 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item = menu.findItem(R.id.block_contact);
if(item!=null) {
item.setTitle(dcContext.getContact(contactId).isBlocked()? R.string.menu_unblock_contact : R.string.menu_block_contact);
if (item != null) {
item.setTitle(
dcContext.getContact(contactId).isBlocked()
? R.string.menu_unblock_contact
: R.string.menu_block_contact);
Util.redMenuItem(menu, R.id.block_contact);
}
item = menu.findItem(R.id.menu_mute_notifications);
if(item!=null) {
item.setTitle(dcContext.getChat(chatId).isMuted()? R.string.menu_unmute : R.string.menu_mute);
if (item != null) {
item.setTitle(
dcContext.getChat(chatId).isMuted() ? R.string.menu_unmute : R.string.menu_mute);
}
super.onPrepareOptionsMenu(menu);
@@ -182,48 +181,47 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
}
@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);
contactIsBot = false;
chatIsMultiUser = false;
chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0);
contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0);
contactIsBot = false;
chatIsMultiUser = false;
chatIsDeviceTalk = false;
chatIsMailingList= false;
chatIsMailingList = false;
chatIsInBroadcast = false;
chatIsOutBroadcast = false;
if (contactId!=0) {
if (contactId != 0) {
DcContact dcContact = dcContext.getContact(contactId);
chatId = dcContext.getChatIdByContactId(contactId);
contactIsBot = dcContact.isBot();
}
if(chatId!=0) {
if (chatId != 0) {
DcChat dcChat = dcContext.getChat(chatId);
chatIsMultiUser = dcChat.isMultiUser();
chatIsDeviceTalk = dcChat.isDeviceTalk();
chatIsMailingList = dcChat.isMailingList();
chatIsInBroadcast = dcChat.isInBroadcast();
chatIsOutBroadcast = dcChat.isOutBroadcast();
if(!chatIsMultiUser) {
if (!chatIsMultiUser) {
final int[] members = dcContext.getChatContacts(chatId);
contactId = members.length>=1? members[0] : 0;
contactId = members.length >= 1 ? members[0] : 0;
}
}
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
}
private boolean isContactProfile() {
// contact-profiles are profiles without a chat or with a one-to-one chat
return contactId!=0 && (chatId==0 || !chatIsMultiUser);
return contactId != 0 && (chatId == 0 || !chatIsMultiUser);
}
private boolean isSelfProfile() {
return isContactProfile() && contactId==DcContact.DC_CONTACT_ID_SELF;
return isContactProfile() && contactId == DcContact.DC_CONTACT_ID_SELF;
}
// handle events
@@ -270,8 +268,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
private void onNotifyOnOff() {
if (dcContext.getChat(chatId).isMuted()) {
setMuted(0);
}
else {
} else {
MuteDialog.show(this, this::setMuted);
}
}
@@ -286,7 +283,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
Uri current = Prefs.getChatRingtone(this, dcContext.getAccountId(), chatId);
Uri defaultUri = Prefs.getNotificationRingtone(this);
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
else if (current.toString().isEmpty()) current = null;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
@@ -301,15 +298,23 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
private void onVibrateSettings() {
int checkedItem = Prefs.getChatVibrate(this, dcContext.getAccountId(), chatId).getId();
int[] selectedChoice = new int[]{checkedItem};
int[] selectedChoice = new int[] {checkedItem};
new AlertDialog.Builder(this)
.setTitle(R.string.pref_vibrate)
.setSingleChoiceItems(R.array.recipient_vibrate_entries, checkedItem,
(dialog, which) -> selectedChoice[0] = which)
.setPositiveButton(R.string.ok,
(dialog, which) -> Prefs.setChatVibrate(this, dcContext.getAccountId(), chatId, Prefs.VibrateState.fromId(selectedChoice[0])))
.setNegativeButton(R.string.cancel, null)
.show();
.setTitle(R.string.pref_vibrate)
.setSingleChoiceItems(
R.array.recipient_vibrate_entries,
checkedItem,
(dialog, which) -> selectedChoice[0] = which)
.setPositiveButton(
R.string.ok,
(dialog, which) ->
Prefs.setChatVibrate(
this,
dcContext.getAccountId(),
chatId,
Prefs.VibrateState.fromId(selectedChoice[0])))
.setNegativeButton(R.string.cancel, null)
.show();
}
public void onEnlargeAvatar() {
@@ -317,7 +322,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
String title;
Uri profileImageUri;
boolean enlargeAvatar = true;
if(chatId!=0) {
if (chatId != 0) {
DcChat dcChat = dcContext.getChat(chatId);
profileImagePath = dcChat.getProfileImage();
title = dcChat.getName();
@@ -338,11 +343,10 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
intent.setDataAndType(profileImageUri, type);
intent.putExtra(MediaPreviewActivity.ACTIVITY_TITLE_EXTRA, title);
intent.putExtra( // show edit-button, if the user is allowed to edit the name/avatar
MediaPreviewActivity.EDIT_AVATAR_CHAT_ID,
(chatIsMultiUser && !chatIsInBroadcast && !chatIsMailingList) ? chatId : 0
);
MediaPreviewActivity.EDIT_AVATAR_CHAT_ID,
(chatIsMultiUser && !chatIsInBroadcast && !chatIsMailingList) ? chatId : 0);
startActivity(intent);
} else if (chatIsMultiUser){
} else if (chatIsMultiUser) {
onEditName();
}
}
@@ -355,8 +359,7 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
intent.putExtra(GroupCreateActivity.EDIT_GROUP_CHAT_ID, chatId);
startActivity(intent);
}
}
else {
} else {
int accountId = dcContext.getAccountId();
DcContact dcContact = dcContext.getContact(contactId);
@@ -375,14 +378,16 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
.setTitle(R.string.menu_edit_name)
.setMessage(getString(R.string.edit_name_explain, authName))
.setView(gl)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
String newName = inputField.getText().toString();
try {
rpc.changeContactName(accountId, contactId, newName);
} catch (RpcException e) {
e.printStackTrace();
}
})
.setPositiveButton(
android.R.string.ok,
(dialog, whichButton) -> {
String newName = inputField.getText().toString();
try {
rpc.changeContactName(accountId, contactId, newName);
} catch (RpcException e) {
e.printStackTrace();
}
})
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.show();
@@ -407,33 +412,46 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
}
private void onEncrInfo() {
String infoStr = isContactProfile() ?
dcContext.getContactEncrInfo(contactId) : dcContext.getChatEncrInfo(chatId);
new AlertDialog.Builder(this)
.setMessage(infoStr)
.setPositiveButton(android.R.string.ok, null)
.show();
String infoStr =
isContactProfile()
? dcContext.getContactEncrInfo(contactId)
: dcContext.getChatEncrInfo(chatId);
AlertDialog dialog =
new AlertDialog.Builder(this)
.setMessage(infoStr)
.setPositiveButton(android.R.string.ok, null)
.show();
TextView messageView = dialog.findViewById(android.R.id.message);
if (messageView != null) {
messageView.setTextIsSelectable(true);
}
}
private void onBlockContact() {
DcContact dcContact = dcContext.getContact(contactId);
if(dcContact.isBlocked()) {
if (dcContact.isBlocked()) {
new AlertDialog.Builder(this)
.setMessage(R.string.ask_unblock_contact)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.menu_unblock_contact, (dialog, which) -> {
dcContext.blockContact(contactId, 0);
}).show();
}
else {
AlertDialog dialog = new AlertDialog.Builder(this)
.setMessage(R.string.ask_block_contact)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.menu_block_contact, (d, which) -> {
dcContext.blockContact(contactId, 1);
}).show();
.setPositiveButton(
R.string.menu_unblock_contact,
(dialog, which) -> {
dcContext.blockContact(contactId, 0);
})
.show();
} else {
AlertDialog dialog =
new AlertDialog.Builder(this)
.setMessage(R.string.ask_block_contact)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(
R.string.menu_block_contact,
(d, which) -> {
dcContext.blockContact(contactId, 1);
})
.show();
Util.redPositiveButton(dialog);
}
}
@@ -447,15 +465,16 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_RINGTONE && resultCode== Activity.RESULT_OK && data!=null) {
if (requestCode == REQUEST_CODE_PICK_RINGTONE
&& resultCode == Activity.RESULT_OK
&& data != null) {
Uri value = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
Uri defaultValue = Prefs.getNotificationRingtone(this);
if (defaultValue.equals(value)) value = null;
else if (value == null) value = Uri.EMPTY;
else if (value == null) value = Uri.EMPTY;
Prefs.setChatRingtone(this, dcContext.getAccountId(), chatId, value);
}
}
}
@@ -6,36 +6,30 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcChatlist;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ProfileAdapter extends RecyclerView.Adapter
{
public class ProfileAdapter extends RecyclerView.Adapter {
private static final String TAG = ProfileAdapter.class.getSimpleName();
public static final int ITEM_AVATAR = 10;
@@ -50,22 +44,22 @@ public class ProfileAdapter extends RecyclerView.Adapter
public static final int ITEM_MEMBERS = 55;
public static final int ITEM_SHARED_CHATS = 60;
private final @NonNull Context context;
private final @NonNull Fragment fragment;
private final @NonNull DcContext dcContext;
private @Nullable DcChat dcChat;
private @Nullable DcContact dcContact;
private final @NonNull Context context;
private final @NonNull Fragment fragment;
private final @NonNull DcContext dcContext;
private @Nullable DcChat dcChat;
private @Nullable DcContact dcContact;
private final @NonNull ArrayList<ItemData> itemData = new ArrayList<>();
private DcChatlist itemDataSharedChats;
private String itemDataStatusText;
private boolean isOutBroadcast;
private int[] memberList;
private final Set<Integer> selectedMembers;
private final @NonNull ArrayList<ItemData> itemData = new ArrayList<>();
private DcChatlist itemDataSharedChats;
private String itemDataStatusText;
private boolean isOutBroadcast;
private int[] memberList;
private final Set<Integer> selectedMembers;
private final LayoutInflater layoutInflater;
private final ItemClickListener clickListener;
private final GlideRequests glideRequests;
private final LayoutInflater layoutInflater;
private final ItemClickListener clickListener;
private final GlideRequests glideRequests;
static class ItemData {
final int viewType;
@@ -82,27 +76,29 @@ public class ProfileAdapter extends RecyclerView.Adapter
this(viewType, contactId, chatlistIndex, null, 0);
}
private ItemData(int viewType, int contactId, int chatlistIndex, @Nullable String label, int icon) {
this.viewType = viewType;
this.contactId = contactId;
private ItemData(
int viewType, int contactId, int chatlistIndex, @Nullable String label, int icon) {
this.viewType = viewType;
this.contactId = contactId;
this.chatlistIndex = chatlistIndex;
this.label = label;
this.icon = icon;
this.label = label;
this.icon = icon;
}
};
}
;
public ProfileAdapter(@NonNull Fragment fragment,
@NonNull GlideRequests glideRequests,
@Nullable ItemClickListener clickListener)
{
public ProfileAdapter(
@NonNull Fragment fragment,
@NonNull GlideRequests glideRequests,
@Nullable ItemClickListener clickListener) {
super();
this.fragment = fragment;
this.context = fragment.requireContext();
this.glideRequests = glideRequests;
this.clickListener = clickListener;
this.dcContext = DcHelper.getContext(context);
this.fragment = fragment;
this.context = fragment.requireContext();
this.glideRequests = glideRequests;
this.clickListener = clickListener;
this.dcContext = DcHelper.getContext(context);
this.layoutInflater = LayoutInflater.from(context);
this.selectedMembers= new HashSet<>();
this.selectedMembers = new HashSet<>();
}
@Override
@@ -125,32 +121,47 @@ public class ProfileAdapter extends RecyclerView.Adapter
@Override
public ProfileAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == ITEM_HEADER) {
final View item = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false);
final View item =
LayoutInflater.from(context)
.inflate(R.layout.contact_selection_list_divider, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_DIVIDER) {
final View item = LayoutInflater.from(context).inflate(R.layout.profile_divider, parent, false);
final View item =
LayoutInflater.from(context).inflate(R.layout.profile_divider, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_MEMBERS) {
final ContactSelectionListItem item = (ContactSelectionListItem)layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false);
final ContactSelectionListItem item =
(ContactSelectionListItem)
layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_SHARED_CHATS) {
final ConversationListItem item = (ConversationListItem)layoutInflater.inflate(R.layout.conversation_list_item_view, parent, false);
final ConversationListItem item =
(ConversationListItem)
layoutInflater.inflate(R.layout.conversation_list_item_view, parent, false);
item.hideItemDivider();
return new ViewHolder(item);
} else if (viewType == ITEM_SIGNATURE) {
final ProfileStatusItem item = (ProfileStatusItem)layoutInflater.inflate(R.layout.profile_status_item, parent, false);
final ProfileStatusItem item =
(ProfileStatusItem) layoutInflater.inflate(R.layout.profile_status_item, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_AVATAR) {
final ProfileAvatarItem item = (ProfileAvatarItem)layoutInflater.inflate(R.layout.profile_avatar_item, parent, false);
final ProfileAvatarItem item =
(ProfileAvatarItem) layoutInflater.inflate(R.layout.profile_avatar_item, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_ALL_MEDIA_BUTTON || viewType == ITEM_SEND_MESSAGE_BUTTON) {
final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_button, parent, false);
final ProfileTextItem item =
(ProfileTextItem)
layoutInflater.inflate(R.layout.profile_text_item_button, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_LAST_SEEN || viewType == ITEM_INTRODUCED_BY || viewType == ITEM_BLOCKED) {
final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_small, parent, false);
} else if (viewType == ITEM_LAST_SEEN
|| viewType == ITEM_INTRODUCED_BY
|| viewType == ITEM_BLOCKED) {
final ProfileTextItem item =
(ProfileTextItem) layoutInflater.inflate(R.layout.profile_text_item_small, parent, false);
return new ViewHolder(item);
} else {
final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item, parent, false);
final ProfileTextItem item =
(ProfileTextItem) layoutInflater.inflate(R.layout.profile_text_item, parent, false);
return new ViewHolder(item);
}
}
@@ -170,11 +181,9 @@ public class ProfileAdapter extends RecyclerView.Adapter
if (contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) {
name = context.getString(R.string.group_add_members);
}
else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) {
} else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) {
name = context.getString(R.string.qrshow_title);
}
else {
} else {
dcContact = dcContext.getContact(contactId);
name = dcContact.getDisplayName();
addr = dcContact.getAddr();
@@ -184,9 +193,12 @@ public class ProfileAdapter extends RecyclerView.Adapter
contactItem.set(glideRequests, contactId, dcContact, name, addr, label, false, true);
contactItem.setSelected(selectedMembers.contains(contactId));
contactItem.setOnClickListener(view -> clickListener.onMemberClicked(contactId));
contactItem.setOnLongClickListener(view -> {clickListener.onMemberLongClicked(contactId); return true;});
}
else if (holder.itemView instanceof ConversationListItem) {
contactItem.setOnLongClickListener(
view -> {
clickListener.onMemberLongClicked(contactId);
return true;
});
} else if (holder.itemView instanceof ConversationListItem) {
ConversationListItem conversationListItem = (ConversationListItem) holder.itemView;
int chatlistIndex = data.chatlistIndex;
@@ -194,39 +206,55 @@ public class ProfileAdapter extends RecyclerView.Adapter
DcChat chat = dcContext.getChat(chatId);
DcLot summary = itemDataSharedChats.getSummary(chatlistIndex, chat);
conversationListItem.bind(DcHelper.getThreadRecord(context, summary, chat),
itemDataSharedChats.getMsgId(chatlistIndex), summary, glideRequests,
Collections.emptySet(), false);
conversationListItem.bind(
DcHelper.getThreadRecord(context, summary, chat),
itemDataSharedChats.getMsgId(chatlistIndex),
summary,
glideRequests,
Collections.emptySet(),
false);
conversationListItem.setOnClickListener(view -> clickListener.onSharedChatClicked(chatId));
}
else if(holder.itemView instanceof ProfileStatusItem) {
} else if (holder.itemView instanceof ProfileStatusItem) {
ProfileStatusItem item = (ProfileStatusItem) holder.itemView;
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(dcContact == null); return true;});
item.setOnLongClickListener(
view -> {
clickListener.onStatusLongClicked(dcContact == null);
return true;
});
item.set(data.label);
}
else if(holder.itemView instanceof ProfileAvatarItem) {
} else if (holder.itemView instanceof ProfileAvatarItem) {
ProfileAvatarItem item = (ProfileAvatarItem) holder.itemView;
item.setAvatarClickListener(view -> clickListener.onAvatarClicked());
item.set(glideRequests, dcChat, dcContact, memberList);
}
else if(holder.itemView instanceof ProfileTextItem) {
} else if (holder.itemView instanceof ProfileTextItem) {
ProfileTextItem item = (ProfileTextItem) holder.itemView;
item.setOnClickListener(view -> clickListener.onSettingsClicked(data.viewType));
boolean tintIcon = data.viewType != ITEM_INTRODUCED_BY && data.viewType != ITEM_BLOCKED;
item.set(data.label, data.icon, tintIcon);
if (data.viewType == ITEM_BLOCKED) {
int padding = (int)((float)context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding) * 1.2);
item.setPadding(item.getPaddingLeft(), item.getPaddingTop(), item.getPaddingRight(), padding);
int padding =
(int)
((float)
context
.getResources()
.getDimensionPixelSize(R.dimen.contact_list_normal_padding)
* 1.2);
item.setPadding(
item.getPaddingLeft(), item.getPaddingTop(), item.getPaddingRight(), padding);
} else if (data.viewType == ITEM_INTRODUCED_BY) {
int padding = context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding);
item.setPadding(item.getPaddingLeft(), padding, item.getPaddingRight(), item.getPaddingBottom());
int padding =
context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding);
item.setPadding(
item.getPaddingLeft(), padding, item.getPaddingRight(), item.getPaddingBottom());
} else if (data.viewType == ITEM_ALL_MEDIA_BUTTON && dcChat != null) {
Util.runOnAnyBackgroundThread(() -> {
String c = getAllMediaCountString(dcChat.getId());
Util.runOnMain(() -> {
item.setValue(c);
});
});
Util.runOnAnyBackgroundThread(
() -> {
String c = getAllMediaCountString(dcChat.getId());
Util.runOnMain(
() -> {
item.setValue(c);
});
});
}
} else if (data.viewType == ITEM_HEADER) {
TextView textView = holder.itemView.findViewById(R.id.label);
@@ -236,10 +264,15 @@ public class ProfileAdapter extends RecyclerView.Adapter
public interface ItemClickListener {
void onSettingsClicked(int settingsId);
void onStatusLongClicked(boolean isMultiUser);
void onSharedChatClicked(int chatId);
void onMemberClicked(int contactId);
void onMemberLongClicked(int contactId);
void onAvatarClicked();
}
@@ -269,7 +302,11 @@ public class ProfileAdapter extends RecyclerView.Adapter
notifyDataSetChanged();
}
public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact, @Nullable DcChatlist sharedChats, @Nullable DcChat dcChat) {
public void changeData(
@Nullable int[] memberList,
@Nullable DcContact dcContact,
@Nullable DcChatlist sharedChats,
@Nullable DcChat dcChat) {
this.dcChat = dcChat;
this.dcContact = dcContact;
itemData.clear();
@@ -304,10 +341,18 @@ public class ProfileAdapter extends RecyclerView.Adapter
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
}
itemData.add(new ItemData(ITEM_ALL_MEDIA_BUTTON, context.getString(R.string.apps_and_media), R.drawable.ic_apps_24));
itemData.add(
new ItemData(
ITEM_ALL_MEDIA_BUTTON,
context.getString(R.string.apps_and_media),
R.drawable.ic_apps_24));
if (dcContact != null && !isDeviceTalk && !isSelfTalk) {
itemData.add(new ItemData(ITEM_SEND_MESSAGE_BUTTON, context.getString(R.string.send_message), R.drawable.ic_send_sms_white_24dp));
itemData.add(
new ItemData(
ITEM_SEND_MESSAGE_BUTTON,
context.getString(R.string.send_message),
R.drawable.ic_send_sms_white_24dp));
}
/*
@@ -316,19 +361,25 @@ public class ProfileAdapter extends RecyclerView.Adapter
String lastSeenTxt;
if (lastSeenTimestamp == 0) {
lastSeenTxt = context.getString(R.string.last_seen_unknown);
}
else {
lastSeenTxt = context.getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, lastSeenTimestamp));
} else {
lastSeenTxt =
context.getString(
R.string.last_seen_at,
DateUtils.getExtendedTimeSpanString(context, lastSeenTimestamp));
}
itemData.add(new ItemData(ITEM_LAST_SEEN, lastSeenTxt, 0));
}
*/
if (dcContact != null && !isDeviceTalk && !isSelfTalk && dcContact.isBlocked()) {
itemData.add(new ItemData(ITEM_BLOCKED, context.getString(R.string.contact_blocked), R.drawable.contact_blocked_24));
itemData.add(
new ItemData(
ITEM_BLOCKED,
context.getString(R.string.contact_blocked),
R.drawable.contact_blocked_24));
}
if (memberList!=null && !isInBroadcast && !isMailingList) {
if (memberList != null && !isInBroadcast && !isMailingList) {
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
if (dcChat != null) {
if (dcChat.canSend() && dcChat.isEncrypted()) {
@@ -358,9 +409,15 @@ public class ProfileAdapter extends RecyclerView.Adapter
if (verifierId == DcContact.DC_CONTACT_ID_SELF) {
introducedBy = context.getString(R.string.verified_by_you);
} else {
introducedBy = context.getString(R.string.verified_by, dcContext.getContact(verifierId).getDisplayName());
introducedBy =
context.getString(
R.string.verified_by, dcContext.getContact(verifierId).getDisplayName());
}
itemData.add(new ItemData(ITEM_INTRODUCED_BY, introducedBy, dcContact.isVerified()? R.drawable.ic_verified : 0));
itemData.add(
new ItemData(
ITEM_INTRODUCED_BY,
introducedBy,
dcContact.isVerified() ? R.drawable.ic_verified : 0));
} else if (dcContact.isVerified()) {
String introducedBy = context.getString(R.string.verified_by_unknown);
itemData.add(new ItemData(ITEM_INTRODUCED_BY, introducedBy, R.drawable.ic_verified));
@@ -371,8 +428,11 @@ public class ProfileAdapter extends RecyclerView.Adapter
}
public int ALL_MEDIA_COUNT_MAX = 500;
public int getAllMediaCount(int chatId) {
int c = dcContext.getChatMedia(chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO).length;
int c =
dcContext.getChatMedia(chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO)
.length;
if (c < ALL_MEDIA_COUNT_MAX) {
c += dcContext.getChatMedia(chatId, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0).length;
}
@@ -5,13 +5,10 @@ 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 com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import org.thoughtcrime.securesms.components.AvatarView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -22,12 +19,12 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class ProfileAvatarItem extends LinearLayout implements RecipientModifiedListener {
private AvatarView avatarView;
private TextView nameView;
private TextView subtitleView;
private AvatarView avatarView;
private TextView nameView;
private TextView subtitleView;
private Recipient recipient;
private GlideRequests glideRequests;
private Recipient recipient;
private GlideRequests glideRequests;
public ProfileAvatarItem(Context context) {
super(context);
@@ -40,14 +37,18 @@ public class ProfileAvatarItem extends LinearLayout implements RecipientModified
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatarView = findViewById(R.id.avatar);
nameView = findViewById(R.id.name);
subtitleView = findViewById(R.id.subtitle);
avatarView = findViewById(R.id.avatar);
nameView = findViewById(R.id.name);
subtitleView = findViewById(R.id.subtitle);
ViewUtil.setTextViewGravityStart(nameView, getContext());
}
public void set(@NonNull GlideRequests glideRequests, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable int[] members) {
public void set(
@NonNull GlideRequests glideRequests,
@Nullable DcChat dcChat,
@Nullable DcContact dcContact,
@Nullable int[] members) {
this.glideRequests = glideRequests;
int memberCount = members != null ? members.length : 0;
@@ -60,17 +61,27 @@ public class ProfileAvatarItem extends LinearLayout implements RecipientModified
if (dcChat.isMailingList()) {
subtitle = dcChat.getMailinglistAddr();
} else if (dcChat.isOutBroadcast()) {
subtitle = getContext().getResources().getQuantityString(R.plurals.n_recipients, memberCount, memberCount);
subtitle =
getContext()
.getResources()
.getQuantityString(R.plurals.n_recipients, memberCount, memberCount);
} else if (dcChat.getType() == DcChat.DC_CHAT_TYPE_GROUP) {
if (memberCount > 1 || Util.contains(members, DcContact.DC_CONTACT_ID_SELF)) {
subtitle = getContext().getResources().getQuantityString(R.plurals.n_members, memberCount, memberCount);
subtitle =
getContext()
.getResources()
.getQuantityString(R.plurals.n_members, memberCount, memberCount);
}
} else if (dcContact != null && !dcChat.isSelfTalk() && !dcChat.isDeviceTalk()) {
long timestamp = dcContact.getLastSeen();
if (timestamp == 0) {
subtitle = getContext().getString(R.string.last_seen_unknown);
} else {
subtitle = getContext().getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(getContext(), timestamp));
subtitle =
getContext()
.getString(
R.string.last_seen_at,
DateUtils.getExtendedTimeSpanString(getContext(), timestamp));
}
}
} else if (dcContact != null) {
@@ -81,7 +92,11 @@ public class ProfileAvatarItem extends LinearLayout implements RecipientModified
if (timestamp == 0) {
subtitle = getContext().getString(R.string.last_seen_unknown);
} else {
subtitle = getContext().getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(getContext(), timestamp));
subtitle =
getContext()
.getString(
R.string.last_seen_at,
DateUtils.getExtendedTimeSpanString(getContext(), timestamp));
}
}
@@ -115,12 +130,13 @@ public class ProfileAvatarItem extends LinearLayout implements RecipientModified
@Override
public void onModified(final Recipient recipient) {
if (this.recipient == recipient) {
Util.runOnMain(() -> {
avatarView.setAvatar(glideRequests, recipient, false);
DcContact contact = recipient.getDcContact();
avatarView.setSeenRecently(contact != null && contact.wasSeenRecently());
nameView.setText(recipient.toShortString());
});
Util.runOnMain(
() -> {
avatarView.setAvatar(glideRequests, recipient, false);
DcContact contact = recipient.getDcContact();
avatarView.setSeenRecently(contact != null && contact.wasSeenRecently());
nameView.setText(recipient.toShortString());
});
}
}
}
@@ -4,13 +4,15 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -18,13 +20,14 @@ import androidx.appcompat.view.ActionMode;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcChatlist;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -32,26 +35,21 @@ import org.thoughtcrime.securesms.qr.QrShowActivity;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ProfileFragment extends Fragment
implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate {
implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate {
private static final String TAG = ProfileFragment.class.getSimpleName();
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
private static final int REQUEST_CODE_PICK_CONTACT = 2;
private ActivityResultLauncher<Intent> pickContactLauncher;
private ProfileAdapter adapter;
private ActionMode actionMode;
private ActionMode actionMode;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
private DcContext dcContext;
protected int chatId;
private int contactId;
private DcContext dcContext;
protected int chatId;
private int contactId;
@Override
public void onCreate(Bundle bundle) {
@@ -60,10 +58,54 @@ public class ProfileFragment extends Fragment
chatId = getArguments() != null ? getArguments().getInt(CHAT_ID_EXTRA, -1) : -1;
contactId = getArguments().getInt(CONTACT_ID_EXTRA, -1);
dcContext = DcHelper.getContext(requireContext());
pickContactLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
Intent data = result.getData();
Log.i(
TAG,
"Received result from activity, resultCode="
+ result.getResultCode()
+ ", data="
+ data);
if (result.getResultCode() == Activity.RESULT_OK && data != null) {
List<Integer> selected =
data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> deselected =
data.getIntegerArrayListExtra(
ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(
() -> {
if (deselected != null) { // Remove members that were deselected
Log.i(TAG, deselected.size() + " members removed");
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
if (selected != null) { // Add new members
Log.i(TAG, selected.size() + " members added");
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
}
});
}
});
}
@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_fragment, container, false);
adapter = new ProfileAdapter(this, GlideApp.with(this), this);
@@ -73,7 +115,8 @@ public class ProfileFragment extends Fragment
ViewUtil.applyWindowInsets(list);
list.setAdapter(adapter);
list.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
list.setLayoutManager(
new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
update();
@@ -96,33 +139,34 @@ public class ProfileFragment extends Fragment
update();
}
private void update()
{
int[] memberList = null;
private void update() {
int[] memberList = null;
DcChatlist sharedChats = null;
DcChat dcChat = null;
DcContact dcContact = null;
if (contactId>0) { dcContact = dcContext.getContact(contactId); }
if (chatId>0) { dcChat = dcContext.getChat(chatId); }
if(dcChat!=null && dcChat.isMultiUser()) {
memberList = dcContext.getChatContacts(chatId);
if (contactId > 0) {
dcContact = dcContext.getContact(contactId);
}
else if(contactId>0 && contactId!=DcContact.DC_CONTACT_ID_SELF) {
if (chatId > 0) {
dcChat = dcContext.getChat(chatId);
}
if (dcChat != null && dcChat.isMultiUser()) {
memberList = dcContext.getChatContacts(chatId);
} else if (contactId > 0 && contactId != DcContact.DC_CONTACT_ID_SELF) {
sharedChats = dcContext.getChatlist(0, null, contactId);
}
adapter.changeData(memberList, dcContact, sharedChats, dcChat);
}
// handle events
// =========================================================================
@Override
public void onSettingsClicked(int settingsId) {
switch(settingsId) {
switch (settingsId) {
case ProfileAdapter.ITEM_ALL_MEDIA_BUTTON:
if (chatId > 0) {
Intent intent = new Intent(getActivity(), AllMediaActivity.class);
@@ -141,28 +185,31 @@ public class ProfileFragment extends Fragment
@Override
public void onStatusLongClicked(boolean isMultiUser) {
Context context = requireContext();
new AlertDialog.Builder(context)
.setTitle(isMultiUser? R.string.chat_description : R.string.pref_default_status_label)
.setItems(new CharSequence[]{
context.getString(R.string.menu_copy_to_clipboard)
},
(dialogInterface, i) -> {
Util.writeTextToClipboard(context, adapter.getStatusText());
Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show();
})
Context context = requireContext();
new AlertDialog.Builder(context)
.setTitle(isMultiUser ? R.string.chat_description : R.string.pref_default_status_label)
.setItems(
new CharSequence[] {context.getString(R.string.menu_copy_to_clipboard)},
(dialogInterface, i) -> {
Util.writeTextToClipboard(context, adapter.getStatusText());
Toast.makeText(
context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show();
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@Override
public void onMemberLongClicked(int contactId) {
if (contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL || contactId==DcContact.DC_CONTACT_ID_SELF) {
if (actionMode==null) {
if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL
|| contactId == DcContact.DC_CONTACT_ID_SELF) {
if (actionMode == null) {
DcChat dcChat = dcContext.getChat(chatId);
if (dcChat.canSend() && dcChat.isEncrypted()) {
adapter.toggleMemberSelection(contactId);
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
actionMode =
((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
}
} else {
onMemberClicked(contactId);
@@ -172,8 +219,9 @@ public class ProfileFragment extends Fragment
@Override
public void onMemberClicked(int contactId) {
if (actionMode!=null) {
if (contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL || contactId==DcContact.DC_CONTACT_ID_SELF) {
if (actionMode != null) {
if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL
|| contactId == DcContact.DC_CONTACT_ID_SELF) {
adapter.toggleMemberSelection(contactId);
if (adapter.getSelectedMembersCount() == 0) {
actionMode.finish();
@@ -182,14 +230,11 @@ public class ProfileFragment extends Fragment
actionMode.setTitle(String.valueOf(adapter.getSelectedMembersCount()));
}
}
}
else if(contactId==DcContact.DC_CONTACT_ID_ADD_MEMBER) {
} else if (contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) {
onAddMember();
}
else if(contactId==DcContact.DC_CONTACT_ID_QR_INVITE) {
} else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) {
onQrInvite();
}
else if(contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL) {
} else if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL) {
Intent intent = new Intent(getContext(), ProfileActivity.class);
intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, contactId);
startActivity(intent);
@@ -198,7 +243,7 @@ public class ProfileFragment extends Fragment
@Override
public void onAvatarClicked() {
ProfileActivity activity = (ProfileActivity)getActivity();
ProfileActivity activity = (ProfileActivity) getActivity();
activity.onEnlargeAvatar();
}
@@ -210,7 +255,7 @@ public class ProfileFragment extends Fragment
preselectedContacts.add(memberId);
}
intent.putExtra(ContactSelectionListFragment.PRESELECTED_CONTACTS, preselectedContacts);
startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
pickContactLauncher.launch(intent);
}
public void onQrInvite() {
@@ -282,16 +327,24 @@ public class ProfileFragment extends Fragment
readableToDelList.append(dcContext.getContact(toDelId).getDisplayName());
}
DcChat dcChat = dcContext.getChat(chatId);
AlertDialog dialog = new AlertDialog.Builder(requireContext())
.setPositiveButton(R.string.remove_desktop, (d, which) -> {
for (Integer toDelId : toDelIds) {
dcContext.removeContactFromChat(chatId, toDelId);
}
mode.finish();
})
.setNegativeButton(android.R.string.cancel, null)
.setMessage(getString(dcChat.isOutBroadcast() ? R.string.ask_remove_from_channel : R.string.ask_remove_members, readableToDelList))
.show();
AlertDialog dialog =
new AlertDialog.Builder(requireContext())
.setPositiveButton(
R.string.remove_desktop,
(d, which) -> {
for (Integer toDelId : toDelIds) {
dcContext.removeContactFromChat(chatId, toDelId);
}
mode.finish();
})
.setNegativeButton(android.R.string.cancel, null)
.setMessage(
getString(
dcChat.isOutBroadcast()
? R.string.ask_remove_from_channel
: R.string.ask_remove_members,
readableToDelList))
.show();
Util.redPositiveButton(dialog);
return true;
}
@@ -304,36 +357,4 @@ public class ProfileFragment extends Fragment
adapter.clearSelection();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(() -> {
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
}
});
}
}
}
@@ -5,16 +5,14 @@ import android.text.SpannableString;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import androidx.appcompat.widget.AppCompatTextView;
import org.thoughtcrime.securesms.util.Linkifier;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
public class ProfileStatusItem extends LinearLayout {
private AppCompatTextView statusTextView;
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
public ProfileStatusItem(Context context) {
super(context);
@@ -1,17 +1,14 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.util.ResUtil;
public class ProfileTextItem extends LinearLayout {
@@ -40,7 +37,10 @@ public class ProfileTextItem extends LinearLayout {
if (icon != 0) {
Drawable orgDrawable = ContextCompat.getDrawable(getContext(), icon);
if (orgDrawable != null) {
Drawable drawable = orgDrawable.mutate(); // avoid global state modification and showing eg. app-icon tinted also elsewhere
Drawable drawable =
orgDrawable
.mutate(); // avoid global state modification and showing eg. app-icon tinted also
// elsewhere
drawable = DrawableCompat.wrap(drawable);
if (tint) {
int color = ResUtil.getColor(getContext(), R.attr.colorAccent);
@@ -8,112 +8,117 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.provider.OpenableColumns;
import android.util.Log;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import de.cketti.safecontentresolver.SafeContentResolver;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import de.cketti.safecontentresolver.SafeContentResolver;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
public class ResolveMediaTask extends AsyncTask<Uri, Void, Uri> {
private static final String TAG = ResolveMediaTask.class.getSimpleName();
private static final String TAG = ResolveMediaTask.class.getSimpleName();
interface OnMediaResolvedListener {
void onMediaResolved(Uri uri);
interface OnMediaResolvedListener {
void onMediaResolved(Uri uri);
}
private final WeakReference<Activity> contextRef;
private final WeakReference<OnMediaResolvedListener> listenerWeakReference;
private static final HashSet<ResolveMediaTask> instances = new HashSet<>();
ResolveMediaTask(Activity activityContext, ResolveMediaTask.OnMediaResolvedListener listener) {
this.contextRef = new WeakReference<>(activityContext);
this.listenerWeakReference = new WeakReference<>(listener);
instances.add(this);
}
@Override
protected Uri doInBackground(Uri... uris) {
try {
Uri uri = uris[0];
if (uris.length != 1 || uri == null) {
return null;
}
InputStream inputStream;
String fileName = null;
Long fileSize = null;
SafeContentResolver safeContentResolver = SafeContentResolver.newInstance(contextRef.get());
inputStream = safeContentResolver.openInputStream(uri);
if (inputStream == null) {
return null;
}
Cursor cursor =
contextRef
.get()
.getContentResolver()
.query(
uri,
new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE},
null,
null,
null);
try {
if (cursor != null && cursor.moveToFirst()) {
try {
fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
}
}
} finally {
if (cursor != null) cursor.close();
}
private final WeakReference<Activity> contextRef;
private final WeakReference<OnMediaResolvedListener> listenerWeakReference;
if (fileName == null) {
fileName = uri.getLastPathSegment();
}
private static final HashSet<ResolveMediaTask> instances = new HashSet<>();
ResolveMediaTask(Activity activityContext, ResolveMediaTask.OnMediaResolvedListener listener) {
this.contextRef = new WeakReference<>(activityContext);
this.listenerWeakReference = new WeakReference<>(listener);
instances.add(this);
}
@Override
protected Uri doInBackground(Uri... uris) {
try {
Uri uri = uris[0];
if (uris.length != 1 || uri == null) {
return null;
}
InputStream inputStream;
String fileName = null;
Long fileSize = null;
SafeContentResolver safeContentResolver = SafeContentResolver.newInstance(contextRef.get());
inputStream = safeContentResolver.openInputStream(uri);
if (inputStream == null) {
return null;
}
Cursor cursor = contextRef.get().getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
try {
fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
}
}
} finally {
if (cursor != null) cursor.close();
}
if (fileName == null) {
fileName = uri.getLastPathSegment();
}
String mimeType = getMimeType(contextRef.get(), uri);
return PersistentBlobProvider.getInstance().create(contextRef.get(), inputStream, mimeType, fileName, fileSize);
} catch (NullPointerException | FileNotFoundException ioe) {
Log.w(TAG, ioe);
return null;
}
}
@Override
protected void onPostExecute(Uri uri) {
instances.remove(this);
if (!this.isCancelled()) {
listenerWeakReference.get().onMediaResolved(uri);
}
}
@Override
protected void onCancelled() {
instances.remove(this);
super.onCancelled();
listenerWeakReference.get().onMediaResolved(null);
}
public static boolean isExecuting() {
return !instances.isEmpty();
}
public static void cancelTasks() {
for (ResolveMediaTask task : instances) {
task.cancel(true);
}
}
private boolean hasFileScheme(Uri uri) {
if (uri == null) {
return false;
}
return "file".equals(uri.getScheme());
String mimeType = getMimeType(contextRef.get(), uri);
return PersistentBlobProvider.getInstance()
.create(contextRef.get(), inputStream, mimeType, fileName, fileSize);
} catch (NullPointerException | FileNotFoundException ioe) {
Log.w(TAG, ioe);
return null;
}
}
@Override
protected void onPostExecute(Uri uri) {
instances.remove(this);
if (!this.isCancelled()) {
listenerWeakReference.get().onMediaResolved(uri);
}
}
@Override
protected void onCancelled() {
instances.remove(this);
super.onCancelled();
listenerWeakReference.get().onMediaResolved(null);
}
public static boolean isExecuting() {
return !instances.isEmpty();
}
public static void cancelTasks() {
for (ResolveMediaTask task : instances) {
task.cancel(true);
}
}
private boolean hasFileScheme(Uri uri) {
if (uri == null) {
return false;
}
return "file".equals(uri.getScheme());
}
}
@@ -2,15 +2,11 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Parcelable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
* Like LinearLayoutManager but you can set a starting position
*/
/** Like LinearLayoutManager but you can set a starting position */
class SetStartingPositionLinearLayoutManager extends LinearLayoutManager {
private int pendingStartingPos = -1;
@@ -46,4 +42,4 @@ class SetStartingPositionLinearLayoutManager extends LinearLayoutManager {
public void setStartingPosition(int position) {
pendingStartingPos = position;
}
}
}
@@ -24,15 +24,14 @@ import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.pm.ShortcutManagerCompat;
import com.b44t.messenger.DcContext;
import java.util.ArrayList;
import java.util.List;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -41,24 +40,21 @@ import org.thoughtcrime.securesms.util.MailtoUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import java.util.ArrayList;
import java.util.List;
/**
* An activity to quickly share content with chats
*
* @author Jake McGinty
*/
public class ShareActivity extends PassphraseRequiredActionBarActivity implements ResolveMediaTask.OnMediaResolvedListener
{
public class ShareActivity extends PassphraseRequiredActionBarActivity
implements ResolveMediaTask.OnMediaResolvedListener {
private static final String TAG = ShareActivity.class.getSimpleName();
public static final String EXTRA_ACC_ID = "acc_id";
public static final String EXTRA_CHAT_ID = "chat_id";
private ArrayList<Uri> resolvedExtras;
private DcContext dcContext;
private boolean isResolvingUrisOnMainThread;
private ArrayList<Uri> resolvedExtras;
private DcContext dcContext;
private boolean isResolvingUrisOnMainThread;
@Override
protected void onPreCreate() {
@@ -115,10 +111,10 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
}
if (Intent.ACTION_SEND.equals(getIntent().getAction()) &&
getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
streamExtras.add(uri);
if (Intent.ACTION_SEND.equals(getIntent().getAction())
&& getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
streamExtras.add(uri);
} else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) {
streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
} else if (getIntent().getData() != null) {
@@ -137,7 +133,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
private boolean needsFilePermission(List<Uri> uris) {
for(Uri uri : uris) {
for (Uri uri : uris) {
// uri may be null, however, hasFileScheme() just returns false in this case
if (hasFileScheme(uri)) {
return true;
@@ -148,17 +144,19 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
private void requestPermissionForFiles(List<Uri> streamExtras) {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.alwaysGrantOnSdk33()
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> resolveUris(streamExtras))
.onAnyDenied(this::abortShare)
.execute();
.request(
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.alwaysGrantOnSdk33()
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> resolveUris(streamExtras))
.onAnyDenied(this::abortShare)
.execute();
}
@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);
}
@@ -187,7 +185,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
case android.R.id.home:
finish();
return true;
}
return false;
}
@@ -199,15 +199,16 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
if (!ResolveMediaTask.isExecuting() && !isResolvingUrisOnMainThread) {
handleResolvedMedia(getIntent());
handleResolvedMedia(getIntent());
}
}
private void handleResolvedMedia(Intent intent) {
int accId = intent.getIntExtra(EXTRA_ACC_ID, -1);
int chatId = intent.getIntExtra(EXTRA_CHAT_ID, -1);
int accId = intent.getIntExtra(EXTRA_ACC_ID, -1);
int chatId = intent.getIntExtra(EXTRA_CHAT_ID, -1);
// the intent coming from shortcuts in the share selector might not include the custom extras but the shortcut ID
// the intent coming from shortcuts in the share selector might not include the custom extras
// but the shortcut ID
String shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
if ((chatId == -1 || accId == -1) && shortcutId != null && shortcutId.startsWith("chat-")) {
String[] args = shortcutId.split("-");
@@ -219,47 +220,47 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
/*
usually, external app will try to start "e-mail sharing" intent, providing it:
1. address(s), packed in array, marked as Intent.EXTRA_EMAIL - mandatory
2. shared content (files, pics, video), packed in Intent.EXTRA_STREAM - optional
usually, external app will try to start "e-mail sharing" intent, providing it:
1. address(s), packed in array, marked as Intent.EXTRA_EMAIL - mandatory
2. shared content (files, pics, video), packed in Intent.EXTRA_STREAM - optional
here is a sample code to trigger this routine from within external app:
here is a sample code to trigger this routine from within external app:
try {
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
"mailto", "someone@example.com", null));
File f = new File(getFilesDir() + "/somebinaryfile.bin");
f.createNewFile();
f.setReadable(true, false);
byte[] b = new byte[1024];
new Random().nextBytes(b);
FileOutputStream fOut = new FileOutputStream(f);
DataOutputStream dataStream = new DataOutputStream(fOut);
dataStream.write(b);
dataStream.close();
try {
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
"mailto", "someone@example.com", null));
File f = new File(getFilesDir() + "/somebinaryfile.bin");
f.createNewFile();
f.setReadable(true, false);
byte[] b = new byte[1024];
new Random().nextBytes(b);
FileOutputStream fOut = new FileOutputStream(f);
DataOutputStream dataStream = new DataOutputStream(fOut);
dataStream.write(b);
dataStream.close();
Uri sharedURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", f);
emailIntent.setAction(Intent.ACTION_SEND);
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"someone@example.com"});
emailIntent.setType("text/plain");
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
emailIntent.putExtra(Intent.EXTRA_STREAM, sharedURI);
Uri sharedURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", f);
emailIntent.setAction(Intent.ACTION_SEND);
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"someone@example.com"});
emailIntent.setType("text/plain");
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
emailIntent.putExtra(Intent.EXTRA_STREAM, sharedURI);
// to EXPLICITLY fire DC's sharing activity:
// emailIntent.setComponent(new ComponentName("com.b44t.messenger.beta", "org.thoughtcrime.securesms.ShareActivity"));
// to EXPLICITLY fire DC's sharing activity:
// emailIntent.setComponent(new ComponentName("com.b44t.messenger.beta", "org.thoughtcrime.securesms.ShareActivity"));
startActivity(emailIntent);
} catch (IOException e) {
e.printStackTrace();
}catch (ActivityNotFoundException e) {
}
*/
startActivity(emailIntent);
} catch (IOException e) {
e.printStackTrace();
}catch (ActivityNotFoundException e) {
}
*/
if(chatId == -1 && extraEmail != null && extraEmail.length > 0) {
if (chatId == -1 && extraEmail != null && extraEmail.length > 0) {
final String addr = extraEmail[0];
int contactId = dcContext.lookupContactIdByAddr(addr);
if(contactId == 0) {
if (contactId == 0) {
contactId = dcContext.createContact(null, addr);
}
@@ -290,9 +291,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
ShareUtil.setSharedUris(intent, resolvedExtras);
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text==null) {
if (text == null) {
CharSequence cs = getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT);
if (cs!=null) {
if (cs != null) {
text = cs.toString();
}
}
@@ -320,10 +321,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
private boolean hasFileScheme(Uri uri) {
if (uri==null) {
if (uri == null) {
return false;
}
return "file".equals(uri.getScheme());
}
}
@@ -1,30 +1,44 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
public class ShareLocationDialog {
public static void show(final Context context, final @NonNull ShareLocationDurationSelectionListener listener) {
public static void show(
final Context context, final @NonNull ShareLocationDurationSelectionListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.title_share_location);
builder.setItems(R.array.share_location_durations, (dialog, which) -> {
final int shareLocationUnit;
builder.setItems(
R.array.share_location_durations,
(dialog, which) -> {
final int shareLocationUnit;
switch (which) {
default:
case 0: shareLocationUnit = 5 * 60; break;
case 1: shareLocationUnit = 30 * 60; break;
case 2: shareLocationUnit = 60 * 60; break;
case 3: shareLocationUnit = 2 * 60 * 60; break;
case 4: shareLocationUnit = 6 * 60 * 60; break;
case 5: shareLocationUnit = 12 * 60 * 60; break;
}
switch (which) {
default:
case 0:
shareLocationUnit = 5 * 60;
break;
case 1:
shareLocationUnit = 30 * 60;
break;
case 2:
shareLocationUnit = 60 * 60;
break;
case 3:
shareLocationUnit = 2 * 60 * 60;
break;
case 4:
shareLocationUnit = 6 * 60 * 60;
break;
case 5:
shareLocationUnit = 12 * 60 * 60;
break;
}
listener.onSelected(shareLocationUnit);
});
listener.onSelected(shareLocationUnit);
});
builder.setNegativeButton(R.string.cancel, null);
builder.show();
@@ -7,21 +7,19 @@ import android.app.Activity;
import android.content.Context;
import android.text.util.Linkify;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import java.util.Locale;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.Prefs;
import java.util.Locale;
public class StatsSending {
/** @noinspection unused: We will start adding a device message once stats-sending is tested a bit */
/**
* @noinspection unused: We will start adding a device message once stats-sending is tested a bit
*/
public static void maybeAddStatsSendingDeviceMsg(Context context) {
if (Prefs.getStatsDeviceMsgId(context) != 0) {
return;
@@ -33,15 +31,15 @@ public class StatsSending {
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
msg.setText(context.getString(R.string.stats_device_message));
int msgId = dcContext.addDeviceMsg("stats_device_message", msg);
if (msgId!=0) {
if (msgId != 0) {
Prefs.setStatsDeviceMsgId(context, msgId);
}
}
public static boolean isStatsSendingDeviceMsg(Context context, DcMsg msg) {
return msg != null
&& msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE
&& msg.getId() == Prefs.getStatsDeviceMsgId(context);
&& msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE
&& msg.getId() == Prefs.getStatsDeviceMsgId(context);
}
public static void statsDeviceMsgTapped(Activity activity) {
@@ -52,17 +50,22 @@ public class StatsSending {
}
}
public static void showStatsConfirmationDialog(Activity activity, Runnable onConfigChangedListener) {
AlertDialog d = new AlertDialog.Builder(activity)
.setMessage(R.string.stats_confirmation_dialog)
.setNegativeButton(R.string.cancel, (_d, i) -> {})
.setPositiveButton(R.string.yes, (_d, i) -> {
DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "1");
onConfigChangedListener.run();
showStatsThanksDialog(activity);
})
.setNeutralButton(R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending"))
.create();
public static void showStatsConfirmationDialog(
Activity activity, Runnable onConfigChangedListener) {
AlertDialog d =
new AlertDialog.Builder(activity)
.setMessage(R.string.stats_confirmation_dialog)
.setNegativeButton(R.string.cancel, (_d, i) -> {})
.setPositiveButton(
R.string.yes,
(_d, i) -> {
DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "1");
onConfigChangedListener.run();
showStatsThanksDialog(activity);
})
.setNeutralButton(
R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending"))
.create();
d.show();
try {
//noinspection DataFlowIssue
@@ -76,25 +79,35 @@ public class StatsSending {
private static void showStatsThanksDialog(Activity activity) {
String stats_id = DcHelper.get(activity, DcHelper.CONFIG_STATS_ID);
new AlertDialog.Builder(activity)
.setMessage(R.string.stats_thanks)
.setNeutralButton(R.string.no, (d, i) -> {})
.setPositiveButton(R.string.yes, (d, i) -> {
String ln = Locale.getDefault().getLanguage();
IntentUtils.showInBrowser(activity, "https://cispa.qualtrics.com/jfe/form/SV_9YmhkpGa48KxfLg?id=" + stats_id + "&ln=" + ln);
})
.show();
.setMessage(R.string.stats_thanks)
.setNeutralButton(R.string.no, (d, i) -> {})
.setPositiveButton(
R.string.yes,
(d, i) -> {
String ln = Locale.getDefault().getLanguage();
IntentUtils.showInBrowser(
activity,
"https://cispa.qualtrics.com/jfe/form/SV_9YmhkpGa48KxfLg?id="
+ stats_id
+ "&ln="
+ ln);
})
.show();
}
public static void showStatsDisableDialog(Activity activity) {
AlertDialog d = new AlertDialog.Builder(activity)
.setMessage(R.string.stats_disable_dialog)
.setNegativeButton(R.string.disable, (_d, i) -> {
DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "0");
})
.setPositiveButton(R.string.stats_keep_sending, (_d, i) -> {
})
.setNeutralButton(R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending"))
.create();
AlertDialog d =
new AlertDialog.Builder(activity)
.setMessage(R.string.stats_disable_dialog)
.setNegativeButton(
R.string.disable,
(_d, i) -> {
DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "0");
})
.setPositiveButton(R.string.stats_keep_sending, (_d, i) -> {})
.setNeutralButton(
R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending"))
.create();
d.show();
}
}
@@ -9,18 +9,18 @@ public class TransportOption {
NORMAL_MAIL
}
private final int drawable;
private final @NonNull String text;
private final @NonNull String composeHint;
private final int drawable;
private final @NonNull String text;
private final @NonNull String composeHint;
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
@NonNull String text,
@NonNull String composeHint)
{
this.drawable = drawable;
this.text = text;
this.composeHint = composeHint;
public TransportOption(
@NonNull Type type,
@DrawableRes int drawable,
@NonNull String text,
@NonNull String composeHint) {
this.drawable = drawable;
this.text = text;
this.composeHint = composeHint;
}
public @NonNull Type getType() {
@@ -3,26 +3,23 @@ package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.TransportOption.Type;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.util.guava.Optional;
public class TransportOptions {
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
private final Context context;
private final List<TransportOption> enabledTransports;
private final Context context;
private final List<TransportOption> enabledTransports;
private Type defaultTransportType = Type.NORMAL_MAIL;
private Optional<TransportOption> selectedOption = Optional.absent();
private Type defaultTransportType = Type.NORMAL_MAIL;
private Optional<TransportOption> selectedOption = Optional.absent();
public TransportOptions(Context context) {
this.context = context;
this.enabledTransports = initializeAvailableTransports();
this.context = context;
this.enabledTransports = initializeAvailableTransports();
}
public void reset() {
@@ -48,7 +45,7 @@ public class TransportOptions {
}
}
public void setSelectedTransport(@Nullable TransportOption transportOption) {
public void setSelectedTransport(@Nullable TransportOption transportOption) {
this.selectedOption = Optional.fromNullable(transportOption);
notifyTransportChangeListeners();
}
@@ -76,9 +73,12 @@ public class TransportOptions {
private List<TransportOption> initializeAvailableTransports() {
List<TransportOption> results = new LinkedList<>();
results.add(new TransportOption(Type.NORMAL_MAIL, R.drawable.ic_send_sms_white_24dp,
context.getString(R.string.menu_send),
context.getString(R.string.chat_input_placeholder)));
results.add(
new TransportOption(
Type.NORMAL_MAIL,
R.drawable.ic_send_sms_white_24dp,
context.getString(R.string.menu_send),
context.getString(R.string.chat_input_placeholder)));
return results;
}
@@ -7,12 +7,9 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
import org.thoughtcrime.securesms.util.ViewUtil;
public class TransportOptionsAdapter extends BaseAdapter {
@@ -20,11 +17,10 @@ public class TransportOptionsAdapter extends BaseAdapter {
private List<TransportOption> enabledTransports;
public TransportOptionsAdapter(@NonNull Context context,
@NonNull List<TransportOption> enabledTransports)
{
public TransportOptionsAdapter(
@NonNull Context context, @NonNull List<TransportOption> enabledTransports) {
super();
this.inflater = LayoutInflater.from(context);
this.inflater = LayoutInflater.from(context);
this.enabledTransports = enabledTransports;
}
@@ -53,10 +49,10 @@ public class TransportOptionsAdapter extends BaseAdapter {
convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false);
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = ViewUtil.findById(convertView, R.id.icon);
TextView textView = ViewUtil.findById(convertView, R.id.text);
TextView subtextView = ViewUtil.findById(convertView, R.id.subtext);
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = ViewUtil.findById(convertView, R.id.icon);
TextView textView = ViewUtil.findById(convertView, R.id.text);
TextView subtextView = ViewUtil.findById(convertView, R.id.subtext);
imageView.setImageResource(transport.getDrawable());
textView.setText(transport.getDescription());
@@ -4,30 +4,32 @@ import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.ListPopupWindow;
import java.util.LinkedList;
import java.util.List;
public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener {
private final TransportOptionsAdapter adapter;
private final SelectedListener listener;
private final SelectedListener listener;
public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
public TransportOptionsPopup(
@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
super(context);
this.listener = listener;
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
setVerticalOffset(
context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
setHorizontalOffset(
context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
setModal(true);
setAnchorView(anchor);
setAdapter(adapter);
setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
setContentWidth(
context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
setOnItemClickListener(this);
}
@@ -40,11 +42,10 @@ public class TransportOptionsPopup extends ListPopupWindow implements ListView.O
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
listener.onSelected((TransportOption)adapter.getItem(position));
listener.onSelected((TransportOption) adapter.getItem(position));
}
public interface SelectedListener {
void onSelected(TransportOption option);
}
}
@@ -12,7 +12,6 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
@@ -21,44 +20,46 @@ import androidx.webkit.ProxyConfig;
import androidx.webkit.ProxyController;
import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewFeature;
import chat.delta.rpc.types.SecurejoinSource;
import java.net.IDN;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import chat.delta.rpc.types.SecurejoinSource;
import java.net.IDN;
public class WebViewActivity extends PassphraseRequiredActionBarActivity
implements SearchView.OnQueryTextListener,
WebView.FindListener
{
implements SearchView.OnQueryTextListener, WebView.FindListener {
private static final String TAG = WebViewActivity.class.getSimpleName();
protected WebView webView;
/** Return true the window content should display fullscreen/edge-to-edge ex. in the integrated maps app */
protected boolean immersiveMode() { return false; }
/**
* Return true the window content should display fullscreen/edge-to-edge ex. in the integrated
* maps app
*/
protected boolean immersiveMode() {
return false;
}
protected boolean shouldAskToOpenLink() { return false; }
protected boolean shouldAskToOpenLink() {
return false;
}
protected void toggleFakeProxy(boolean enable) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
if (enable) {
// Set proxy to non-routable address.
ProxyConfig proxyConfig = new ProxyConfig.Builder()
.removeImplicitRules()
.addProxyRule("0.0.0.0")
.build();
ProxyController.getInstance().setProxyOverride(proxyConfig, Runnable::run, () -> Log.i(TAG, "Set WebView proxy."));
ProxyConfig proxyConfig =
new ProxyConfig.Builder().removeImplicitRules().addProxyRule("0.0.0.0").build();
ProxyController.getInstance()
.setProxyOverride(proxyConfig, Runnable::run, () -> Log.i(TAG, "Set WebView proxy."));
} else {
ProxyController.getInstance().clearProxyOverride(Runnable::run, () -> Log.i(TAG, "Cleared WebView proxy."));
ProxyController.getInstance()
.clearProxyOverride(Runnable::run, () -> Log.i(TAG, "Cleared WebView proxy."));
}
} else {
Log.w(TAG, "Cannot " + (enable? "set": "clear") + " WebView proxy.");
Log.w(TAG, "Cannot " + (enable ? "set" : "clear") + " WebView proxy.");
}
}
@@ -72,65 +73,70 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
webView = findViewById(R.id.webview);
if(immersiveMode()) {
if (immersiveMode()) {
// set a shadow in the status bar to make it more readable
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
findViewById(R.id.status_bar_background)
.setBackgroundResource(R.drawable.search_toolbar_shadow);
} else {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
ViewUtil.applyWindowInsets(
findViewById(R.id.content_container), true, true, true, true, true, false);
}
webView.setWebViewClient(new WebViewClient() {
// IMPORTANT: this is will likely not be called inside iframes unless target=_blank is used in the anchor/link tag.
// `shouldOverrideUrlLoading()` is called when the user clicks a URL,
// returning `true` causes the WebView to abort loading the URL,
// returning `false` causes the WebView to continue loading the URL as usual.
// the method is not called for POST request nor for on-page-links.
//
// nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and
// `shouldOverrideUrlLoading(WebResourceRequest)` shall be used.
// the new one has the same functionality, and the old one still exist,
// so, to support all systems, for now, using the old one seems to be the simplest way.
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null) {
String schema = url.split(":")[0].toLowerCase();
switch (schema) {
case "http":
case "https":
case "gemini":
case "tel":
case "sms":
case "mailto":
case "openpgp4fpr":
case "geo":
case "dcaccount":
case "dclogin":
return openOnlineUrl(url);
webView.setWebViewClient(
new WebViewClient() {
// IMPORTANT: this is will likely not be called inside iframes unless target=_blank is
// used in the anchor/link tag.
// `shouldOverrideUrlLoading()` is called when the user clicks a URL,
// returning `true` causes the WebView to abort loading the URL,
// returning `false` causes the WebView to continue loading the URL as usual.
// the method is not called for POST request nor for on-page-links.
//
// nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and
// `shouldOverrideUrlLoading(WebResourceRequest)` shall be used.
// the new one has the same functionality, and the old one still exist,
// so, to support all systems, for now, using the old one seems to be the simplest way.
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null) {
String schema = url.split(":")[0].toLowerCase();
switch (schema) {
case "http":
case "https":
case "gemini":
case "tel":
case "sms":
case "mailto":
case "openpgp4fpr":
case "geo":
case "dcaccount":
case "dclogin":
return openOnlineUrl(url);
}
}
return true; // returning `true` aborts loading
}
}
return true; // returning `true` aborts loading
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
WebResourceResponse res = interceptRequest(url);
if (res!=null) {
return res;
}
return super.shouldInterceptRequest(view, url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
WebResourceResponse res = interceptRequest(url);
if (res != null) {
return res;
}
return super.shouldInterceptRequest(view, url);
}
@Override
@RequiresApi(21)
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebResourceResponse res = interceptRequest(request.getUrl().toString());
if (res!=null) {
return res;
}
return super.shouldInterceptRequest(view, request);
}
});
@Override
@RequiresApi(21)
public WebResourceResponse shouldInterceptRequest(
WebView view, WebResourceRequest request) {
WebResourceResponse res = interceptRequest(request.getUrl().toString());
if (res != null) {
return res;
}
return super.shouldInterceptRequest(view, request);
}
});
webView.setFindListener(this);
// disable "safe browsing" as this has privacy issues,
@@ -143,10 +149,15 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
protected void setForceDark() {
if (Build.VERSION.SDK_INT <= 32 && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// needed for older API (tested on android7) that do not set `color-scheme` without the following hint
WebSettingsCompat.setForceDark(webView.getSettings(),
DynamicTheme.isDarkTheme(this) ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF);
if (Build.VERSION.SDK_INT <= 32
&& WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// needed for older API (tested on android7) that do not set `color-scheme` without the
// following hint
WebSettingsCompat.setForceDark(
webView.getSettings(),
DynamicTheme.isDarkTheme(this)
? WebSettingsCompat.FORCE_DARK_ON
: WebSettingsCompat.FORCE_DARK_OFF);
}
}
@@ -177,29 +188,31 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
try {
MenuItem searchItem = menu.findItem(R.id.menu_search_web_view);
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(final MenuItem item) {
searchMenu = menu;
WebViewActivity.this.lastQuery = "";
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true);
return true;
}
searchItem.setOnActionExpandListener(
new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(final MenuItem item) {
searchMenu = menu;
WebViewActivity.this.lastQuery = "";
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true);
return true;
}
@Override
public boolean onMenuItemActionCollapse(final MenuItem item) {
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false);
return true;
}
});
@Override
public boolean onMenuItemActionCollapse(final MenuItem item) {
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false);
return true;
}
});
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
searchView.setQueryHint(getString(R.string.search));
searchView.setIconifiedByDefault(true);
// hide the [X] beside the search field - this is too much noise, search can be aborted eg. by "back"
// hide the [X] beside the search field - this is too much noise, search can be aborted eg. by
// "back"
ImageView closeBtn = searchView.findViewById(R.id.search_close_btn);
if (closeBtn!=null) {
if (closeBtn != null) {
closeBtn.setEnabled(false);
closeBtn.setImageDrawable(null);
}
@@ -211,18 +224,17 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
return true;
}
// search
private Menu searchMenu = null;
private Menu searchMenu = null;
private String lastQuery = "";
private Toast lastToast = null;
private Toast lastToast = null;
private void updateResultCounter(int curr, int total) {
if (searchMenu!=null) {
if (searchMenu != null) {
MenuItem item = searchMenu.findItem(R.id.menu_search_counter);
if (curr!=-1) {
item.setTitle(String.format("%d/%d", total==0? 0 : curr+1, total));
if (curr != -1) {
item.setTitle(String.format("%d/%d", total == 0 ? 0 : curr + 1, total));
item.setVisible(true);
} else {
item.setVisible(false);
@@ -239,7 +251,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
public boolean onQueryTextChange(String query) {
String normQuery = query.trim();
lastQuery = normQuery;
if (lastToast!=null) {
if (lastToast != null) {
lastToast.cancel();
lastToast = null;
}
@@ -248,10 +260,10 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
@Override
public void onFindResultReceived (int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)
{
public void onFindResultReceived(
int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
if (isDoneCounting) {
if (numberOfMatches>0) {
if (numberOfMatches > 0) {
updateResultCounter(activeMatchOrdinal, numberOfMatches);
} else {
if (lastQuery.isEmpty()) {
@@ -269,7 +281,6 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
}
// other actions
@Override
@@ -310,15 +321,17 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
if (shouldAskToOpenLink()) {
new AlertDialog.Builder(this)
.setTitle(R.string.open_url_confirmation)
.setMessage(IDN.toASCII(url))
.setNeutralButton(R.string.cancel, null)
.setPositiveButton(R.string.open, (d, w) -> IntentUtils.showInBrowser(this, url))
.setNegativeButton(R.string.global_menu_edit_copy_desktop, (d, w) -> {
Util.writeTextToClipboard(this, url);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
})
.show();
.setTitle(R.string.open_url_confirmation)
.setMessage(IDN.toASCII(url))
.setNeutralButton(R.string.cancel, null)
.setPositiveButton(R.string.open, (d, w) -> IntentUtils.showInBrowser(this, url))
.setNegativeButton(
R.string.global_menu_edit_copy_desktop,
(d, w) -> {
Util.writeTextToClipboard(this, url);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
})
.show();
} else {
IntentUtils.showInBrowser(this, url);
}
@@ -25,7 +25,6 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
@@ -33,23 +32,13 @@ import androidx.core.app.TaskStackBuilder;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.common.base.Charsets;
import org.json.JSONObject;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -60,11 +49,16 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.json.JSONObject;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = WebxdcActivity.class.getSimpleName();
private static final String EXTRA_ACCOUNT_ID = "accountId";
private static final String EXTRA_APP_MSG_ID = "appMessageId";
@@ -109,8 +103,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
e.printStackTrace();
}
if (msgId == 0) {
Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG).show();
return;
Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG)
.show();
return;
}
dcContext.setConfigInt("ui.maps_version", mapsVersion);
}
@@ -125,13 +120,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
openWebxdcActivity(context, instance.getId(), false, href);
}
public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar, String href) {
public static void openWebxdcActivity(
Context context, int msgId, boolean hideActionBar, String href) {
if (!Util.isClickedRecently()) {
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
}
}
private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar, String href) {
private static Intent getWebxdcIntent(
Context context, int msgId, boolean hideActionBar, String href) {
DcContext dcContext = DcHelper.getContext(context);
Intent intent = new Intent(context, WebxdcActivity.class);
intent.setAction(Intent.ACTION_VIEW);
@@ -145,20 +142,23 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) {
DcContext dcContext = DcHelper.getContext(context);
final Intent chatIntent = new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
.setAction(Intent.ACTION_VIEW);
final Intent chatIntent =
new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
.setAction(Intent.ACTION_VIEW);
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
.addNextIntent(webxdcIntent)
.getIntents();
.addNextIntentWithParentStack(chatIntent)
.addNextIntent(webxdcIntent)
.getIntents();
}
@Override
protected boolean immersiveMode() { return hideActionBar; }
protected boolean immersiveMode() {
return hideActionBar;
}
@Override
protected void onCreate(Bundle state, boolean ready) {
@@ -169,29 +169,36 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
rpc = DcHelper.getRpc(this);
initTTS();
webView.setWebChromeClient(
new WebChromeClient() {
@Override
@RequiresApi(21)
public boolean onShowFileChooser(
WebView webView,
ValueCallback<Uri[]> filePathCallback,
WebChromeClient.FileChooserParams fileChooserParams) {
if (WebxdcActivity.this.filePathCallback != null) {
WebxdcActivity.this.filePathCallback.onReceiveValue(null);
}
WebxdcActivity.this.filePathCallback = filePathCallback;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(
Intent.EXTRA_ALLOW_MULTIPLE,
fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
WebxdcActivity.this.startActivityForResult(
Intent.createChooser(intent, getString(R.string.select)), REQUEST_CODE_FILE_PICKER);
return true;
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
@RequiresApi(21)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
if (WebxdcActivity.this.filePathCallback != null) {
WebxdcActivity.this.filePathCallback.onReceiveValue(null);
}
WebxdcActivity.this.filePathCallback = filePathCallback;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
WebxdcActivity.this.startActivityForResult(Intent.createChooser(intent, getString(R.string.select)), REQUEST_CODE_FILE_PICKER);
return true;
}
});
DcEventCenter eventCenter = DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
DcEventCenter eventCenter =
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
int accountId = b.getInt(EXTRA_ACCOUNT_ID);
this.dcContext = DcHelper.getContext(getApplicationContext());
@@ -207,20 +214,23 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
return;
}
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same cache entry
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same
// cache entry
// (WebView may use a global cache shared across objects).
// (a random-id would also work, but would need maintenance and does not add benefits as we regard the file-part interceptRequest() only,
// (a random-id would also work, but would need maintenance and does not add benefits as we
// regard the file-part interceptRequest() only,
// also a random-id is not that useful for debugging)
this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost";
final JSONObject info = this.dcAppMsg.getWebxdcInfo();
internetAccess = JsonUtils.optBoolean(info, "internet_access");
if ("landscape".equals(JsonUtils.optString(info, "orientation"))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode,
// onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
}
selfAddr = info.optString("self_addr");
sendUpdateMaxSize = info.optInt("send_update_max_size");
@@ -238,7 +248,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
webSettings.setAllowUniversalAccessFromFileURLs(false);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webView.setNetworkAvailable(internetAccess); // this does not block network but sets `window.navigator.isOnline` in js land
webView.setNetworkAvailable(
internetAccess); // this does not block network but sets `window.navigator.isOnline` in js
// land
webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
String extraHref = b.getString(EXTRA_HREF, "");
@@ -256,18 +268,21 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
webView.loadUrl(this.baseURL + "/webxdc_bootstrap324567869.html?i=1&href=" + encodedHref);
Util.runOnAnyBackgroundThread(() -> {
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(() -> {
updateTitleAndMenu(info, chat);
});
});
Util.runOnAnyBackgroundThread(
() -> {
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(
() -> {
updateTitleAndMenu(info, chat);
});
});
}
@Override
public void onResume() {
super.onResume();
DcHelper.getNotificationCenter(this).updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId());
DcHelper.getNotificationCenter(this)
.updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId());
}
@Override
@@ -322,18 +337,24 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
private void initTTS() {
tts = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
Log.i(TAG, "TTS Init Status: " + status);
}
});
tts =
new TextToSpeech(
this,
new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
Log.i(TAG, "TTS Init Status: " + status);
}
});
}
private void setScreenMode(Configuration config) {
// enter/exit fullscreen mode depending on orientation (landscape/portrait),
// on tablets there is enough height so fullscreen mode is never enabled there
boolean enable = config.orientation == Configuration.ORIENTATION_LANDSCAPE && !getResources().getBoolean(R.bool.isBigScreen);
getWindow().getDecorView().setSystemUiVisibility(enable? View.SYSTEM_UI_FLAG_FULLSCREEN : 0);
boolean enable =
config.orientation == Configuration.ORIENTATION_LANDSCAPE
&& !getResources().getBoolean(R.bool.isBigScreen);
getWindow().getDecorView().setSystemUiVisibility(enable ? View.SYSTEM_UI_FLAG_FULLSCREEN : 0);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (hideActionBar || enable) {
@@ -345,11 +366,14 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
@Override
protected boolean shouldAskToOpenLink() { return true; }
protected boolean shouldAskToOpenLink() {
return true;
}
// This is usually only called when internetAccess == true or for mailto/openpgp4fpr scheme,
// because when internetAccess == false, the page is loaded inside an iframe,
// and WebViewClient.shouldOverrideUrlLoading is not called for HTTP(S) links inside the iframe unless target=_blank is used
// and WebViewClient.shouldOverrideUrlLoading is not called for HTTP(S) links inside the iframe
// unless target=_blank is used
@Override
protected boolean openOnlineUrl(String url) {
Log.i(TAG, "openOnlineUrl: " + url);
@@ -378,8 +402,10 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
} else if (path.equalsIgnoreCase("/webxdc_bootstrap324567869.html")) {
InputStream targetStream = getResources().openRawResource(R.raw.webxdc_wrapper);
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
} else if (path.equalsIgnoreCase("/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) {
InputStream targetStream = getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check);
} else if (path.equalsIgnoreCase(
"/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) {
InputStream targetStream =
getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check);
res = new WebResourceResponse("text/html", "UTF-8", targetStream);
} else {
byte[] blob = this.dcAppMsg.getWebxdcBlob(path);
@@ -393,33 +419,41 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
if (mimeType == null) {
switch (ext) {
case "js": mimeType = "text/javascript"; break;
case "wasm": mimeType = "application/wasm"; break;
default: mimeType = "application/octet-stream"; Log.i(TAG, "unknown mime type for " + rawUrl); break;
case "js":
mimeType = "text/javascript";
break;
case "wasm":
mimeType = "application/wasm";
break;
default:
mimeType = "application/octet-stream";
Log.i(TAG, "unknown mime type for " + rawUrl);
break;
}
}
String encoding = mimeType.startsWith("text/")? "UTF-8" : null;
String encoding = mimeType.startsWith("text/") ? "UTF-8" : null;
InputStream targetStream = new ByteArrayInputStream(blob);
res = new WebResourceResponse(mimeType, encoding, targetStream);
}
} catch (Exception e) {
e.printStackTrace();
InputStream targetStream = new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes());
InputStream targetStream =
new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes());
res = new WebResourceResponse("text/plain", "UTF-8", targetStream);
}
if (!internetAccess) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Security-Policy",
headers.put(
"Content-Security-Policy",
"default-src 'self'; "
+ "style-src 'self' 'unsafe-inline' blob: ; "
+ "font-src 'self' data: blob: ; "
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; "
+ "connect-src 'self' data: blob: ; "
+ "img-src 'self' data: blob: ; "
+ "media-src 'self' data: blob: ;"
+ "webrtc 'block' ; "
);
+ "style-src 'self' 'unsafe-inline' blob: ; "
+ "font-src 'self' data: blob: ; "
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; "
+ "connect-src 'self' data: blob: ; "
+ "img-src 'self' data: blob: ; "
+ "media-src 'self' data: blob: ;"
+ "webrtc 'block' ; ");
headers.put("X-DNS-Prefetch-Control", "off");
res.setResponseHeaders(headers);
}
@@ -427,49 +461,59 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
private void callJavaScriptFunction(String func) {
webView.evaluateJavascript("document.getElementById('frame').contentWindow." + func + ";", null);
webView.evaluateJavascript(
"document.getElementById('frame').contentWindow." + func + ";", null);
}
@Override
public void handleEvent(@NonNull DcEvent event) {
int eventId = event.getId();
if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE && event.getData1Int() == dcAppMsg.getId())) {
if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE
&& event.getData1Int() == dcAppMsg.getId())) {
Log.i(TAG, "handling status update event");
callJavaScriptFunction("__webxdcUpdate()");
} else if ((eventId == DcContext.DC_EVENT_WEBXDC_REALTIME_DATA && event.getData1Int() == dcAppMsg.getId())) {
} else if ((eventId == DcContext.DC_EVENT_WEBXDC_REALTIME_DATA
&& event.getData1Int() == dcAppMsg.getId())) {
Log.i(TAG, "handling realtime data event");
StringBuilder data = new StringBuilder();
for (byte b : event.getData2Blob()) {
data.append(((int) b) + ",");
data.append(((int) b) + ",");
}
callJavaScriptFunction("__webxdcRealtimeData([" + data + "])");
} else if ((eventId == DcContext.DC_EVENT_MSGS_CHANGED && event.getData2Int() == dcAppMsg.getId())) {
this.dcAppMsg = this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
Util.runOnAnyBackgroundThread(() -> {
final JSONObject info = dcAppMsg.getWebxdcInfo();
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(() -> {
updateTitleAndMenu(info, chat);
});
});
} else if ((eventId == DcContext.DC_EVENT_MSGS_CHANGED
&& event.getData2Int() == dcAppMsg.getId())) {
this.dcAppMsg =
this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
Util.runOnAnyBackgroundThread(
() -> {
final JSONObject info = dcAppMsg.getWebxdcInfo();
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(
() -> {
updateTitleAndMenu(info, chat);
});
});
}
}
private void updateTitleAndMenu(JSONObject info, DcChat chat) {
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
getSupportActionBar().setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName());
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
sourceCodeUrl = currSourceCodeUrl;
invalidateOptionsMenu();
}
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
getSupportActionBar()
.setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName());
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
sourceCodeUrl = currSourceCodeUrl;
invalidateOptionsMenu();
}
}
private void showInChat() {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcAppMsg, dcContext));
intent.putExtra(
ConversationActivity.STARTING_POSITION_EXTRA,
DcMsg.getMessagePosition(dcAppMsg, dcContext));
startActivity(intent);
}
@@ -487,17 +531,23 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
Bitmap bitmap = drawable.getBitmap();
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
.setShortLabel(docName.isEmpty() ? xdcName : docName)
.setIcon(IconCompat.createWithBitmap(bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too small circle and defamiliarize the icon too much
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
.build();
ShortcutInfoCompat shortcutInfoCompat =
new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
.setShortLabel(docName.isEmpty() ? xdcName : docName)
.setIcon(
IconCompat.createWithBitmap(
bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too
// small circle and defamiliarize the icon too much
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
.build();
Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show();
if (!ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
Toast.makeText(context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show();
Toast.makeText(
context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG)
.show();
}
} catch(Exception e) {
} catch (Exception e) {
Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show();
}
}
@@ -509,7 +559,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
if (resultCode == Activity.RESULT_OK && data != null) {
try {
if (data.getDataString() != null) {
dataUris = new Uri[]{Uri.parse(data.getDataString())};
dataUris = new Uri[] {Uri.parse(data.getDataString())};
} else if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
dataUris = new Uri[numSelectedFiles];
@@ -558,36 +608,49 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
return WebxdcActivity.this.selfAddr;
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public String selfName() {
return WebxdcActivity.this.dcContext.getName();
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public boolean sendStatusUpdate(String payload) {
Log.i(TAG, "sendStatusUpdate");
if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload)) {
DcChat dcChat = WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId());
Toast.makeText(WebxdcActivity.this,
dcChat.isContactRequest() ?
WebxdcActivity.this.getString(R.string.accept_request_first) :
WebxdcActivity.this.dcContext.getLastError(),
Toast.LENGTH_LONG).show();
if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(
WebxdcActivity.this.dcAppMsg.getId(), payload)) {
DcChat dcChat =
WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId());
Toast.makeText(
WebxdcActivity.this,
dcChat.isContactRequest()
? WebxdcActivity.this.getString(R.string.accept_request_first)
: WebxdcActivity.this.dcContext.getLastError(),
Toast.LENGTH_LONG)
.show();
return false;
}
return true;
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public String getStatusUpdates(int lastKnownSerial) {
Log.i(TAG, "getStatusUpdates");
return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates(WebxdcActivity.this.dcAppMsg.getId(), lastKnownSerial );
return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates(
WebxdcActivity.this.dcAppMsg.getId(), lastKnownSerial);
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public String sendToChat(String message) {
Log.i(TAG, "sendToChat");
@@ -601,24 +664,27 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
String name = null;
String type = null;
if (jsonObject.has("base64")) {
data = Base64.decode(jsonObject.getString("base64"), Base64.NO_WRAP | Base64.NO_PADDING);
name = jsonObject.getString("name");
data = Base64.decode(jsonObject.getString("base64"), Base64.NO_WRAP | Base64.NO_PADDING);
name = jsonObject.getString("name");
}
if (jsonObject.has("type")) {
type = jsonObject.getString("type");
type = jsonObject.getString("type");
}
if (jsonObject.has("text")) {
text = jsonObject.getString("text");
text = jsonObject.getString("text");
}
if (jsonObject.has("subject")) {
subject = jsonObject.getString("subject");
subject = jsonObject.getString("subject");
}
if (jsonObject.has("html")) {
html = jsonObject.getString("html");
html = jsonObject.getString("html");
}
if (TextUtils.isEmpty(text) && TextUtils.isEmpty(subject) && TextUtils.isEmpty(html) && TextUtils.isEmpty(name)) {
return "provided file is invalid, you need to set both name and base64 content";
if (TextUtils.isEmpty(text)
&& TextUtils.isEmpty(subject)
&& TextUtils.isEmpty(html)
&& TextUtils.isEmpty(name)) {
return "provided file is invalid, you need to set both name and base64 content";
}
DcHelper.sendToChat(WebxdcActivity.this, data, type, name, html, subject, text);
@@ -629,7 +695,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public void sendRealtimeAdvertisement() {
int accountId = WebxdcActivity.this.dcContext.getAccountId();
@@ -641,13 +709,17 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public void leaveRealtimeChannel() {
WebxdcActivity.this.leaveRealtimeChannel();
}
/** @noinspection unused*/
/**
* @noinspection unused
*/
@JavascriptInterface
public void sendRealtimeData(String jsonData) {
int accountId = WebxdcActivity.this.dcContext.getAccountId();
@@ -665,6 +737,5 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
if (lang != null && !lang.isEmpty()) tts.setLanguage(Locale.forLanguageTag(lang));
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
}
}
}

Some files were not shown because too many files have changed in this diff Show More