1/*
2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation and/or
12 * other materials provided with the distribution.
13 *
14 * 3. Neither the name of the copyright holder nor the names of its contributors
15 * may be used to endorse or promote products derived from this software without
16 * specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30package eu.siacs.conversations.ui;
31
32import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
33
34import android.Manifest;
35import android.annotation.SuppressLint;
36import android.app.Activity;
37import android.app.Fragment;
38import android.app.FragmentManager;
39import android.app.FragmentTransaction;
40import android.content.ActivityNotFoundException;
41import android.content.ComponentName;
42import android.content.Context;
43import android.content.Intent;
44import android.content.pm.PackageManager;
45import android.graphics.Bitmap;
46import android.net.Uri;
47import android.os.Build;
48import android.os.Bundle;
49import android.provider.Settings;
50import android.util.Log;
51import android.util.Pair;
52import android.view.KeyEvent;
53import android.view.Menu;
54import android.view.MenuItem;
55import android.widget.Toast;
56import androidx.annotation.IdRes;
57import androidx.annotation.NonNull;
58import androidx.appcompat.app.ActionBar;
59import androidx.appcompat.app.AlertDialog;
60import androidx.core.app.ActivityCompat;
61import androidx.core.content.ContextCompat;
62import androidx.databinding.DataBindingUtil;
63
64import com.cheogram.android.DownloadDefaultStickers;
65import com.cheogram.android.FinishOnboarding;
66
67import com.google.common.collect.ImmutableList;
68
69import io.michaelrocks.libphonenumber.android.NumberParseException;
70import com.google.android.material.dialog.MaterialAlertDialogBuilder;
71import com.google.android.material.color.MaterialColors;
72import com.google.android.material.elevation.SurfaceColors;
73
74import org.jetbrains.annotations.NotNull;
75import org.openintents.openpgp.util.OpenPgpApi;
76
77import java.util.Arrays;
78import java.util.ArrayList;
79import java.util.HashSet;
80import java.util.HashMap;
81import java.util.List;
82import java.util.Objects;
83import java.util.Set;
84import java.util.TreeMap;
85import java.util.concurrent.RejectedExecutionException;
86import java.util.concurrent.atomic.AtomicBoolean;
87import java.util.stream.Collectors;
88import java.util.stream.Stream;
89
90import eu.siacs.conversations.Config;
91import eu.siacs.conversations.R;
92import eu.siacs.conversations.crypto.OmemoSetting;
93import eu.siacs.conversations.databinding.ActivityConversationsBinding;
94import eu.siacs.conversations.entities.Account;
95import eu.siacs.conversations.entities.Contact;
96import eu.siacs.conversations.entities.Conversation;
97import eu.siacs.conversations.entities.Conversational;
98import eu.siacs.conversations.entities.ListItem.Tag;
99import eu.siacs.conversations.persistance.FileBackend;
100import eu.siacs.conversations.services.XmppConnectionService;
101import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
102import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
103import eu.siacs.conversations.ui.interfaces.OnConversationRead;
104import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
105import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
106import eu.siacs.conversations.ui.util.ActivityResult;
107import eu.siacs.conversations.ui.util.AvatarWorkerTask;
108import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
109import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
110import eu.siacs.conversations.ui.util.PendingItem;
111import eu.siacs.conversations.ui.util.ToolbarUtils;
112import eu.siacs.conversations.ui.util.SendButtonTool;
113import eu.siacs.conversations.utils.AccountUtils;
114import eu.siacs.conversations.utils.ExceptionHelper;
115import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
116import eu.siacs.conversations.utils.SignupUtils;
117import eu.siacs.conversations.utils.ThemeHelper;
118import eu.siacs.conversations.utils.XmppUri;
119import eu.siacs.conversations.xmpp.Jid;
120import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
121import java.util.Arrays;
122import java.util.List;
123import java.util.concurrent.atomic.AtomicBoolean;
124import org.openintents.openpgp.util.OpenPgpApi;
125
126public class ConversationsActivity extends XmppActivity
127 implements OnConversationSelected,
128 OnConversationArchived,
129 OnConversationsListItemUpdated,
130 OnConversationRead,
131 XmppConnectionService.OnAccountUpdate,
132 XmppConnectionService.OnConversationUpdate,
133 XmppConnectionService.OnRosterUpdate,
134 OnUpdateBlocklist,
135 XmppConnectionService.OnShowErrorToast,
136 XmppConnectionService.OnAffiliationChanged {
137
138 public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
139 public static final String EXTRA_CONVERSATION = "conversationUuid";
140 public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
141 public static final String EXTRA_AS_QUOTE = "eu.siacs.conversations.as_quote";
142 public static final String EXTRA_NICK = "nick";
143 public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
144 public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
145 public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
146 public static final String POST_ACTION_RECORD_VOICE = "record_voice";
147 public static final String EXTRA_THREAD = "threadId";
148 public static final String EXTRA_TYPE = "type";
149 public static final String EXTRA_NODE = "node";
150 public static final String EXTRA_JID = "jid";
151 public static final String EXTRA_MESSAGE_UUID = "messageUuid";
152
153 private static final List<String> VIEW_AND_SHARE_ACTIONS =
154 Arrays.asList(
155 ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE);
156
157 public static final int REQUEST_OPEN_MESSAGE = 0x9876;
158 public static final int REQUEST_PLAY_PAUSE = 0x5432;
159 public static final int REQUEST_MICROPHONE = 0x5432f;
160 public static final int DIALLER_INTEGRATION = 0x5432ff;
161 public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
162
163 public static final long DRAWER_ALL_CHATS = 1;
164 public static final long DRAWER_UNREAD_CHATS = 2;
165 public static final long DRAWER_DIRECT_MESSAGES = 3;
166 public static final long DRAWER_MANAGE_ACCOUNT = 4;
167 public static final long DRAWER_ADD_ACCOUNT = 5;
168 public static final long DRAWER_MANAGE_PHONE_ACCOUNTS = 6;
169 public static final long DRAWER_CHANNELS = 7;
170 public static final long DRAWER_CHAT_REQUESTS = 8;
171 public static final long DRAWER_SETTINGS = 9;
172 public static final long DRAWER_START_CHAT = 10;
173 public static final long DRAWER_START_CHAT_CONTACT = 11;
174 public static final long DRAWER_START_CHAT_NEW = 12;
175 public static final long DRAWER_START_CHAT_GROUP = 13;
176 public static final long DRAWER_START_CHAT_PUBLIC = 14;
177 public static final long DRAWER_START_CHAT_DISCOVER = 15;
178
179 // secondary fragment (when holding the conversation, must be initialized before refreshing the
180 // overview fragment
181 private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {
182 R.id.secondary_fragment, R.id.main_fragment
183 };
184 private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
185 private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
186 private ActivityConversationsBinding binding;
187 private boolean mActivityPaused = true;
188 private final AtomicBoolean mRedirectInProcess = new AtomicBoolean(false);
189 private boolean refreshForNewCaps = false;
190 private Set<Jid> newCapsJids = new HashSet<>();
191 private int mRequestCode = -1;
192 private com.mikepenz.materialdrawer.widget.AccountHeaderView accountHeader;
193 private Bundle savedState = null;
194 private HashSet<Tag> selectedTag = new HashSet<>();
195 private long mainFilter = DRAWER_ALL_CHATS;
196 private boolean refreshAccounts = true;
197 private boolean invisibles = false;
198
199 private static boolean isViewOrShareIntent(Intent i) {
200 Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
201 return i != null
202 && VIEW_AND_SHARE_ACTIONS.contains(i.getAction())
203 && i.hasExtra(EXTRA_CONVERSATION);
204 }
205
206 private static Intent createLauncherIntent(Context context) {
207 final Intent intent = new Intent(context, ConversationsActivity.class);
208 intent.setAction(Intent.ACTION_MAIN);
209 intent.addCategory(Intent.CATEGORY_LAUNCHER);
210 return intent;
211 }
212
213 @Override
214 protected void refreshUiReal() {
215 invalidateOptionsMenu();
216 for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
217 refreshFragment(id);
218 }
219 refreshForNewCaps = false;
220 newCapsJids.clear();
221
222 if (accountHeader == null) return;
223
224 final var chatRequestsPref = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
225 final var accountUnreads = new HashMap<Account, Integer>();
226 binding.drawer.apply(dr -> {
227 final var items = binding.drawer.getItemAdapter().getAdapterItems();
228 final var tags = new TreeMap<Tag, Integer>();
229 final var conversations = new ArrayList<Conversation>();
230 final var removedConversations = new ArrayList<Conversation>();
231 var totalUnread = 0;
232 var dmUnread = 0;
233 var channelUnread = 0;
234 var chatRequests = 0;
235 final var selectedAccount = selectedAccount();
236 // What sort of memory footprint does this function typically have?
237 populateWithOrderedConversations(
238 conversations,
239 removedConversations,
240 false
241 );
242
243 invisibles = removedConversations.stream().anyMatch(c -> c.unreadCount(xmppConnectionService) > 0);
244 invalidateActionBarTitle();
245
246 // Reconstruct the complete list for counts and tags computation
247 conversations.addAll(removedConversations);
248
249 for (final var c : conversations) {
250 final var unread = c.unreadCount(xmppConnectionService);
251 if (selectedAccount == null || selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
252 if (c.isChatRequest(chatRequestsPref)) {
253 chatRequests++;
254 } else {
255 totalUnread += unread;
256 if (c.getMode() == Conversation.MODE_MULTI) {
257 channelUnread += unread;
258 } else {
259 dmUnread += unread;
260 }
261 }
262 }
263 var accountUnread = accountUnreads.get(c.getAccount());
264 if (accountUnread == null) accountUnread = 0;
265 accountUnreads.put(c.getAccount(), accountUnread + unread);
266 }
267
268 filterByMainFilter(conversations);
269 for (final var c : conversations) {
270 if (selectedAccount == null || selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
271 final var unread = c.unreadCount(xmppConnectionService);
272 for (final var tag : c.getTags(this)) {
273 if ("Channel".equals(tag.getName())) continue;
274 var count = tags.get(tag);
275 if (count == null) count = 0;
276 tags.put(tag, count + unread);
277 }
278 }
279 }
280
281 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
282 binding.drawer,
283 DRAWER_UNREAD_CHATS,
284 new com.mikepenz.materialdrawer.holder.StringHolder(totalUnread > 0 ? new Integer(totalUnread).toString() : null)
285 );
286
287 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
288 binding.drawer,
289 DRAWER_DIRECT_MESSAGES,
290 new com.mikepenz.materialdrawer.holder.StringHolder(dmUnread > 0 ? new Integer(dmUnread).toString() : null)
291 );
292
293 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
294 binding.drawer,
295 DRAWER_CHANNELS,
296 new com.mikepenz.materialdrawer.holder.StringHolder(channelUnread > 0 ? new Integer(channelUnread).toString() : null)
297 );
298
299 if (chatRequests > 0) {
300 if (binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHAT_REQUESTS) < 0) {
301 final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
302 final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
303 final var requests = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
304 requests.setIdentifier(DRAWER_CHAT_REQUESTS);
305 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(requests, "Chat Requests");
306 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(requests, R.drawable.ic_person_add_24dp);
307 requests.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
308 binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHANNELS) + 1), requests);
309 }
310 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
311 binding.drawer,
312 DRAWER_CHAT_REQUESTS,
313 new com.mikepenz.materialdrawer.holder.StringHolder(chatRequests > 0 ? new Integer(chatRequests).toString() : null)
314 );
315 } else {
316 binding.drawer.getItemAdapter().removeByIdentifier(DRAWER_CHAT_REQUESTS);
317 }
318
319 final var endOfMainFilters = chatRequests > 0 ? 6 : 5;
320 long id = 1000;
321 final var inDrawer = new HashMap<Tag, Long>();
322 for (final var item : ImmutableList.copyOf(items)) {
323 if (item.getIdentifier() >= 1000 && !tags.containsKey(item.getTag())) {
324 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.removeItems(binding.drawer, item);
325 } else if (item.getIdentifier() >= 1000) {
326 inDrawer.put((Tag)item.getTag(), item.getIdentifier());
327 id = item.getIdentifier() + 1;
328 }
329 }
330
331 for (final var entry : tags.entrySet()) {
332 final var badge = entry.getValue() > 0 ? entry.getValue().toString() : null;
333 if (inDrawer.containsKey(entry.getKey())) {
334 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
335 binding.drawer,
336 inDrawer.get(entry.getKey()),
337 new com.mikepenz.materialdrawer.holder.StringHolder(badge)
338 );
339 } else {
340 final var item = new com.mikepenz.materialdrawer.model.SecondaryDrawerItem();
341 item.setIdentifier(id++);
342 item.setTag(entry.getKey());
343 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(item, entry.getKey().getName());
344 if (badge != null) com.mikepenz.materialdrawer.model.interfaces.BadgeableKt.setBadgeText(item, badge);
345 final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
346 final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
347 item.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
348 binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(endOfMainFilters), item);
349 }
350 }
351
352 items.subList(endOfMainFilters, Math.min(endOfMainFilters + tags.size(), items.size())).sort((x, y) -> x.getTag() == null ? -1 : ((Comparable) x.getTag()).compareTo(y.getTag()));
353 binding.drawer.getItemAdapter().getFastAdapter().notifyDataSetChanged();
354 return kotlin.Unit.INSTANCE;
355 });
356
357
358 accountHeader.apply(ah -> {
359 //if (!refreshAccounts) return kotlin.Unit.INSTANCE;
360 refreshAccounts = false;
361 final var accounts = xmppConnectionService.getAccounts();
362 final var inHeader = new HashMap<Account, com.mikepenz.materialdrawer.model.ProfileDrawerItem>();
363 long id = 101;
364 for (final var p : ImmutableList.copyOf(accountHeader.getProfiles())) {
365 if (p instanceof com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem) continue;
366 final var pId = p.getIdentifier();
367 if (id < pId) id = pId;
368 if (accounts.contains(p.getTag()) || (accounts.size() > 1 && p.getTag() == null)) {
369 inHeader.put((Account) p.getTag(), (com.mikepenz.materialdrawer.model.ProfileDrawerItem) p);
370 } else {
371 accountHeader.removeProfile(p);
372 }
373 }
374
375 if (accounts.size() > 1 && !inHeader.containsKey(null)) {
376 final var all = new com.mikepenz.materialdrawer.model.ProfileDrawerItem();
377 all.setIdentifier(100);
378 com.mikepenz.materialdrawer.model.interfaces.DescribableKt.setDescriptionText(all, "All Accounts");
379 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(all, R.drawable.main_logo);
380 accountHeader.addProfile(all, 0);
381 }
382
383 accountHeader.removeProfileByIdentifier(DRAWER_MANAGE_PHONE_ACCOUNTS);
384 final var hasPhoneAccounts = accounts.stream().anyMatch(a -> a.getGateways("pstn").size() > 0);
385 if (hasPhoneAccounts) {
386 final var phoneAccounts = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
387 phoneAccounts.setIdentifier(DRAWER_MANAGE_PHONE_ACCOUNTS);
388 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(phoneAccounts, "Manage Phone Accounts");
389 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(phoneAccounts, R.drawable.ic_call_24dp);
390 accountHeader.addProfile(phoneAccounts, accountHeader.getProfiles().size() - 2);
391 }
392
393 final boolean nightMode = Activities.isNightMode(this);
394 for (final var a : accounts) {
395 final var size = (int) getResources().getDimension(R.dimen.avatar_on_drawer);
396 final var avatar = xmppConnectionService.getAvatarService().get(a, size, true);
397 if (avatar == null) {
398 final var task = new AvatarWorkerTask(this, R.dimen.avatar_on_drawer);
399 try { task.execute(a); } catch (final RejectedExecutionException ignored) { }
400 refreshAccounts = true;
401 }
402 final var alreadyInHeader = inHeader.get(a);
403 final com.mikepenz.materialdrawer.model.ProfileDrawerItem p;
404 if (alreadyInHeader == null) {
405 p = new com.mikepenz.materialdrawer.model.ProfileDrawerItem();
406 p.setIdentifier(id++);
407 p.setTag(a);
408 } else {
409 p = alreadyInHeader;
410 }
411 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(p, a.getDisplayName() == null ? "" : a.getDisplayName());
412 com.mikepenz.materialdrawer.model.interfaces.DescribableKt.setDescriptionText(p, a.getJid().asBareJid().toString());
413 if (avatar != null) com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconBitmap(p, FileBackend.drawDrawable(avatar).copy(Bitmap.Config.ARGB_8888, false));
414 var color = SendButtonTool.getSendButtonColor(binding.drawer, a.getPresenceStatus());
415 if (!a.isOnlineAndConnected()) {
416 if (a.getStatus().isError()) {
417 color = MaterialColors.harmonizeWithPrimary(this, ContextCompat.getColor(this, nightMode ? R.color.red_300 : R.color.red_800));
418 } else {
419 color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnSurface);
420 }
421 }
422 final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimary);
423 p.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
424 final var badgeNumber = accountUnreads.get(a);
425 p.setBadge(new com.mikepenz.materialdrawer.holder.StringHolder(badgeNumber == null || badgeNumber < 1 ? " " : badgeNumber.toString()));
426 if (alreadyInHeader == null) {
427 accountHeader.addProfile(p, accountHeader.getProfiles().size() - (hasPhoneAccounts ? 3 : 2));
428 } else {
429 accountHeader.updateProfile(p);
430 }
431 }
432 return kotlin.Unit.INSTANCE;
433 });
434 }
435
436 @Override
437 protected void onBackendConnected() {
438 final var useSavedState = savedState;
439 savedState = null;
440 if (performRedirectIfNecessary(true)) {
441 return;
442 }
443 xmppConnectionService.getNotificationService().setIsInForeground(true);
444 var intent = pendingViewIntent.pop();
445 if (intent != null) {
446 if (processViewIntent(intent)) {
447 if (binding.secondaryFragment != null) {
448 notifyFragmentOfBackendConnected(R.id.main_fragment);
449 }
450 } else {
451 intent = null;
452 }
453 }
454
455 if (intent == null) {
456 for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
457 notifyFragmentOfBackendConnected(id);
458 }
459
460 final ActivityResult activityResult = postponedActivityResult.pop();
461 if (activityResult != null) {
462 handleActivityResult(activityResult);
463 }
464 if (binding.secondaryFragment != null
465 && ConversationFragment.getConversation(this) == null) {
466 Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
467 if (conversation != null) {
468 openConversation(conversation, null);
469 }
470 }
471 showDialogsIfMainIsOverview();
472 }
473 invalidateActionBarTitle();
474
475 if (accountHeader != null || binding == null || binding.drawer == null) {
476 refreshUiReal();
477 return;
478 }
479
480 accountHeader = new com.mikepenz.materialdrawer.widget.AccountHeaderView(this);
481 final var manageAccount = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
482 manageAccount.setIdentifier(DRAWER_MANAGE_ACCOUNT);
483 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(manageAccount, getResources().getString(R.string.action_accounts));
484 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(manageAccount, R.drawable.ic_settings_24dp);
485 accountHeader.addProfiles(manageAccount);
486
487 final var addAccount = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
488 addAccount.setIdentifier(DRAWER_ADD_ACCOUNT);
489 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(addAccount, getResources().getString(R.string.action_add_account));
490 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(addAccount, R.drawable.ic_add_24dp);
491 accountHeader.addProfiles(addAccount);
492
493 final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
494 final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
495
496 final var allChats = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
497 allChats.setIdentifier(DRAWER_ALL_CHATS);
498 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(allChats, "All Chats");
499 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(allChats, R.drawable.ic_chat_24dp);
500
501 final var unreadChats = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
502 unreadChats.setIdentifier(DRAWER_UNREAD_CHATS);
503 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(unreadChats, "Unread Chats");
504 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(unreadChats, R.drawable.chat_unread_24dp);
505 unreadChats.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
506
507 final var directMessages = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
508 directMessages.setIdentifier(DRAWER_DIRECT_MESSAGES);
509 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(directMessages, "Direct Messages");
510 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(directMessages, R.drawable.ic_person_24dp);
511 directMessages.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
512
513 final var channels = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
514 channels.setIdentifier(DRAWER_CHANNELS);
515 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(channels, "Channels");
516 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(channels, R.drawable.ic_group_24dp);
517 channels.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
518
519 binding.drawer.getItemAdapter().add(
520 allChats,
521 unreadChats,
522 directMessages,
523 channels,
524 new com.mikepenz.materialdrawer.model.DividerDrawerItem()
525 );
526
527 final var settings = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
528 settings.setIdentifier(DRAWER_SETTINGS);
529 settings.setSelectable(false);
530 com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(settings, "Settings");
531 com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(settings, R.drawable.ic_settings_24dp);
532 com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.addStickyDrawerItems(binding.drawer, settings);
533
534 if (useSavedState != null) {
535 mainFilter = useSavedState.getLong("mainFilter", DRAWER_ALL_CHATS);
536 selectedTag = (HashSet<Tag>) useSavedState.getSerializable("selectedTag");
537 }
538 refreshUiReal();
539 if (useSavedState != null) binding.drawer.setSavedInstance(useSavedState);
540 accountHeader.attachToSliderView(binding.drawer);
541 if (useSavedState != null) accountHeader.withSavedInstance(useSavedState);
542
543 if (mainFilter == DRAWER_ALL_CHATS && selectedTag.isEmpty()) {
544 binding.drawer.setSelectedItemIdentifier(DRAWER_ALL_CHATS);
545 }
546
547 binding.drawer.setOnDrawerItemClickListener((v, drawerItem, pos) -> {
548 final var id = drawerItem.getIdentifier();
549 if (id != DRAWER_START_CHAT) binding.drawer.getExpandableExtension().collapse(false);
550 if (id == DRAWER_SETTINGS) {
551 startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
552 return false;
553 } else if (id == DRAWER_START_CHAT_CONTACT) {
554 launchStartConversation();
555 } else if (id == DRAWER_START_CHAT_NEW) {
556 launchStartConversation(R.id.create_contact);
557 } else if (id == DRAWER_START_CHAT_GROUP) {
558 launchStartConversation(R.id.create_private_group_chat);
559 } else if (id == DRAWER_START_CHAT_PUBLIC) {
560 launchStartConversation(R.id.create_public_channel);
561 } else if (id == DRAWER_START_CHAT_DISCOVER) {
562 launchStartConversation(R.id.discover_public_channels);
563 } else if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
564 selectedTag.clear();
565 mainFilter = id;
566 binding.drawer.getSelectExtension().deselect();
567 } else if (id >= 1000) {
568 selectedTag.clear();
569 selectedTag.add((Tag) drawerItem.getTag());
570 binding.drawer.getSelectExtension().deselect();
571 binding.drawer.getSelectExtension().select(pos, false, true);
572 }
573 binding.drawer.getSelectExtension().selectByIdentifier(mainFilter, false, true);
574
575 final var fm = getFragmentManager();
576 while (fm.getBackStackEntryCount() > 0) {
577 try {
578 fm.popBackStackImmediate();
579 } catch (IllegalStateException e) {
580 break;
581 }
582 }
583
584 refreshUi();
585 return false;
586 });
587
588 binding.drawer.setOnDrawerItemLongClickListener((v, drawerItem, pos) -> {
589 final var id = drawerItem.getIdentifier();
590 if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
591 selectedTag.clear();
592 mainFilter = id;
593 binding.drawer.getSelectExtension().deselect();
594 } else if (id >= 1000) {
595 final var tag = (Tag) drawerItem.getTag();
596 if (selectedTag.contains(tag)) {
597 selectedTag.remove(tag);
598 binding.drawer.getSelectExtension().deselect(pos);
599 } else {
600 selectedTag.add(tag);
601 binding.drawer.getSelectExtension().select(pos, false, true);
602 }
603 }
604 binding.drawer.getSelectExtension().selectByIdentifier(mainFilter, false, true);
605
606 refreshUi();
607 return true;
608 });
609
610 accountHeader.setOnAccountHeaderListener((v, profile, isCurrent) -> {
611 final var id = profile.getIdentifier();
612 if (isCurrent) return false; // Ignore switching to already selected profile
613
614 if (id == DRAWER_MANAGE_ACCOUNT) {
615 AccountUtils.launchManageAccounts(this);
616 return false;
617 }
618
619 if (id == DRAWER_ADD_ACCOUNT) {
620 startActivity(new Intent(this, EditAccountActivity.class));
621 return false;
622 }
623
624 if (id == DRAWER_MANAGE_PHONE_ACCOUNTS) {
625 final String[] permissions;
626 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
627 permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
628 } else {
629 permissions = new String[]{Manifest.permission.RECORD_AUDIO};
630 }
631 requestPermissions(permissions, REQUEST_MICROPHONE);
632 return false;
633 }
634
635 final var fm = getFragmentManager();
636 while (fm.getBackStackEntryCount() > 0) {
637 try {
638 fm.popBackStackImmediate();
639 } catch (IllegalStateException e) {
640 break;
641 }
642 }
643
644 refreshUi();
645
646 return false;
647 });
648
649 accountHeader.setOnAccountHeaderProfileImageListener((v, profile, isCurrent) -> {
650 if (isCurrent) {
651 final Account account = (Account) accountHeader.getActiveProfile().getTag();
652 if (account == null) {
653 AccountUtils.launchManageAccounts(this);
654 } else {
655 switchToAccount(account);
656 }
657 }
658 return false;
659 });
660 }
661
662 @Override
663 public boolean colorCodeAccounts() {
664 if (accountHeader != null) {
665 final var active = accountHeader.getActiveProfile();
666 if (active != null && active.getTag() != null) return false;
667 }
668 return super.colorCodeAccounts();
669 }
670
671 @Override
672 public void populateWithOrderedConversations(List<Conversation> list) {
673 populateWithOrderedConversations(list, new ArrayList<>(), true);
674 }
675
676 public void populateWithOrderedConversations(
677 @NotNull List<Conversation> list,
678 final boolean sort
679 ) {
680 populateWithOrderedConversations(list, new ArrayList<>(), sort);
681 }
682
683 // @param conversations an initially empty list representing the messages
684 // to returned as relevant
685 // @return a `boolean` value indicating whether any conversations
686 // were filtered out and therefore would be invisible to
687 // the user.
688 public void populateWithOrderedConversations(
689 @NotNull List<Conversation> retained,
690 @NotNull List<Conversation> removed,
691 final boolean sort
692 ) {
693 if (sort) {
694 super.populateWithOrderedConversations(retained);
695 } else {
696 retained.addAll(xmppConnectionService.getConversations());
697 }
698
699 filterByMainFilter(retained, removed);
700
701 final var selectedAccount = selectedAccount();
702
703 for (final var c : ImmutableList.copyOf(retained)) {
704 if (selectedAccount != null && !selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
705 retained.remove(c);
706 removed.add(c);
707 } else if (!selectedTag.isEmpty()) {
708 final var tags = new HashSet<>(c.getTags(this));
709 tags.retainAll(selectedTag);
710 if (tags.isEmpty()) {
711 retained.remove(c);
712 removed.add(c);
713 }
714 }
715 }
716 }
717
718 protected Account selectedAccount() {
719 if (accountHeader == null || accountHeader.getActiveProfile() == null) return null;
720 return (Account) accountHeader.getActiveProfile().getTag();
721 }
722
723 // Applies pre-defined filters to specify that only conversations
724 // from certain accounts or types of conversations are displayed.
725 //
726 // @param `retained` an initially-empty list representing the messages
727 // to eventually be returned as relevant.
728 // @param `removed` an initially-empty list representing the messages
729 // filtered out by tags and `filterByMainFilter`
730 protected void filterByMainFilter(
731 @NotNull List<Conversation> list,
732 @NotNull List<Conversation> removed
733 ) {
734 final var chatRequests = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
735 for (final var c : ImmutableList.copyOf(list)) {
736 if (mainFilter == DRAWER_CHANNELS && c.getMode() != Conversation.MODE_MULTI) {
737 list.remove(c);
738 removed.add(c);
739 } else if (mainFilter == DRAWER_DIRECT_MESSAGES && c.getMode() == Conversation.MODE_MULTI) {
740 list.remove(c);
741 removed.add(c);
742 } else if (mainFilter == DRAWER_UNREAD_CHATS && c.unreadCount(xmppConnectionService) < 1) {
743 list.remove(c);
744 removed.add(c);
745 } else if (mainFilter == DRAWER_CHAT_REQUESTS && !c.isChatRequest(chatRequests)) {
746 list.remove(c);
747 removed.add(c);
748 }
749 if (mainFilter != DRAWER_CHAT_REQUESTS && c.isChatRequest(chatRequests)) {
750 list.remove(c);
751 removed.add(c);
752 }
753 }
754 }
755
756 protected void filterByMainFilter(@NotNull List<Conversation> list) {
757 filterByMainFilter(list, new ArrayList<>());
758 }
759
760 @Override
761 public void launchStartConversation() {
762 launchStartConversation(0);
763 }
764
765 public void launchStartConversation(int goTo) {
766 StartConversationActivity.launch(this, accountHeader == null ? null : (Account) accountHeader.getActiveProfile().getTag(), selectedTag.stream().map(tag -> tag.getName()).collect(Collectors.joining(", ")), goTo);
767 }
768
769 private boolean performRedirectIfNecessary(boolean noAnimation) {
770 return performRedirectIfNecessary(null, noAnimation);
771 }
772
773 private boolean performRedirectIfNecessary(
774 final Conversation ignore, final boolean noAnimation) {
775 if (xmppConnectionService == null) {
776 return false;
777 }
778
779 boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore);
780 if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) {
781 final Intent intent = SignupUtils.getRedirectionIntent(this);
782 if (noAnimation) {
783 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
784 }
785 runOnUiThread(
786 () -> {
787 startActivity(intent);
788 if (noAnimation) {
789 overridePendingTransition(0, 0);
790 }
791 });
792 }
793 return mRedirectInProcess.get();
794 }
795
796 private void showDialogsIfMainIsOverview() {
797 Pair<Account, Account> incomplete = null;
798 if (xmppConnectionService != null && (incomplete = xmppConnectionService.onboardingIncomplete()) != null) {
799 FinishOnboarding.finish(xmppConnectionService, this, incomplete.first, incomplete.second);
800 }
801 if (xmppConnectionService == null || xmppConnectionService.isOnboarding()) {
802 return;
803 }
804 final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
805 if (fragment instanceof ConversationsOverviewFragment) {
806 if (ExceptionHelper.checkForCrash(this)) return;
807 if (offerToSetupDiallerIntegration()) return;
808 if (offerToDownloadStickers()) return;
809 if (openBatteryOptimizationDialogIfNeeded()) return;
810 if (requestNotificationPermissionIfNeeded()) return;
811 if (askAboutNomedia()) return;
812 xmppConnectionService.rescanStickers();
813 }
814 }
815
816 private String getBatteryOptimizationPreferenceKey() {
817 @SuppressLint("HardwareIds")
818 String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
819 return "show_battery_optimization" + (device == null ? "" : device);
820 }
821
822 private void setNeverAskForBatteryOptimizationsAgain() {
823 getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
824 }
825
826 private boolean openBatteryOptimizationDialogIfNeeded() {
827 if (isOptimizingBattery()
828 && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
829 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
830 builder.setTitle(R.string.battery_optimizations_enabled);
831 builder.setMessage(
832 getString(
833 R.string.battery_optimizations_enabled_dialog,
834 getString(R.string.app_name)));
835 builder.setPositiveButton(
836 R.string.next,
837 (dialog, which) -> {
838 final Intent intent =
839 new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
840 final Uri uri = Uri.parse("package:" + getPackageName());
841 intent.setData(uri);
842 try {
843 startActivityForResult(intent, REQUEST_BATTERY_OP);
844 } catch (final ActivityNotFoundException e) {
845 Toast.makeText(
846 this,
847 R.string.device_does_not_support_battery_op,
848 Toast.LENGTH_SHORT)
849 .show();
850 }
851 });
852 builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
853 final AlertDialog dialog = builder.create();
854 dialog.setCanceledOnTouchOutside(false);
855 dialog.show();
856 return true;
857 }
858 return false;
859 }
860
861 private boolean requestNotificationPermissionIfNeeded() {
862 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
863 && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
864 != PackageManager.PERMISSION_GRANTED) {
865 requestPermissions(
866 new String[] {Manifest.permission.POST_NOTIFICATIONS},
867 REQUEST_POST_NOTIFICATION);
868 return true;
869 }
870 return false;
871 }
872
873 private boolean askAboutNomedia() {
874 if (getPreferences().contains("nomedia")) return false;
875
876 AlertDialog.Builder builder = new AlertDialog.Builder(this);
877 builder.setTitle("Show media in gallery");
878 builder.setMessage("Would you like to show received and sent media in your system gallery?");
879 builder.setPositiveButton(R.string.yes, (dialog, which) -> {
880 getPreferences().edit().putBoolean("nomedia", false).apply();
881 });
882 builder.setNegativeButton(R.string.no, (dialog, which) -> {
883 getPreferences().edit().putBoolean("nomedia", true).apply();
884 });
885 final AlertDialog dialog = builder.create();
886 dialog.setCanceledOnTouchOutside(false);
887 dialog.show();
888 return true;
889 }
890
891 private boolean offerToDownloadStickers() {
892 int offered = getPreferences().getInt("default_stickers_offered", 0);
893 if (offered > 0) return false;
894 getPreferences().edit().putInt("default_stickers_offered", 1).apply();
895
896 AlertDialog.Builder builder = new AlertDialog.Builder(this);
897 builder.setTitle("Download Stickers?");
898 builder.setMessage("Would you like to download some default sticker packs?");
899 builder.setPositiveButton(R.string.yes, (dialog, which) -> {
900 if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
901 downloadStickers();
902 }
903 });
904 builder.setNegativeButton(R.string.no, (dialog, which) -> {
905 showDialogsIfMainIsOverview();
906 });
907 final AlertDialog dialog = builder.create();
908 dialog.setCanceledOnTouchOutside(false);
909 dialog.show();
910 return true;
911 }
912
913 private boolean offerToSetupDiallerIntegration() {
914 if (mRequestCode == DIALLER_INTEGRATION) {
915 mRequestCode = -1;
916 return true;
917 }
918 if (Build.VERSION.SDK_INT < 23) return false;
919 if (Build.VERSION.SDK_INT >= 33) {
920 if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
921 } else {
922 if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
923 }
924
925 Set<String> pstnGateways = xmppConnectionService.getAccounts().stream()
926 .flatMap(a -> a.getGateways("pstn").stream())
927 .map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
928
929 if (pstnGateways.size() < 1) return false;
930 Set<String> fromPrefs = getPreferences().getStringSet("pstn_gateways", Set.of("UPGRADE"));
931 getPreferences().edit().putStringSet("pstn_gateways", pstnGateways).apply();
932 pstnGateways.removeAll(fromPrefs);
933 if (pstnGateways.size() < 1) return false;
934
935 if (fromPrefs.contains("UPGRADE")) return false;
936
937 AlertDialog.Builder builder = new AlertDialog.Builder(this);
938 builder.setTitle("Dialler Integration");
939 builder.setMessage("Cheogram Android is able to integrate with your system's dialler app to allow dialling calls via your configured gateway " + String.join(", ", pstnGateways) + ".\n\nEnabling this integration will require granting microphone permission to the app. Would you like to enable it now?");
940 builder.setPositiveButton(R.string.yes, (dialog, which) -> {
941 final String[] permissions;
942 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
943 permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
944 } else {
945 permissions = new String[]{Manifest.permission.RECORD_AUDIO};
946 }
947 requestPermissions(permissions, REQUEST_MICROPHONE);
948 });
949 builder.setNegativeButton(R.string.no, (dialog, which) -> {
950 showDialogsIfMainIsOverview();
951 });
952 final AlertDialog dialog = builder.create();
953 dialog.setCanceledOnTouchOutside(false);
954 dialog.show();
955 return true;
956 }
957
958 private void notifyFragmentOfBackendConnected(@IdRes int id) {
959 final Fragment fragment = getFragmentManager().findFragmentById(id);
960 if (fragment instanceof OnBackendConnected callback) {
961 callback.onBackendConnected();
962 }
963 }
964
965 private void refreshFragment(@IdRes int id) {
966 final Fragment fragment = getFragmentManager().findFragmentById(id);
967 if (fragment instanceof XmppFragment xmppFragment) {
968 xmppFragment.refresh();
969 if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids);
970 }
971 }
972
973 private boolean processViewIntent(final Intent intent) {
974 final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
975 final Conversation conversation =
976 uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null;
977 if (conversation == null) {
978 Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
979 return false;
980 }
981 openConversation(conversation, intent.getExtras());
982 return true;
983 }
984
985 @Override
986 public void onRequestPermissionsResult(
987 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
988 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
989 UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
990 if (grantResults.length > 0) {
991 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
992 switch (requestCode) {
993 case REQUEST_OPEN_MESSAGE:
994 refreshUiReal();
995 ConversationFragment.openPendingMessage(this);
996 break;
997 case REQUEST_PLAY_PAUSE:
998 ConversationFragment.startStopPending(this);
999 break;
1000 case REQUEST_MICROPHONE:
1001 Intent intent = new Intent();
1002 intent.setComponent(new ComponentName("com.android.server.telecom",
1003 "com.android.server.telecom.settings.EnableAccountPreferenceActivity"));
1004 try {
1005 startActivityForResult(intent, DIALLER_INTEGRATION);
1006 } catch (ActivityNotFoundException e) {
1007 displayToast("Dialler integration not available on your OS");
1008 }
1009 break;
1010 case REQUEST_DOWNLOAD_STICKERS:
1011 downloadStickers();
1012 break;
1013 }
1014 } else {
1015 if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1016 }
1017 } else {
1018 if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1019 }
1020 }
1021
1022 private void downloadStickers() {
1023 Intent intent = new Intent(this, DownloadDefaultStickers.class);
1024 intent.putExtra("tor", xmppConnectionService.useTorToConnect());
1025 ContextCompat.startForegroundService(this, intent);
1026 displayToast("Sticker download started");
1027 showDialogsIfMainIsOverview();
1028 }
1029
1030 @Override
1031 public void onActivityResult(int requestCode, int resultCode, final Intent data) {
1032 super.onActivityResult(requestCode, resultCode, data);
1033
1034 if (requestCode == DIALLER_INTEGRATION) {
1035 mRequestCode = requestCode;
1036 try {
1037 startActivity(new Intent(android.telecom.TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
1038 } catch (ActivityNotFoundException e) {
1039 displayToast("Dialler integration not available on your OS");
1040 }
1041 return;
1042 }
1043
1044 ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
1045 if (xmppConnectionService != null) {
1046 handleActivityResult(activityResult);
1047 } else {
1048 this.postponedActivityResult.push(activityResult);
1049 }
1050 }
1051
1052 private void handleActivityResult(final ActivityResult activityResult) {
1053 if (activityResult.resultCode == Activity.RESULT_OK) {
1054 handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
1055 } else {
1056 handleNegativeActivityResult(activityResult.requestCode);
1057 }
1058 if (activityResult.requestCode == REQUEST_BATTERY_OP) {
1059 // the result code is always 0 even when battery permission were granted
1060 requestNotificationPermissionIfNeeded();
1061 XmppConnectionService.toggleForegroundService(xmppConnectionService);
1062 }
1063 }
1064
1065 private void handleNegativeActivityResult(int requestCode) {
1066 Conversation conversation = ConversationFragment.getConversationReliable(this);
1067 switch (requestCode) {
1068 case REQUEST_DECRYPT_PGP:
1069 if (conversation == null) {
1070 break;
1071 }
1072 conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1073 break;
1074 case REQUEST_BATTERY_OP:
1075 setNeverAskForBatteryOptimizationsAgain();
1076 break;
1077 }
1078 }
1079
1080 private void handlePositiveActivityResult(int requestCode, final Intent data) {
1081 Conversation conversation = ConversationFragment.getConversationReliable(this);
1082 if (conversation == null) {
1083 Log.d(Config.LOGTAG, "conversation not found");
1084 return;
1085 }
1086 switch (requestCode) {
1087 case REQUEST_DECRYPT_PGP:
1088 conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
1089 break;
1090 case REQUEST_CHOOSE_PGP_ID:
1091 long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
1092 if (id != 0) {
1093 conversation.getAccount().setPgpSignId(id);
1094 announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
1095 } else {
1096 choosePgpSignId(conversation.getAccount());
1097 }
1098 break;
1099 case REQUEST_ANNOUNCE_PGP:
1100 announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
1101 break;
1102 }
1103 }
1104
1105 @Override
1106 protected void onCreate(final Bundle savedInstanceState) {
1107 super.onCreate(savedInstanceState);
1108 savedState = savedInstanceState;
1109 ConversationMenuConfigurator.reloadFeatures(this);
1110 OmemoSetting.load(this);
1111 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
1112 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
1113 ((androidx.drawerlayout.widget.DrawerLayout) binding.getRoot()).setStatusBarBackgroundColor(SurfaceColors.SURFACE_0.getColor(this));
1114 setSupportActionBar(binding.toolbar);
1115 configureActionBar(getSupportActionBar());
1116 this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
1117 this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
1118 this.initializeFragments();
1119 this.invalidateActionBarTitle();
1120 final Intent intent;
1121 if (savedInstanceState == null) {
1122 intent = getIntent();
1123 } else {
1124 intent = savedInstanceState.getParcelable("intent");
1125 }
1126 if (isViewOrShareIntent(intent)) {
1127 pendingViewIntent.push(intent);
1128 setIntent(createLauncherIntent(this));
1129 }
1130 }
1131
1132 @Override
1133 public boolean onCreateOptionsMenu(Menu menu) {
1134 getMenuInflater().inflate(R.menu.activity_conversations, menu);
1135 final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
1136 final var reportSpamItem = menu.findItem(R.id.action_report_spam);
1137 final var fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1138 final var overview = fragment instanceof ConversationsOverviewFragment;
1139 if (qrCodeScanMenuItem != null) {
1140 if (isCameraFeatureAvailable() && (xmppConnectionService == null || !xmppConnectionService.isOnboarding())) {
1141 final var visible = getResources().getBoolean(R.bool.show_qr_code_scan) && overview;
1142 qrCodeScanMenuItem.setVisible(visible);
1143 } else {
1144 qrCodeScanMenuItem.setVisible(false);
1145 }
1146 }
1147 reportSpamItem.setVisible(overview && mainFilter == DRAWER_CHAT_REQUESTS);
1148 return super.onCreateOptionsMenu(menu);
1149 }
1150
1151 @Override
1152 public void onConversationSelected(Conversation conversation) {
1153 clearPendingViewIntent();
1154 if (ConversationFragment.getConversation(this) == conversation) {
1155 Log.d(
1156 Config.LOGTAG,
1157 "ignore onConversationSelected() because conversation is already open");
1158 return;
1159 }
1160 openConversation(conversation, null);
1161 }
1162
1163 public void clearPendingViewIntent() {
1164 if (pendingViewIntent.clear()) {
1165 Log.e(Config.LOGTAG, "cleared pending view intent");
1166 }
1167 }
1168
1169 private void displayToast(final String msg) {
1170 runOnUiThread(
1171 () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
1172 }
1173
1174 @Override
1175 public void onAffiliationChangedSuccessful(Jid jid) {}
1176
1177 @Override
1178 public void onAffiliationChangeFailed(Jid jid, int resId) {
1179 displayToast(getString(resId, jid.asBareJid().toString()));
1180 }
1181
1182 private void openConversation(Conversation conversation, Bundle extras) {
1183 final FragmentManager fragmentManager = getFragmentManager();
1184 executePendingTransactions(fragmentManager);
1185 ConversationFragment conversationFragment =
1186 (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
1187 final boolean mainNeedsRefresh;
1188 if (conversationFragment == null) {
1189 mainNeedsRefresh = false;
1190 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1191 if (mainFragment instanceof ConversationFragment) {
1192 conversationFragment = (ConversationFragment) mainFragment;
1193 } else {
1194 conversationFragment = new ConversationFragment();
1195 FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
1196 fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
1197 fragmentTransaction.addToBackStack(null);
1198 try {
1199 fragmentTransaction.commit();
1200 } catch (IllegalStateException e) {
1201 Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
1202 // allowing state loss is probably fine since view intents et all are already
1203 // stored and a click can probably be 'ignored'
1204 return;
1205 }
1206 }
1207 } else {
1208 mainNeedsRefresh = true;
1209 }
1210 conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
1211 if (mainNeedsRefresh) {
1212 refreshFragment(R.id.main_fragment);
1213 }
1214 invalidateActionBarTitle();
1215 }
1216
1217 private static void executePendingTransactions(final FragmentManager fragmentManager) {
1218 try {
1219 fragmentManager.executePendingTransactions();
1220 } catch (final Exception e) {
1221 Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
1222 }
1223 }
1224
1225 public boolean onXmppUriClicked(Uri uri) {
1226 XmppUri xmppUri = new XmppUri(uri);
1227 if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
1228 final Conversation conversation =
1229 xmppConnectionService.findUniqueConversationByJid(xmppUri);
1230 if (conversation != null) {
1231 if (xmppUri.getParameter("password") != null) {
1232 xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password"));
1233 }
1234 if (xmppUri.isAction("command")) {
1235 startCommand(conversation.getAccount(), xmppUri.getJid(), xmppUri.getParameter("node"));
1236 } else {
1237 Bundle extras = new Bundle();
1238 extras.putString(Intent.EXTRA_TEXT, xmppUri.getBody());
1239 if (xmppUri.isAction("message")) extras.putString(EXTRA_POST_INIT_ACTION, "message");
1240 openConversation(conversation, extras);
1241 }
1242 return true;
1243 }
1244 }
1245 return false;
1246 }
1247
1248 public boolean onTelUriClicked(Uri uri, Account acct) {
1249 final String tel;
1250 try {
1251 tel = PhoneNumberUtilWrapper.normalize(this, uri.getSchemeSpecificPart());
1252 } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) {
1253 return false;
1254 }
1255
1256 Set<String> gateways = (acct == null ? xmppConnectionService.getAccounts().stream() : List.of(acct).stream()).flatMap(account ->
1257 Stream.concat(
1258 account.getGateways("pstn").stream(),
1259 account.getGateways("sms").stream()
1260 )
1261 ).map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
1262
1263 for (String gateway : gateways) {
1264 if (onXmppUriClicked(Uri.parse("xmpp:" + tel + "@" + gateway))) return true;
1265 }
1266
1267 if (gateways.size() == 1 && acct != null) {
1268 openConversation(xmppConnectionService.findOrCreateConversation(acct, Jid.ofLocalAndDomain(tel, gateways.iterator().next()), false, true), null);
1269 return true;
1270 }
1271
1272 return false;
1273 }
1274
1275 @Override
1276 public boolean onOptionsItemSelected(MenuItem item) {
1277 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
1278 return false;
1279 }
1280 switch (item.getItemId()) {
1281 case android.R.id.home:
1282 FragmentManager fm = getFragmentManager();
1283 if (android.os.Build.VERSION.SDK_INT >= 26) {
1284 Fragment f = fm.getFragments().get(fm.getFragments().size() - 1);
1285 if (f != null && f instanceof ConversationFragment) {
1286 if (((ConversationFragment) f).onBackPressed()) {
1287 return true;
1288 }
1289 }
1290 }
1291 if (fm.getBackStackEntryCount() > 0) {
1292 try {
1293 fm.popBackStack();
1294 } catch (IllegalStateException e) {
1295 Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button");
1296 }
1297 return true;
1298 } else {
1299 if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer);
1300 return true;
1301 }
1302 case R.id.action_scan_qr_code:
1303 UriHandlerActivity.scan(this);
1304 return true;
1305 case R.id.action_search_all_conversations:
1306 startActivity(new Intent(this, SearchActivity.class));
1307 return true;
1308 case R.id.action_search_this_conversation: {
1309 final Conversation conversation = ConversationFragment.getConversation(this);
1310 if (conversation == null) {
1311 return true;
1312 }
1313 final Intent intent = new Intent(this, SearchActivity.class);
1314 intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
1315 startActivity(intent);
1316 return true;
1317 }
1318 case R.id.action_report_spam: {
1319 final var list = new ArrayList<Conversation>();
1320 populateWithOrderedConversations(list, false);
1321 new AlertDialog.Builder(this)
1322 .setTitle(R.string.report_spam)
1323 .setMessage("Do you really want to block all these users and report as SPAM?")
1324 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
1325 for (final var conversation : list) {
1326 final var m = conversation.getLatestMessage();
1327 xmppConnectionService.sendBlockRequest(conversation, true, m == null ? null : m.getServerMsgId());
1328 }
1329 })
1330 .setNegativeButton(R.string.no, null).show();
1331 return true;
1332 }
1333 }
1334 return super.onOptionsItemSelected(item);
1335 }
1336
1337 @Override
1338 public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
1339 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) {
1340 final ConversationFragment conversationFragment = ConversationFragment.get(this);
1341 if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) {
1342 return true;
1343 }
1344 }
1345 return super.onKeyDown(keyCode, keyEvent);
1346 }
1347
1348 @Override
1349 public void onSaveInstanceState(Bundle savedInstanceState) {
1350 final Intent pendingIntent = pendingViewIntent.peek();
1351 savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
1352 savedInstanceState.putLong("mainFilter", mainFilter);
1353 savedInstanceState.putSerializable("selectedTag", selectedTag);
1354 if (binding.drawer != null) savedInstanceState = binding.drawer.saveInstanceState(savedInstanceState);
1355 if (accountHeader != null) savedInstanceState = accountHeader.saveInstanceState(savedInstanceState);
1356 super.onSaveInstanceState(savedInstanceState);
1357 }
1358
1359 @Override
1360 public void onStart() {
1361 super.onStart();
1362 mRedirectInProcess.set(false);
1363 }
1364
1365 @Override
1366 protected void onNewIntent(final Intent intent) {
1367 super.onNewIntent(intent);
1368 if (isViewOrShareIntent(intent)) {
1369 if (xmppConnectionService != null) {
1370 clearPendingViewIntent();
1371 processViewIntent(intent);
1372 } else {
1373 pendingViewIntent.push(intent);
1374 }
1375 }
1376 setIntent(createLauncherIntent(this));
1377 }
1378
1379 @Override
1380 public void onPause() {
1381 this.mActivityPaused = true;
1382 super.onPause();
1383 }
1384
1385 @Override
1386 public void onResume() {
1387 super.onResume();
1388 this.mActivityPaused = false;
1389 }
1390
1391 private void initializeFragments() {
1392 final FragmentManager fragmentManager = getFragmentManager();
1393 FragmentTransaction transaction = fragmentManager.beginTransaction();
1394 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1395 final Fragment secondaryFragment =
1396 fragmentManager.findFragmentById(R.id.secondary_fragment);
1397 if (mainFragment != null) {
1398 if (binding.secondaryFragment != null) {
1399 if (mainFragment instanceof ConversationFragment) {
1400 getFragmentManager().popBackStack();
1401 transaction.remove(mainFragment);
1402 transaction.commit();
1403 fragmentManager.executePendingTransactions();
1404 transaction = fragmentManager.beginTransaction();
1405 transaction.replace(R.id.secondary_fragment, mainFragment);
1406 transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1407 transaction.commit();
1408 return;
1409 }
1410 } else {
1411 if (secondaryFragment instanceof ConversationFragment) {
1412 transaction.remove(secondaryFragment);
1413 transaction.commit();
1414 getFragmentManager().executePendingTransactions();
1415 transaction = fragmentManager.beginTransaction();
1416 transaction.replace(R.id.main_fragment, secondaryFragment);
1417 transaction.addToBackStack(null);
1418 transaction.commit();
1419 return;
1420 }
1421 }
1422 } else {
1423 transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1424 }
1425 if (binding.secondaryFragment != null && secondaryFragment == null) {
1426 transaction.replace(R.id.secondary_fragment, new ConversationFragment());
1427 }
1428 transaction.commit();
1429 }
1430
1431 private void invalidateActionBarTitle() {
1432 final ActionBar actionBar = getSupportActionBar();
1433 if (actionBar == null) {
1434 return;
1435 }
1436 actionBar.setHomeAsUpIndicator(0);
1437 final FragmentManager fragmentManager = getFragmentManager();
1438 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1439 if (mainFragment instanceof ConversationFragment conversationFragment) {
1440 final Conversation conversation = conversationFragment.getConversation();
1441 if (conversation != null) {
1442 actionBar.setTitle(conversation.getName());
1443 actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding() || !conversation.getJid().equals(Jid.of("cheogram.com")));
1444 ToolbarUtils.setActionBarOnClickListener(
1445 binding.toolbar,
1446 (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
1447 );
1448 return;
1449 }
1450 }
1451 final Fragment secondaryFragment =
1452 fragmentManager.findFragmentById(R.id.secondary_fragment);
1453 if (secondaryFragment instanceof ConversationFragment conversationFragment) {
1454 final Conversation conversation = conversationFragment.getConversation();
1455 if (conversation != null) {
1456 actionBar.setTitle(conversation.getName());
1457 } else {
1458 actionBar.setTitle(R.string.app_name);
1459 }
1460 } else {
1461 actionBar.setTitle(R.string.app_name);
1462 }
1463 actionBar.setDisplayHomeAsUpEnabled(true);
1464 actionBar.setHomeAsUpIndicator(invisibles ? R.drawable.menu_with_dot_24dp : R.drawable.menu_24dp);
1465 ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
1466 ToolbarUtils.setActionBarOnClickListener(
1467 binding.toolbar,
1468 (v) -> { if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer); }
1469 );
1470 }
1471
1472 private void openConversationDetails(final Conversation conversation) {
1473 if (conversation.getMode() == Conversational.MODE_MULTI) {
1474 ConferenceDetailsActivity.open(this, conversation);
1475 } else {
1476 final Contact contact = conversation.getContact();
1477 if (contact.isSelf()) {
1478 switchToAccount(conversation.getAccount());
1479 } else {
1480 switchToContactDetails(contact);
1481 }
1482 }
1483 }
1484
1485 @Override
1486 public void onConversationArchived(Conversation conversation) {
1487 if (performRedirectIfNecessary(conversation, false)) {
1488 return;
1489 }
1490 final FragmentManager fragmentManager = getFragmentManager();
1491 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1492 if (mainFragment instanceof ConversationFragment) {
1493 try {
1494 fragmentManager.popBackStack();
1495 } catch (final IllegalStateException e) {
1496 Log.w(
1497 Config.LOGTAG,
1498 "state loss while popping back state after archiving conversation",
1499 e);
1500 // this usually means activity is no longer active; meaning on the next open we will
1501 // run through this again
1502 }
1503 return;
1504 }
1505 final Fragment secondaryFragment =
1506 fragmentManager.findFragmentById(R.id.secondary_fragment);
1507 if (secondaryFragment instanceof ConversationFragment) {
1508 if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
1509 Conversation suggestion =
1510 ConversationsOverviewFragment.getSuggestion(this, conversation);
1511 if (suggestion != null) {
1512 openConversation(suggestion, null);
1513 }
1514 }
1515 }
1516 }
1517
1518 @Override
1519 public void onConversationsListItemUpdated() {
1520 Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1521 if (fragment instanceof ConversationsOverviewFragment) {
1522 ((ConversationsOverviewFragment) fragment).refresh();
1523 }
1524 }
1525
1526 @Override
1527 public void switchToConversation(Conversation conversation) {
1528 Log.d(Config.LOGTAG, "override");
1529 openConversation(conversation, null);
1530 }
1531
1532 @Override
1533 public void onConversationRead(Conversation conversation, String upToUuid) {
1534 if (!mActivityPaused && pendingViewIntent.peek() == null) {
1535 xmppConnectionService.sendReadMarker(conversation, upToUuid);
1536 } else {
1537 Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + mActivityPaused);
1538 }
1539 }
1540
1541 @Override
1542 public void onAccountUpdate() {
1543 refreshAccounts = true;
1544 this.refreshUi();
1545 }
1546
1547 @Override
1548 public void onConversationUpdate(boolean newCaps) {
1549 if (performRedirectIfNecessary(false)) {
1550 return;
1551 }
1552 refreshForNewCaps = newCaps;
1553 this.refreshUi();
1554 }
1555
1556 @Override
1557 public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
1558 if (reason != XmppConnectionService.UpdateRosterReason.AVATAR) {
1559 refreshForNewCaps = true;
1560 if (contact != null) newCapsJids.add(contact.getJid().asBareJid());
1561 }
1562 this.refreshUi();
1563 }
1564
1565 @Override
1566 public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
1567 this.refreshUi();
1568 }
1569
1570 @Override
1571 public void onShowErrorToast(int resId) {
1572 runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
1573 }
1574}