Catch ActivityNotFoundException when launching the backup location
picker. Devices without an OPEN_DOCUMENT_TREE handler now get the
existing no_application_found toast instead of crashing from the
preference tap.
Amolith
created
9ddb808
fix `isBlockedMediaSha1` crash on malformed SHA1
Phillip Davis
created
794554f
Fix locale-dependent SQL in truncatedAttributesColumn()
Click to expand commit body
String.format with %d uses the default locale's numeral system.
On Persian (fa) locale this produces ۶۵۵۳۴ instead of 65534,
causing an SQLite error.
Extract the expression into truncatedAttributesColumn() so it
can be tested under a non-default locale, and replace
String.format with concatenation which always uses ASCII digits.
Phillip Davis
created
7ee1d24
UnifiedPush: add support for link activity
a4f7fb2
fix(entities): cache MUC occupants in own DB rows
Phillip Davis
created
a9d648b
Require a SCRAM iteration count of 4096 or higher
Click to expand commit body
The RFC specifies a minimum iteration count of 4096 only as a "SHOULD".
Therefore, requiring 4096 on the client side is technically not fully in
line with the spec. However, in the existing ecosystem, the servers that
we tested all use 4096 or more.
Previously we used a custom protocol extension to optionally use pending
intents instead of an 'application' extra; nowadays the UnifiedPush spec
supports pending intents officially (and we can stop accepting the
insecure 'application' extra)
beyond that we could theoretically get stack overflow exceptions due to
the recursion in our parser
Daniel Gultsch
created
53e8ec1
Always indicate support for channel binding in SASL header
Click to expand commit body
This commit breaks logging in on servers that announce a -PLUS variant for
SCRAM but do not support XEP-0440.
On servers that do not support XEP-0440, we previously decided to use no
channel binding because picking "none" was better than picking an
unsupported one and failing the login.
However, this behaviour also required us to indicate that we did not
support channel binding; otherwise, the server, seeing an unknown binding
mechanism, would fail our login.
This was a decision made for the broadest possible compatibility with the
pre-0440 ecosystem.
Note that the y flag wasn't the only security layer. Conversations also
uses pinning (if you logged in once with -PLUS, it won't fall back) and
XEP-0474 (basically a fancy version of the y flag).
In addition there is a setting in Conversations to always require Channel
Binding. This will also automatically be turned on for conversations.im
and quicksy.im.
It has now been two years since XEP-0440 was released for ejabberd and
Prosody, and our compatibility concerns have shifted: if you want to have
-PLUS on your server, please update the server to support XEP-0440.
Daniel Gultsch
created
dc5b902
fix: NPE from unsynchronized MucOptions access
Click to expand commit body
- ConversationGetMucOptionsRaceTest is in its own file to make the
instrumentation barriers a little easier to see and enforce
- It simulates basically the following situation:
i. On app startup, all accounts connect to all MUCs, which
eventually triggers joinMuc, which calls
Conversation.resetMucOptions.
This happens on account's individual XMPP connection Thread.
ii. At the same time, on the UI thread, we bind
ConversationsOverviewFragment, which eventually leads to
ConversationAdapter.onBindViewHolder, which calls getMucOptions.
iii. Conversation.resetMucOptions is not synchronized, so it can
run after the null-check in getMucOptions, resulting in a null
return value.
- Used AtomicReference instead of synchronizing resetMucOptions
because joinMuc is run on a directExecutor associated with the
XmppConnection thread. While the synchronized getMucOptions was
called right after this in the existing code, the documentation for
directExecutor has this to say:
When a ListenableFuture listener is registered to run under
directExecutor, the listener can execute in any of three possible
threads:
- When a thread attaches a listener to a ListenableFuture that's
already complete, the listener runs immediately in that thread.
- When a thread attaches a listener to a ListenableFuture that's
incomplete and the ListenableFuture later completes normally,
the listener runs in the thread that completes the ListenableFuture.
- When a listener is attached to a ListenableFuture and the
ListenableFuture gets cancelled, the listener runs immediately
in the thread that cancelled the Future.
- (This is applicable, since directExecutor uses in
XmppConnectionService are extensively attached to ListenableFuture)
The docs continue:
A specific warning about locking: Code that executes user-supplied
tasks, such as ListenableFuture listeners, should take care not to
do so while holding a lock. Additionally, as a further line of
defense, prefer not to perform any locking inside a task that will
be run under directExecutor: Not only might the wait for a lock
be long, but if the running thread was holding a lock, the
listener may deadlock or break lock isolation.
- Altogether, AtomicReference seems much more aligned with how Google
intends directExecutor to be used.
Anyway, here's the stacktrace that reported this error:
```
2026-02-20 10:41:09.089 7610-7610 AndroidRuntime com.cheogram.android E FATAL EXCEPTION: main (Ask Gemini)
Process: com.cheogram.android, PID: 7610
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String eu.siacs.conversations.entities.MucOptions.getName()' on a null object reference
at eu.siacs.conversations.entities.Conversation.getName(Conversation.java:1068)
at eu.siacs.conversations.entities.Conversation.getAvatarName(Conversation.java:1784)
at eu.siacs.conversations.ui.util.AvatarWorkerTask.setContentDescription(AvatarWorkerTask.java:135)
at eu.siacs.conversations.ui.util.AvatarWorkerTask.loadAvatar(AvatarWorkerTask.java:105)
at eu.siacs.conversations.ui.adapter.ConversationAdapter.onBindViewHolder(ConversationAdapter.java:255)
at eu.siacs.conversations.ui.adapter.ConversationAdapter.onBindViewHolder(ConversationAdapter.java:33)
at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7846)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7953)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6742)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:7013)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6853)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6849)
at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2422)
at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1722)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1682)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:747)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4737)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:4459)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:5011)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)
at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)
at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:919)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at androidx.drawerlayout.widget.DrawerLayout.onLayout(DrawerLayout.java:1273)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
```
ConcurrentModificationException can only be triggered by (a) obviously,
multiple threads modifying the same collection or (b) one thread which
modifies the thread while iterating over it (see [here](https://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html)). Assuming that `systemTags` doesn't alias `this.systemTags`, it must be (a). Also, the stacktrace we got comes from inside `old.equals`. consider:
```java
final JSONArray old = this.systemTags;
this.systemTags = new JSONArray();
final JSONArray old = this.systemTags;
...
this.systemTags.put(...)
!old.equal(...)
```
and we get ConcurrentModificationException bc Thread 2's `old` alias's
thread 1's `this.systemTags`
The patch fixes this bug by making suring that no references escape
function scope, so no aliasing can occur.
d8152c4
Never fall back to iterative DNS for DNSSEC
Click to expand commit body
This can work around if your local resolver strips DNSSEC, but also it
means resolution is bonkers slow and might even take forever / fail if
DNS queries are blocked (because you're on TOR VPN or similar). So if
recursive DNSSEC fails, just fail DNSSEC and fall back to regular DNS lookups.
- now `EmojiSearch.emoji` is truly immutable, in the sense that its
members are not mutated after being inserted.
- attempt to fix: https://todo.sr.ht/~singpolyma/soprani.ca/473
Phillip Davis
created
b05fdb5
if setAvatar to null, dont check blocked media
Click to expand commit body
codepath is deleteAvatar, which is setAvatar(from, null)
a8324a0
try-catch `new ToneGenerator` and log errors
Click to expand commit body
trouble reproducing reports of failure stemming from
`WebRTCWrapper.applyDtmfTone` failing on this constructor, specifically:
```
at android.media.ToneGenerator.native_setup(Native Method)
at android.media.ToneGenerator.<init>(ToneGenerator.java:751)
```
so we log the error for next time and catch it to prevent crashing, and
instead just dont play the tone
- in CommandSession constructor, must construct a hard reference to the
ViewPager to make garbage collection impossible until at least
`getContext()` is called
- in setupLayoutManager, current code doesn't have a case for if mPager
is null. Probably we wouldn't be there anyway, but in any case the
`ctx` parameter should be equivalent, i.e., it should refer to the
ActivityContext, so we use that instead of going through mPager