cheogram-android

created pr with cheogram-android/onboarding-cancel.1 on 2026-11-03T17:40:04Z · by 9e834e0d
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-android:onboarding-cancel | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-android:onboarding-cancel [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-android:onboarding-cancel
add comment to patch request:
ssh pr.pico.sh comment cheogram-android:onboarding-cancel "lgtm!"

Patchset cheogram-android/onboarding-cancel.1 on 2026-11-03T17:40:04Z · commit 318b50a

Refactor and improve onboarding cancel block
Stephen Paul Weber 2026-11-03T17:37:59Z
Don't wait for server to reply to remember what the user chose. Once
they've chosen something, we don't cancel out to non-cheogram
onboarding. Disable back button during onboarding from the
MagicCreateActivity so that they can't back out after they've already
basically finished.
Semantic diff summary
0 added, 0 modified, 0 signature changed, 0 removed across 0 analyzed files (7 files skipped: unsupported file type)
+17 -0 src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java #
......@@ -38,7 +38,24 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
3838
3939 @Override
4040 protected void onBackendConnected() {
41+ updateBackNavigation();
42+ }
43+
44+ private void updateBackNavigation() {
45+ if (xmppConnectionService == null) {
46+ return;
47+ }
48+ configureActionBar(
49+ getSupportActionBar(),
50+ this.domain == null && !xmppConnectionService.isOnboarding());
51+ }
4152
53+ @Override
54+ public void onBackPressed() {
55+ if (xmppConnectionService != null && xmppConnectionService.isOnboarding()) {
56+ return;
57+ }
58+ super.onBackPressed();
4259 }
4360
4461 @Override
+41 -23 src/main/java/eu/siacs/conversations/entities/Conversation.java #
......@@ -181,6 +181,7 @@ import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
181181 import eu.siacs.conversations.utils.Emoticons;
182182 import eu.siacs.conversations.utils.JidHelper;
183183 import eu.siacs.conversations.utils.MessageUtils;
184+import eu.siacs.conversations.utils.OnboardingUtils;
184185 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
185186 import eu.siacs.conversations.utils.UIHelper;
186187 import eu.siacs.conversations.xml.Element;
......@@ -2150,6 +2151,12 @@ public class Conversation extends AbstractEntity
21502151 show();
21512152 CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
21522153
2154+ if (xmppConnectionService.isOnboarding()
2155+ && "jabber:iq:register".equals(command.getAttribute("node"))
2156+ && OnboardingUtils.getSelectedGateway(xmppConnectionService.getPreferences()) != null) {
2157+ OnboardingUtils.markResume(xmppConnectionService.getPreferences());
2158+ }
2159+
21532160 final var packet = new Iq(Iq.Type.SET);
21542161 packet.setTo(command.getAttributeAsJid("jid"));
21552162 final Element c = packet.addChild("command", Namespace.COMMANDS);
......@@ -3430,6 +3437,7 @@ public class Conversation extends AbstractEntity
34303437 protected Iq response = null;
34313438 protected Element responseElement = null;
34323439 protected boolean expectingRemoval = false;
3440+ private boolean resumed = false;
34333441 protected List<Field> reported = null;
34343442 protected SparseArray<Item> items = new SparseArray<>();
34353443 protected XmppConnectionService xmppConnectionService;
......@@ -3487,15 +3495,13 @@ public class Conversation extends AbstractEntity
34873495 xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()));
34883496 }
34893497
3490- if (xmppConnectionService.isOnboarding() && mNode.equals("jabber:iq:register") && !"canceled".equals(command.getAttribute("status")) && xmppConnectionService.getPreferences().contains("onboarding_action")) {
3491- xmppConnectionService.getPreferences().edit().putBoolean("onboarding_continued", true).commit();
3492- }
3493-
3498+ final boolean suppressCancel = isCancelSuppressed();
34943499 Element actions = command.findChild("actions", "http://jabber.org/protocol/commands");
34953500 if (actions != null) {
34963501 for (Element action : actions.getChildren()) {
34973502 if (!"http://jabber.org/protocol/commands".equals(action.getNamespace())) continue;
34983503 if ("execute".equals(action.getName())) continue;
3504+ if (suppressCancel && "cancel".equals(action.getName())) continue;
34993505
35003506 actionsAdapter.add(Pair.create(action.getName(), action.getName()));
35013507 }
......@@ -3521,6 +3527,7 @@ public class Conversation extends AbstractEntity
35213527 actionsAdapter.clear();
35223528
35233529 for (Option action : actionList.getOptions()) {
3530+ if (suppressCancel && "cancel".equals(action.getValue())) continue;
35243531 actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
35253532 }
35263533 }
......@@ -3571,17 +3578,17 @@ public class Conversation extends AbstractEntity
35713578 if (responseElement == null && command.getAttribute("status") != null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
35723579 if ("jabber:iq:register".equals(mNode) && "canceled".equals(command.getAttribute("status"))) {
35733580 if (xmppConnectionService.isOnboarding()) {
3574- if (xmppConnectionService.getPreferences().contains("onboarding_action")) {
3575- xmppConnectionService.deleteAccount(getAccount());
3576- } else {
3577- if (xmppConnectionService.getPreferences().getBoolean("onboarding_continued", false)) {
3578- removeSession(this);
3579- return;
3580- } else {
3581- xmppConnectionService.getPreferences().edit().putString("onboarding_action", "cancel").commit();
3582- xmppConnectionService.deleteAccount(getAccount());
3583- }
3581+ final boolean continued = OnboardingUtils.isContinued(xmppConnectionService.getPreferences());
3582+ if (continued) {
3583+ // They already committed to this onboarding flow. Do not destroy
3584+ // their account just because a command session went away.
3585+ removeSession(this);
3586+ return;
3587+ }
3588+ if (!OnboardingUtils.isCanceled(xmppConnectionService.getPreferences())) {
3589+ OnboardingUtils.markCanceled(xmppConnectionService.getPreferences());
35843590 }
3591+ xmppConnectionService.deleteAccount(getAccount());
35853592 }
35863593 xmppConnectionService.archiveConversation(Conversation.this);
35873594 }
......@@ -3620,15 +3627,19 @@ public class Conversation extends AbstractEntity
36203627
36213628 Data dataForm = null;
36223629 if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
3623- if (mNode.equals("jabber:iq:register") &&
3624- xmppConnectionService.getPreferences().contains("onboarding_action") &&
3630+ if (xmppConnectionService.isOnboarding() &&
3631+ mNode.equals("jabber:iq:register") &&
36253632 dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
36263633
3627-
3628- dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
3629- execute();
3634+ final String gateway = OnboardingUtils.getSelectedGateway(xmppConnectionService.getPreferences());
3635+ if (gateway != null) {
3636+ dataForm.put("gateway-jid", gateway);
3637+ if (!resumed && OnboardingUtils.isResumePending(xmppConnectionService.getPreferences())) {
3638+ resumed = true;
3639+ execute();
3640+ }
3641+ }
36303642 }
3631- xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
36323643 notifyDataSetChanged();
36333644 }
36343645
......@@ -3846,6 +3857,12 @@ public class Conversation extends AbstractEntity
38463857 return execute(actionsAdapter.getItem(actionPosition).first);
38473858 }
38483859
3860+ private boolean isCancelSuppressed() {
3861+ return mNode.equals("jabber:iq:register")
3862+ && xmppConnectionService.isOnboarding()
3863+ && OnboardingUtils.isContinued(xmppConnectionService.getPreferences());
3864+ }
3865+
38493866 public synchronized boolean execute(String action) {
38503867 if (!"cancel".equals(action) && executing) {
38513868 loadingHasBeenLong = true;
......@@ -3887,10 +3904,11 @@ public class Conversation extends AbstractEntity
38873904 }
38883905
38893906 if (mNode.equals("jabber:iq:register") && xmppConnectionService.isOnboarding() && form.getFieldByName("gateway-jid") != null) {
3890- if (form.getValue("gateway-jid") == null) {
3891- xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
3907+ final String gateway = form.getValue("gateway-jid");
3908+ if (gateway == null || gateway.isEmpty()) {
3909+ OnboardingUtils.clearSelectedGateway(xmppConnectionService.getPreferences());
38923910 } else {
3893- xmppConnectionService.getPreferences().edit().putString("onboarding_action", form.getValue("gateway-jid")).commit();
3911+ OnboardingUtils.setSelectedGateway(xmppConnectionService.getPreferences(), gateway);
38943912 }
38953913 }
38963914
+2 -1 src/main/java/eu/siacs/conversations/services/XmppConnectionService.java #
......@@ -164,6 +164,7 @@ import eu.siacs.conversations.utils.FileUtils;
164164 import eu.siacs.conversations.utils.MessageUtils;
165165 import eu.siacs.conversations.utils.Emoticons;
166166 import eu.siacs.conversations.utils.MimeUtils;
167+import eu.siacs.conversations.utils.OnboardingUtils;
167168 import eu.siacs.conversations.utils.PhoneHelper;
168169 import eu.siacs.conversations.utils.QuickLoader;
169170 import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
......@@ -3122,7 +3123,7 @@ public class XmppConnectionService extends Service {
31223123 }
31233124
31243125 public void deleteAccount(final Account account) {
3125- getPreferences().edit().remove("onboarding_continued").commit();
3126+ OnboardingUtils.clearProgress(getPreferences());
31263127 final boolean connected = account.getStatus() == Account.State.ONLINE;
31273128 synchronized (this.conversations) {
31283129 if (connected) {
+3 -2 src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java #
......@@ -88,6 +88,7 @@ import eu.siacs.conversations.ui.util.PendingItem;
8888 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
8989 import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
9090 import eu.siacs.conversations.utils.AccountUtils;
91+import eu.siacs.conversations.utils.OnboardingUtils;
9192 import eu.siacs.conversations.utils.UIHelper;
9293 import eu.siacs.conversations.utils.XmppUri;
9394 import eu.siacs.conversations.utils.XEP0392Helper;
......@@ -1147,8 +1148,8 @@ public class StartConversationActivity extends XmppActivity
11471148 break;
11481149 }
11491150
1150- final boolean onboardingCancel = xmppConnectionService.getPreferences().getString("onboarding_action", "").equals("cancel");
1151- if (onboardingCancel) xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
1151+ final boolean onboardingCancel = OnboardingUtils.isCanceled(xmppConnectionService.getPreferences());
1152+ if (onboardingCancel) OnboardingUtils.clearCanceled(xmppConnectionService.getPreferences());
11521153
11531154 if ((xmppConnectionService.isOnboarding() || (intent != null && intent.getBooleanExtra("init", false))) && !onboardingCancel && !xmppConnectionService.getAccounts().isEmpty()) {
11541155 Account selectedAccount = xmppConnectionService.getAccounts().get(0);
+94 -0 src/main/java/eu/siacs/conversations/utils/OnboardingUtils.java #
......@@ -0,0 +1,94 @@
1+package eu.siacs.conversations.utils;
2+
3+import android.content.SharedPreferences;
4+
5+public final class OnboardingUtils {
6+
7+ private static final String PREF_CANCELED = "onboarding_action";
8+ private static final String PREF_CONTINUED = "onboarding_continued";
9+ private static final String PREF_RESUME = "onboarding_resume";
10+ private static final String CANCELED = "cancel";
11+
12+ public static boolean isCanceled(final SharedPreferences preferences) {
13+ return CANCELED.equals(preferences.getString(PREF_CANCELED, null));
14+ }
15+
16+ public static void markCanceled(final SharedPreferences preferences) {
17+ preferences.edit().putString(PREF_CANCELED, CANCELED).commit();
18+ }
19+
20+ public static void clearCanceled(final SharedPreferences preferences) {
21+ preferences.edit().remove(PREF_CANCELED).commit();
22+ }
23+
24+ public static String getSelectedGateway(final SharedPreferences preferences) {
25+ // onboarding_continued used to be a boolean. Read it as an Object so
26+ // installations that still have the old value do not crash.
27+ final Object value = preferences.getAll().get(PREF_CONTINUED);
28+ if (value instanceof String) {
29+ final String gateway = (String) value;
30+ return gateway.isEmpty() ? null : gateway;
31+ }
32+ // Older versions temporarily stored the selected gateway here while a
33+ // request was in flight. Keep treating that as a committed selection.
34+ final Object legacy = preferences.getAll().get(PREF_CANCELED);
35+ if (legacy instanceof String && !CANCELED.equals(legacy)) {
36+ final String gateway = (String) legacy;
37+ return gateway.isEmpty() ? null : gateway;
38+ }
39+ return null;
40+ }
41+
42+ public static boolean isContinued(final SharedPreferences preferences) {
43+ final Object value = preferences.getAll().get(PREF_CONTINUED);
44+ if (value instanceof Boolean && (Boolean) value) {
45+ return true;
46+ }
47+ return getSelectedGateway(preferences) != null;
48+ }
49+
50+ public static void setSelectedGateway(
51+ final SharedPreferences preferences, final String gateway) {
52+ if (gateway == null || gateway.isEmpty()) {
53+ return;
54+ }
55+ preferences
56+ .edit()
57+ .putString(PREF_CONTINUED, gateway)
58+ .remove(PREF_CANCELED)
59+ .remove(PREF_RESUME)
60+ .commit();
61+ }
62+
63+ public static void clearSelectedGateway(final SharedPreferences preferences) {
64+ final SharedPreferences.Editor editor =
65+ preferences.edit().remove(PREF_CONTINUED).remove(PREF_RESUME);
66+ // Older versions temporarily stored the selected gateway in the cancel
67+ // preference. Drop that too, but leave the real cancel marker alone.
68+ final Object legacy = preferences.getAll().get(PREF_CANCELED);
69+ if (legacy instanceof String && !CANCELED.equals(legacy)) {
70+ editor.remove(PREF_CANCELED);
71+ }
72+ editor.commit();
73+ }
74+
75+ public static void markResume(final SharedPreferences preferences) {
76+ preferences.edit().putBoolean(PREF_RESUME, true).commit();
77+ }
78+
79+ public static boolean isResumePending(final SharedPreferences preferences) {
80+ return preferences.getBoolean(PREF_RESUME, false);
81+ }
82+
83+ public static void clearProgress(final SharedPreferences preferences) {
84+ final SharedPreferences.Editor editor =
85+ preferences.edit().remove(PREF_CONTINUED).remove(PREF_RESUME);
86+ // Preserve the one-shot cancel marker so the next start-up does not
87+ // immediately offer cheogram onboarding again. Drop stale legacy values
88+ // that used to store the selected gateway here.
89+ if (!isCanceled(preferences)) {
90+ editor.remove(PREF_CANCELED);
91+ }
92+ editor.commit();
93+ }
94+}
+1 -1 src/test/java/eu/siacs/conversations/entities/MessageTest.java #
......@@ -24,7 +24,7 @@ public class MessageTest {
2424 public void extractLinksIgnoresInvalidUris() {
2525 final var conversation = mock(Conversational.class);
2626 when(conversation.getUuid()).thenReturn("test-uuid");
27- when(conversation.getJid()).thenReturn(Jid.ofLocalAndDomain("test", "example.com"));
27+ when(conversation.getAddress()).thenReturn(Jid.ofLocalAndDomain("test", "example.com"));
2828
2929 final var message = new Message(conversation, "https://example.com?q=%s", Message.ENCRYPTION_NONE);
3030 Assert.assertEquals(0, message.getLinks().size());
+127 -0 src/test/java/eu/siacs/conversations/utils/OnboardingUtilsTest.java #
......@@ -0,0 +1,127 @@
1+package eu.siacs.conversations.utils;
2+
3+import static org.junit.Assert.assertEquals;
4+import static org.junit.Assert.assertFalse;
5+import static org.junit.Assert.assertNull;
6+import static org.junit.Assert.assertTrue;
7+
8+import android.content.Context;
9+import android.content.SharedPreferences;
10+import android.os.Build;
11+
12+import org.junit.Before;
13+import org.junit.Test;
14+import org.junit.runner.RunWith;
15+import org.robolectric.RobolectricTestRunner;
16+import org.robolectric.RuntimeEnvironment;
17+import org.robolectric.annotation.Config;
18+import org.robolectric.annotation.ConscryptMode;
19+
20+import eu.siacs.conversations.Conversations;
21+
22+@RunWith(RobolectricTestRunner.class)
23+@Config(sdk = Build.VERSION_CODES.TIRAMISU, application = Conversations.class)
24+@ConscryptMode(ConscryptMode.Mode.OFF)
25+public class OnboardingUtilsTest {
26+
27+ private static final String PREF_CANCELED = "onboarding_action";
28+ private static final String PREF_CONTINUED = "onboarding_continued";
29+ private static final String PREF_RESUME = "onboarding_resume";
30+
31+ private SharedPreferences preferences;
32+
33+ @Before
34+ public void setUp() {
35+ preferences =
36+ RuntimeEnvironment.getApplication()
37+ .getSharedPreferences("onboarding-test", Context.MODE_PRIVATE);
38+ preferences.edit().clear().commit();
39+ }
40+
41+ @Test
42+ public void selectedGatewayMarksContinuedAndClearsCancel() {
43+ OnboardingUtils.markCanceled(preferences);
44+ OnboardingUtils.markResume(preferences);
45+ assertTrue(OnboardingUtils.isCanceled(preferences));
46+
47+ OnboardingUtils.setSelectedGateway(preferences, "snikket.example");
48+
49+ assertFalse(OnboardingUtils.isCanceled(preferences));
50+ assertFalse(OnboardingUtils.isResumePending(preferences));
51+ assertTrue(OnboardingUtils.isContinued(preferences));
52+ assertEquals("snikket.example", OnboardingUtils.getSelectedGateway(preferences));
53+ }
54+
55+ @Test
56+ public void legacyBooleanIsTreatedAsContinuedWithoutGateway() {
57+ preferences.edit().putBoolean(PREF_CONTINUED, true).commit();
58+
59+ assertTrue(OnboardingUtils.isContinued(preferences));
60+ assertNull(OnboardingUtils.getSelectedGateway(preferences));
61+ }
62+
63+ @Test
64+ public void legacyGatewayInCancelPreferenceIsTreatedAsContinued() {
65+ preferences.edit().putString(PREF_CANCELED, "snikket.example").commit();
66+
67+ assertTrue(OnboardingUtils.isContinued(preferences));
68+ assertEquals("snikket.example", OnboardingUtils.getSelectedGateway(preferences));
69+ }
70+
71+ @Test
72+ public void clearingSelectionRemovesGatewayResumeAndLegacyValue() {
73+ OnboardingUtils.setSelectedGateway(preferences, "snikket.example");
74+ OnboardingUtils.markResume(preferences);
75+ preferences.edit().putString(PREF_CANCELED, "legacy.example").commit();
76+
77+ OnboardingUtils.clearSelectedGateway(preferences);
78+
79+ assertFalse(OnboardingUtils.isContinued(preferences));
80+ assertNull(OnboardingUtils.getSelectedGateway(preferences));
81+ assertFalse(OnboardingUtils.isResumePending(preferences));
82+ assertNull(preferences.getString(PREF_CANCELED, null));
83+ }
84+
85+ @Test
86+ public void clearingSelectionKeepsCancelMarker() {
87+ OnboardingUtils.markCanceled(preferences);
88+
89+ OnboardingUtils.clearSelectedGateway(preferences);
90+
91+ assertTrue(OnboardingUtils.isCanceled(preferences));
92+ }
93+
94+ @Test
95+ public void resumeStaysPendingUntilProgressIsCleared() {
96+ OnboardingUtils.markResume(preferences);
97+
98+ assertTrue(OnboardingUtils.isResumePending(preferences));
99+ assertTrue(OnboardingUtils.isResumePending(preferences));
100+
101+ OnboardingUtils.clearProgress(preferences);
102+
103+ assertFalse(OnboardingUtils.isResumePending(preferences));
104+ }
105+
106+ @Test
107+ public void clearProgressPreservesCancelButClearsGateway() {
108+ OnboardingUtils.setSelectedGateway(preferences, "snikket.example");
109+ OnboardingUtils.markResume(preferences);
110+ OnboardingUtils.markCanceled(preferences);
111+
112+ OnboardingUtils.clearProgress(preferences);
113+
114+ assertNull(OnboardingUtils.getSelectedGateway(preferences));
115+ assertFalse(OnboardingUtils.isResumePending(preferences));
116+ assertTrue(OnboardingUtils.isCanceled(preferences));
117+ }
118+
119+ @Test
120+ public void clearProgressRemovesStaleLegacyGatewayFromCancelPreference() {
121+ preferences.edit().putString(PREF_CANCELED, "snikket.example").commit();
122+
123+ OnboardingUtils.clearProgress(preferences);
124+
125+ assertNull(preferences.getString(PREF_CANCELED, null));
126+ }
127+}
Back to top