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 Log.d(Config.LOGTAG, "Dialler integration: launching " + intent.getComponent());
1005 try {
1006 startActivityForResult(intent, DIALLER_INTEGRATION);
1007 } catch (ActivityNotFoundException e) {
1008 Log.w(Config.LOGTAG, "Dialler integration: missing legacy telecom enable-account activity, trying telecomui fallback", e);
1009 intent = new Intent();
1010 intent.setComponent(new ComponentName("com.google.android.telecomui",
1011 "com.android.server.telecomui.settings.EnableAccountPreferenceActivity"));
1012 Log.d(Config.LOGTAG, "Dialler integration: launching fallback " + intent.getComponent());
1013 try {
1014 startActivityForResult(intent, DIALLER_INTEGRATION);
1015 } catch (ActivityNotFoundException e2) {
1016 Log.w(Config.LOGTAG, "Dialler integration: telecomui fallback activity not found", e2);
1017 displayToast("Dialler integration not available on your OS");
1018 }
1019 }
1020 break;
1021 case REQUEST_DOWNLOAD_STICKERS:
1022 downloadStickers();
1023 break;
1024 }
1025 } else {
1026 if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1027 }
1028 } else {
1029 if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1030 }
1031 }
1032
1033 private void downloadStickers() {
1034 Intent intent = new Intent(this, DownloadDefaultStickers.class);
1035 intent.putExtra("tor", xmppConnectionService.useTorToConnect());
1036 ContextCompat.startForegroundService(this, intent);
1037 displayToast("Sticker download started");
1038 showDialogsIfMainIsOverview();
1039 }
1040
1041 @Override
1042 public void onActivityResult(int requestCode, int resultCode, final Intent data) {
1043 super.onActivityResult(requestCode, resultCode, data);
1044
1045 if (requestCode == DIALLER_INTEGRATION) {
1046 mRequestCode = requestCode;
1047 try {
1048 final Intent phoneAccountsIntent = new Intent(android.telecom.TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
1049 Log.d(
1050 Config.LOGTAG,
1051 "Dialler integration: result="
1052 + resultCode
1053 + ", launching "
1054 + phoneAccountsIntent.getAction()
1055 + " resolved="
1056 + phoneAccountsIntent.resolveActivity(getPackageManager()));
1057 startActivity(phoneAccountsIntent);
1058 } catch (ActivityNotFoundException e) {
1059 Log.w(Config.LOGTAG, "Dialler integration: ACTION_CHANGE_PHONE_ACCOUNTS not available", e);
1060 displayToast("Dialler integration not available on your OS");
1061 }
1062 return;
1063 }
1064
1065 ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
1066 if (xmppConnectionService != null) {
1067 handleActivityResult(activityResult);
1068 } else {
1069 this.postponedActivityResult.push(activityResult);
1070 }
1071 }
1072
1073 private void handleActivityResult(final ActivityResult activityResult) {
1074 if (activityResult.resultCode == Activity.RESULT_OK) {
1075 handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
1076 } else {
1077 handleNegativeActivityResult(activityResult.requestCode);
1078 }
1079 if (activityResult.requestCode == REQUEST_BATTERY_OP) {
1080 // the result code is always 0 even when battery permission were granted
1081 requestNotificationPermissionIfNeeded();
1082 XmppConnectionService.toggleForegroundService(xmppConnectionService);
1083 }
1084 }
1085
1086 private void handleNegativeActivityResult(int requestCode) {
1087 Conversation conversation = ConversationFragment.getConversationReliable(this);
1088 switch (requestCode) {
1089 case REQUEST_DECRYPT_PGP:
1090 if (conversation == null) {
1091 break;
1092 }
1093 conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1094 break;
1095 case REQUEST_BATTERY_OP:
1096 setNeverAskForBatteryOptimizationsAgain();
1097 break;
1098 }
1099 }
1100
1101 private void handlePositiveActivityResult(int requestCode, final Intent data) {
1102 Conversation conversation = ConversationFragment.getConversationReliable(this);
1103 if (conversation == null) {
1104 Log.d(Config.LOGTAG, "conversation not found");
1105 return;
1106 }
1107 switch (requestCode) {
1108 case REQUEST_DECRYPT_PGP:
1109 conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
1110 break;
1111 case REQUEST_CHOOSE_PGP_ID:
1112 long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
1113 if (id != 0) {
1114 conversation.getAccount().setPgpSignId(id);
1115 announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
1116 } else {
1117 choosePgpSignId(conversation.getAccount());
1118 }
1119 break;
1120 case REQUEST_ANNOUNCE_PGP:
1121 announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
1122 break;
1123 }
1124 }
1125
1126 @Override
1127 protected void onCreate(final Bundle savedInstanceState) {
1128 super.onCreate(savedInstanceState);
1129 savedState = savedInstanceState;
1130 ConversationMenuConfigurator.reloadFeatures(this);
1131 OmemoSetting.load(this);
1132 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
1133 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
1134 ((androidx.drawerlayout.widget.DrawerLayout) binding.getRoot()).setStatusBarBackgroundColor(SurfaceColors.SURFACE_0.getColor(this));
1135 setSupportActionBar(binding.toolbar);
1136 configureActionBar(getSupportActionBar());
1137 this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
1138 this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
1139 this.initializeFragments();
1140 this.invalidateActionBarTitle();
1141 final Intent intent;
1142 if (savedInstanceState == null) {
1143 intent = getIntent();
1144 } else {
1145 intent = savedInstanceState.getParcelable("intent");
1146 }
1147 if (isViewOrShareIntent(intent)) {
1148 pendingViewIntent.push(intent);
1149 setIntent(createLauncherIntent(this));
1150 }
1151 }
1152
1153 @Override
1154 public boolean onCreateOptionsMenu(Menu menu) {
1155 getMenuInflater().inflate(R.menu.activity_conversations, menu);
1156 final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
1157 final var reportSpamItem = menu.findItem(R.id.action_report_spam);
1158 final var fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1159 final var overview = fragment instanceof ConversationsOverviewFragment;
1160 if (qrCodeScanMenuItem != null) {
1161 if (isCameraFeatureAvailable() && (xmppConnectionService == null || !xmppConnectionService.isOnboarding())) {
1162 final var visible = getResources().getBoolean(R.bool.show_qr_code_scan) && overview;
1163 qrCodeScanMenuItem.setVisible(visible);
1164 } else {
1165 qrCodeScanMenuItem.setVisible(false);
1166 }
1167 }
1168 reportSpamItem.setVisible(overview && mainFilter == DRAWER_CHAT_REQUESTS);
1169 return super.onCreateOptionsMenu(menu);
1170 }
1171
1172 @Override
1173 public void onConversationSelected(Conversation conversation) {
1174 clearPendingViewIntent();
1175 if (ConversationFragment.getConversation(this) == conversation) {
1176 Log.d(
1177 Config.LOGTAG,
1178 "ignore onConversationSelected() because conversation is already open");
1179 return;
1180 }
1181 openConversation(conversation, null);
1182 }
1183
1184 public void clearPendingViewIntent() {
1185 if (pendingViewIntent.clear()) {
1186 Log.e(Config.LOGTAG, "cleared pending view intent");
1187 }
1188 }
1189
1190 private void displayToast(final String msg) {
1191 runOnUiThread(
1192 () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
1193 }
1194
1195 @Override
1196 public void onAffiliationChangedSuccessful(Jid jid) {}
1197
1198 @Override
1199 public void onAffiliationChangeFailed(Jid jid, int resId) {
1200 displayToast(getString(resId, jid.asBareJid().toString()));
1201 }
1202
1203 private void openConversation(Conversation conversation, Bundle extras) {
1204 final FragmentManager fragmentManager = getFragmentManager();
1205 executePendingTransactions(fragmentManager);
1206 ConversationFragment conversationFragment =
1207 (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
1208 final boolean mainNeedsRefresh;
1209 if (conversationFragment == null) {
1210 mainNeedsRefresh = false;
1211 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1212 if (mainFragment instanceof ConversationFragment) {
1213 conversationFragment = (ConversationFragment) mainFragment;
1214 } else {
1215 conversationFragment = new ConversationFragment();
1216 FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
1217 fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
1218 fragmentTransaction.addToBackStack(null);
1219 try {
1220 fragmentTransaction.commit();
1221 } catch (IllegalStateException e) {
1222 Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
1223 // allowing state loss is probably fine since view intents et all are already
1224 // stored and a click can probably be 'ignored'
1225 return;
1226 }
1227 }
1228 } else {
1229 mainNeedsRefresh = true;
1230 }
1231 conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
1232 if (mainNeedsRefresh) {
1233 refreshFragment(R.id.main_fragment);
1234 }
1235 invalidateActionBarTitle();
1236 }
1237
1238 private static void executePendingTransactions(final FragmentManager fragmentManager) {
1239 try {
1240 fragmentManager.executePendingTransactions();
1241 } catch (final Exception e) {
1242 Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
1243 }
1244 }
1245
1246 public boolean onXmppUriClicked(Uri uri) {
1247 XmppUri xmppUri = new XmppUri(uri);
1248 if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
1249 final Conversation conversation =
1250 xmppConnectionService.findUniqueConversationByJid(xmppUri);
1251 if (conversation != null) {
1252 if (xmppUri.getParameter("password") != null) {
1253 xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password"));
1254 }
1255 if (xmppUri.isAction("command")) {
1256 startCommand(conversation.getAccount(), xmppUri.getJid(), xmppUri.getParameter("node"));
1257 } else {
1258 Bundle extras = new Bundle();
1259 extras.putString(Intent.EXTRA_TEXT, xmppUri.getBody());
1260 if (xmppUri.isAction("message")) extras.putString(EXTRA_POST_INIT_ACTION, "message");
1261 openConversation(conversation, extras);
1262 }
1263 return true;
1264 }
1265 }
1266 return false;
1267 }
1268
1269 public boolean onTelUriClicked(Uri uri, Account acct) {
1270 final String tel;
1271 try {
1272 tel = PhoneNumberUtilWrapper.normalize(this, uri.getSchemeSpecificPart());
1273 } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) {
1274 return false;
1275 }
1276
1277 Set<String> gateways = (acct == null ? xmppConnectionService.getAccounts().stream() : List.of(acct).stream()).flatMap(account ->
1278 Stream.concat(
1279 account.getGateways("pstn").stream(),
1280 account.getGateways("sms").stream()
1281 )
1282 ).map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
1283
1284 for (String gateway : gateways) {
1285 if (onXmppUriClicked(Uri.parse("xmpp:" + tel + "@" + gateway))) return true;
1286 }
1287
1288 if (gateways.size() == 1 && acct != null) {
1289 openConversation(xmppConnectionService.findOrCreateConversation(acct, Jid.ofLocalAndDomain(tel, gateways.iterator().next()), false, true), null);
1290 return true;
1291 }
1292
1293 return false;
1294 }
1295
1296 @Override
1297 public boolean onOptionsItemSelected(MenuItem item) {
1298 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
1299 return false;
1300 }
1301 switch (item.getItemId()) {
1302 case android.R.id.home:
1303 FragmentManager fm = getFragmentManager();
1304 if (android.os.Build.VERSION.SDK_INT >= 26) {
1305 Fragment f = fm.getFragments().get(fm.getFragments().size() - 1);
1306 if (f != null && f instanceof ConversationFragment) {
1307 if (((ConversationFragment) f).onBackPressed()) {
1308 return true;
1309 }
1310 }
1311 }
1312 if (fm.getBackStackEntryCount() > 0) {
1313 try {
1314 fm.popBackStack();
1315 } catch (IllegalStateException e) {
1316 Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button");
1317 }
1318 return true;
1319 } else {
1320 if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer);
1321 return true;
1322 }
1323 case R.id.action_scan_qr_code:
1324 UriHandlerActivity.scan(this);
1325 return true;
1326 case R.id.action_search_all_conversations:
1327 startActivity(new Intent(this, SearchActivity.class));
1328 return true;
1329 case R.id.action_search_this_conversation: {
1330 final Conversation conversation = ConversationFragment.getConversation(this);
1331 if (conversation == null) {
1332 return true;
1333 }
1334 final Intent intent = new Intent(this, SearchActivity.class);
1335 intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
1336 startActivity(intent);
1337 return true;
1338 }
1339 case R.id.action_report_spam: {
1340 final var list = new ArrayList<Conversation>();
1341 populateWithOrderedConversations(list, false);
1342 new AlertDialog.Builder(this)
1343 .setTitle(R.string.report_spam)
1344 .setMessage("Do you really want to block all these users and report as SPAM?")
1345 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
1346 for (final var conversation : list) {
1347 final var m = conversation.getLatestMessage();
1348 xmppConnectionService.sendBlockRequest(conversation, true, m == null ? null : m.getServerMsgId());
1349 }
1350 })
1351 .setNegativeButton(R.string.no, null).show();
1352 return true;
1353 }
1354 }
1355 return super.onOptionsItemSelected(item);
1356 }
1357
1358 @Override
1359 public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
1360 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) {
1361 final ConversationFragment conversationFragment = ConversationFragment.get(this);
1362 if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) {
1363 return true;
1364 }
1365 }
1366 return super.onKeyDown(keyCode, keyEvent);
1367 }
1368
1369 @Override
1370 public void onSaveInstanceState(Bundle savedInstanceState) {
1371 final Intent pendingIntent = pendingViewIntent.peek();
1372 savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
1373 savedInstanceState.putLong("mainFilter", mainFilter);
1374 savedInstanceState.putSerializable("selectedTag", selectedTag);
1375 if (binding.drawer != null) savedInstanceState = binding.drawer.saveInstanceState(savedInstanceState);
1376 if (accountHeader != null) savedInstanceState = accountHeader.saveInstanceState(savedInstanceState);
1377 super.onSaveInstanceState(savedInstanceState);
1378 }
1379
1380 @Override
1381 public void onStart() {
1382 super.onStart();
1383 mRedirectInProcess.set(false);
1384 }
1385
1386 @Override
1387 protected void onNewIntent(final Intent intent) {
1388 super.onNewIntent(intent);
1389 if (isViewOrShareIntent(intent)) {
1390 if (xmppConnectionService != null) {
1391 clearPendingViewIntent();
1392 processViewIntent(intent);
1393 } else {
1394 pendingViewIntent.push(intent);
1395 }
1396 }
1397 setIntent(createLauncherIntent(this));
1398 }
1399
1400 @Override
1401 public void onPause() {
1402 this.mActivityPaused = true;
1403 super.onPause();
1404 }
1405
1406 @Override
1407 public void onResume() {
1408 super.onResume();
1409 this.mActivityPaused = false;
1410 }
1411
1412 private void initializeFragments() {
1413 final FragmentManager fragmentManager = getFragmentManager();
1414 FragmentTransaction transaction = fragmentManager.beginTransaction();
1415 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1416 final Fragment secondaryFragment =
1417 fragmentManager.findFragmentById(R.id.secondary_fragment);
1418 if (mainFragment != null) {
1419 if (binding.secondaryFragment != null) {
1420 if (mainFragment instanceof ConversationFragment) {
1421 getFragmentManager().popBackStack();
1422 transaction.remove(mainFragment);
1423 transaction.commit();
1424 fragmentManager.executePendingTransactions();
1425 transaction = fragmentManager.beginTransaction();
1426 transaction.replace(R.id.secondary_fragment, mainFragment);
1427 transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1428 transaction.commit();
1429 return;
1430 }
1431 } else {
1432 if (secondaryFragment instanceof ConversationFragment) {
1433 transaction.remove(secondaryFragment);
1434 transaction.commit();
1435 getFragmentManager().executePendingTransactions();
1436 transaction = fragmentManager.beginTransaction();
1437 transaction.replace(R.id.main_fragment, secondaryFragment);
1438 transaction.addToBackStack(null);
1439 transaction.commit();
1440 return;
1441 }
1442 }
1443 } else {
1444 transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1445 }
1446 if (binding.secondaryFragment != null && secondaryFragment == null) {
1447 transaction.replace(R.id.secondary_fragment, new ConversationFragment());
1448 }
1449 transaction.commit();
1450 }
1451
1452 private void invalidateActionBarTitle() {
1453 final ActionBar actionBar = getSupportActionBar();
1454 if (actionBar == null) {
1455 return;
1456 }
1457 actionBar.setHomeAsUpIndicator(0);
1458 final FragmentManager fragmentManager = getFragmentManager();
1459 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1460 if (mainFragment instanceof ConversationFragment conversationFragment) {
1461 final Conversation conversation = conversationFragment.getConversation();
1462 if (conversation != null) {
1463 actionBar.setTitle(conversation.getName());
1464 actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding() || !conversation.getJid().equals(Jid.of("cheogram.com")));
1465 ToolbarUtils.setActionBarOnClickListener(
1466 binding.toolbar,
1467 (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
1468 );
1469 return;
1470 }
1471 }
1472 final Fragment secondaryFragment =
1473 fragmentManager.findFragmentById(R.id.secondary_fragment);
1474 if (secondaryFragment instanceof ConversationFragment conversationFragment) {
1475 final Conversation conversation = conversationFragment.getConversation();
1476 if (conversation != null) {
1477 actionBar.setTitle(conversation.getName());
1478 } else {
1479 actionBar.setTitle(R.string.app_name);
1480 }
1481 } else {
1482 actionBar.setTitle(R.string.app_name);
1483 }
1484 actionBar.setDisplayHomeAsUpEnabled(true);
1485 actionBar.setHomeAsUpIndicator(invisibles ? R.drawable.menu_with_dot_24dp : R.drawable.menu_24dp);
1486 ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
1487 ToolbarUtils.setActionBarOnClickListener(
1488 binding.toolbar,
1489 (v) -> { if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer); }
1490 );
1491 }
1492
1493 private void openConversationDetails(final Conversation conversation) {
1494 if (conversation.getMode() == Conversational.MODE_MULTI) {
1495 ConferenceDetailsActivity.open(this, conversation);
1496 } else {
1497 final Contact contact = conversation.getContact();
1498 if (contact.isSelf()) {
1499 switchToAccount(conversation.getAccount());
1500 } else {
1501 switchToContactDetails(contact);
1502 }
1503 }
1504 }
1505
1506 @Override
1507 public void onConversationArchived(Conversation conversation) {
1508 if (performRedirectIfNecessary(conversation, false)) {
1509 return;
1510 }
1511 final FragmentManager fragmentManager = getFragmentManager();
1512 final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1513 if (mainFragment instanceof ConversationFragment) {
1514 try {
1515 fragmentManager.popBackStack();
1516 } catch (final IllegalStateException e) {
1517 Log.w(
1518 Config.LOGTAG,
1519 "state loss while popping back state after archiving conversation",
1520 e);
1521 // this usually means activity is no longer active; meaning on the next open we will
1522 // run through this again
1523 }
1524 return;
1525 }
1526 final Fragment secondaryFragment =
1527 fragmentManager.findFragmentById(R.id.secondary_fragment);
1528 if (secondaryFragment instanceof ConversationFragment) {
1529 if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
1530 Conversation suggestion =
1531 ConversationsOverviewFragment.getSuggestion(this, conversation);
1532 if (suggestion != null) {
1533 openConversation(suggestion, null);
1534 }
1535 }
1536 }
1537 }
1538
1539 @Override
1540 public void onConversationsListItemUpdated() {
1541 Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1542 if (fragment instanceof ConversationsOverviewFragment) {
1543 ((ConversationsOverviewFragment) fragment).refresh();
1544 }
1545 }
1546
1547 @Override
1548 public void switchToConversation(Conversation conversation) {
1549 Log.d(Config.LOGTAG, "override");
1550 openConversation(conversation, null);
1551 }
1552
1553 @Override
1554 public void onConversationRead(Conversation conversation, String upToUuid) {
1555 if (!mActivityPaused && pendingViewIntent.peek() == null) {
1556 xmppConnectionService.sendReadMarker(conversation, upToUuid);
1557 } else {
1558 Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + mActivityPaused);
1559 }
1560 }
1561
1562 @Override
1563 public void onAccountUpdate() {
1564 refreshAccounts = true;
1565 this.refreshUi();
1566 }
1567
1568 @Override
1569 public void onConversationUpdate(boolean newCaps) {
1570 if (performRedirectIfNecessary(false)) {
1571 return;
1572 }
1573 refreshForNewCaps = newCaps;
1574 this.refreshUi();
1575 }
1576
1577 @Override
1578 public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
1579 if (reason != XmppConnectionService.UpdateRosterReason.AVATAR) {
1580 refreshForNewCaps = true;
1581 if (contact != null) newCapsJids.add(contact.getJid().asBareJid());
1582 }
1583 this.refreshUi();
1584 }
1585
1586 @Override
1587 public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
1588 this.refreshUi();
1589 }
1590
1591 @Override
1592 public void onShowErrorToast(int resId) {
1593 runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
1594 }
1595}