ConversationActivity.java

  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
 32
 33import android.annotation.SuppressLint;
 34import android.app.Activity;
 35import android.app.Fragment;
 36import android.app.FragmentManager;
 37import android.app.FragmentTransaction;
 38import android.content.ActivityNotFoundException;
 39import android.content.Context;
 40import android.content.Intent;
 41import android.databinding.DataBindingUtil;
 42import android.net.Uri;
 43import android.os.Build;
 44import android.os.Bundle;
 45import android.provider.Settings;
 46import android.support.annotation.IdRes;
 47import android.support.annotation.NonNull;
 48import android.support.v7.app.ActionBar;
 49import android.support.v7.app.AlertDialog;
 50import android.util.Log;
 51import android.view.Menu;
 52import android.view.MenuItem;
 53import android.widget.Toast;
 54
 55import org.openintents.openpgp.util.OpenPgpApi;
 56
 57import java.util.concurrent.atomic.AtomicBoolean;
 58
 59import eu.siacs.conversations.Config;
 60import eu.siacs.conversations.R;
 61import eu.siacs.conversations.databinding.ActivityConversationsBinding;
 62import eu.siacs.conversations.entities.Account;
 63import eu.siacs.conversations.entities.Conversation;
 64import eu.siacs.conversations.services.XmppConnectionService;
 65import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 66import eu.siacs.conversations.ui.interfaces.OnConversationRead;
 67import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
 68import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
 69import eu.siacs.conversations.ui.service.EmojiService;
 70import eu.siacs.conversations.ui.util.ActivityResult;
 71import eu.siacs.conversations.ui.util.PendingItem;
 72import eu.siacs.conversations.utils.ExceptionHelper;
 73import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 74
 75import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
 76
 77public class ConversationActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
 78
 79	public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
 80	public static final String EXTRA_CONVERSATION = "conversationUuid";
 81	public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
 82	public static final String EXTRA_TEXT = "text";
 83	public static final String EXTRA_NICK = "nick";
 84	public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
 85
 86
 87	//secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
 88	private static final @IdRes
 89	int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment};
 90	private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
 91	private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
 92	private ActivityConversationsBinding binding;
 93	private boolean mActivityPaused = true;
 94	private AtomicBoolean mRedirectInProcess = new AtomicBoolean(false);
 95
 96	private static boolean isViewIntent(Intent i) {
 97		return i != null && ACTION_VIEW_CONVERSATION.equals(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION);
 98	}
 99
100	private static Intent createLauncherIntent(Context context) {
101		final Intent intent = new Intent(context, ConversationActivity.class);
102		intent.setAction(Intent.ACTION_MAIN);
103		intent.addCategory(Intent.CATEGORY_LAUNCHER);
104		return intent;
105	}
106
107	@Override
108	protected void refreshUiReal() {
109		for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
110			refreshFragment(id);
111		}
112	}
113
114	@Override
115	void onBackendConnected() {
116		if (performRedirectIfNecessary(true)) {
117			return;
118		}
119		xmppConnectionService.getNotificationService().setIsInForeground(true);
120		Intent intent = pendingViewIntent.pop();
121		if (intent != null) {
122			if (processViewIntent(intent)) {
123				if (binding.secondaryFragment != null) {
124					notifyFragmentOfBackendConnected(R.id.main_fragment);
125				}
126				invalidateActionBarTitle();
127				return;
128			}
129		}
130		for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
131			notifyFragmentOfBackendConnected(id);
132		}
133
134		ActivityResult activityResult = postponedActivityResult.pop();
135		if (activityResult != null) {
136			handleActivityResult(activityResult);
137		}
138
139		invalidateActionBarTitle();
140		if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) {
141			Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
142			if (conversation != null) {
143				openConversation(conversation, null);
144			}
145		}
146		showDialogsIfMainIsOverview();
147	}
148
149	private boolean performRedirectIfNecessary(boolean noAnimation) {
150		return performRedirectIfNecessary(null, noAnimation);
151	}
152
153	private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) {
154		if (xmppConnectionService == null) {
155			return false;
156		}
157		boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore);
158		if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) {
159			final Intent intent = getRedirectionIntent(noAnimation);
160			runOnUiThread(() -> {
161				startActivity(intent);
162				if (noAnimation) {
163					overridePendingTransition(0, 0);
164				}
165			});
166		}
167		return mRedirectInProcess.get();
168	}
169
170	private Intent getRedirectionIntent(boolean noAnimation) {
171		Account pendingAccount = xmppConnectionService.getPendingAccount();
172		Intent intent;
173		if (pendingAccount != null) {
174			intent = new Intent(this, EditAccountActivity.class);
175			intent.putExtra("jid", pendingAccount.getJid().toBareJid().toString());
176		} else {
177			if (xmppConnectionService.getAccounts().size() == 0) {
178				if (Config.X509_VERIFICATION) {
179					intent = new Intent(this, ManageAccountActivity.class);
180				} else if (Config.MAGIC_CREATE_DOMAIN != null) {
181					intent = new Intent(this, WelcomeActivity.class);
182					WelcomeActivity.addInviteUri(intent, getIntent());
183				} else {
184					intent = new Intent(this, EditAccountActivity.class);
185				}
186			} else {
187				intent = new Intent(this, StartConversationActivity.class);
188			}
189		}
190		intent.putExtra("init", true);
191		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
192		if (noAnimation) {
193			intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
194		}
195		return intent;
196	}
197
198	private void showDialogsIfMainIsOverview() {
199		if (xmppConnectionService == null) {
200			return;
201		}
202		final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
203		if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
204			if (ExceptionHelper.checkForCrash(this)) {
205				return;
206			}
207			openBatteryOptimizationDialogIfNeeded();
208		}
209	}
210
211	private String getBatteryOptimizationPreferenceKey() {
212		@SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
213		return "show_battery_optimization" + (device == null ? "" : device);
214	}
215
216	private void setNeverAskForBatteryOptimizationsAgain() {
217		getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
218	}
219
220	private void openBatteryOptimizationDialogIfNeeded() {
221		if (hasAccountWithoutPush()
222				&& isOptimizingBattery()
223				&& getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
224			AlertDialog.Builder builder = new AlertDialog.Builder(this);
225			builder.setTitle(R.string.battery_optimizations_enabled);
226			builder.setMessage(R.string.battery_optimizations_enabled_dialog);
227			builder.setPositiveButton(R.string.next, (dialog, which) -> {
228				Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
229				Uri uri = Uri.parse("package:" + getPackageName());
230				intent.setData(uri);
231				try {
232					startActivityForResult(intent, REQUEST_BATTERY_OP);
233				} catch (ActivityNotFoundException e) {
234					Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
235				}
236			});
237			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
238				builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
239			}
240			AlertDialog dialog = builder.create();
241			dialog.setCanceledOnTouchOutside(false);
242			dialog.show();
243		}
244	}
245
246	private boolean hasAccountWithoutPush() {
247		for (Account account : xmppConnectionService.getAccounts()) {
248			if (account.getStatus() == Account.State.ONLINE && !xmppConnectionService.getPushManagementService().available(account)) {
249				return true;
250			}
251		}
252		return false;
253	}
254
255	private void notifyFragmentOfBackendConnected(@IdRes int id) {
256		final Fragment fragment = getFragmentManager().findFragmentById(id);
257		if (fragment != null && fragment instanceof XmppFragment) {
258			((XmppFragment) fragment).onBackendConnected();
259		}
260	}
261
262	private void refreshFragment(@IdRes int id) {
263		final Fragment fragment = getFragmentManager().findFragmentById(id);
264		if (fragment != null && fragment instanceof XmppFragment) {
265			((XmppFragment) fragment).refresh();
266		}
267	}
268
269	private boolean processViewIntent(Intent intent) {
270		Log.d(Config.LOGTAG,"process view intent");
271		String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
272		Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null;
273		if (conversation == null) {
274			Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
275			return false;
276		}
277		openConversation(conversation, intent.getExtras());
278		return true;
279	}
280
281	@Override
282	public void onRequestPermissionsResult(int requestCode,@NonNull String permissions[], @NonNull int[] grantResults) {
283		UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
284	}
285
286	@Override
287	public void onActivityResult(int requestCode, int resultCode, final Intent data) {
288		super.onActivityResult(requestCode, resultCode, data);
289		ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
290		if (xmppConnectionService != null) {
291			handleActivityResult(activityResult);
292		} else {
293			this.postponedActivityResult.push(activityResult);
294		}
295	}
296
297	private void handleActivityResult(ActivityResult activityResult) {
298		if (activityResult.resultCode == Activity.RESULT_OK) {
299			handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
300		} else {
301			handleNegativeActivityResult(activityResult.requestCode);
302		}
303	}
304
305	private void handleNegativeActivityResult(int requestCode) {
306		Conversation conversation = ConversationFragment.getConversationReliable(this);
307		switch (requestCode) {
308			case REQUEST_DECRYPT_PGP:
309				if (conversation == null) {
310					break;
311				}
312				conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
313				break;
314			case REQUEST_BATTERY_OP:
315				setNeverAskForBatteryOptimizationsAgain();
316				break;
317		}
318	}
319
320	private void handlePositiveActivityResult(int requestCode, final Intent data) {
321		Log.d(Config.LOGTAG,"positive activity result");
322		Conversation conversation = ConversationFragment.getConversationReliable(this);
323		if (conversation == null) {
324			Log.d(Config.LOGTAG,"conversation not found");
325			return;
326		}
327		switch (requestCode) {
328			case REQUEST_DECRYPT_PGP:
329				conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
330				break;
331			case REQUEST_CHOOSE_PGP_ID:
332				long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
333				if (id != 0) {
334					conversation.getAccount().setPgpSignId(id);
335					announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
336				} else {
337					choosePgpSignId(conversation.getAccount());
338				}
339				break;
340			case REQUEST_ANNOUNCE_PGP:
341				announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
342				break;
343		}
344	}
345
346	@Override
347	protected void onCreate(final Bundle savedInstanceState) {
348		super.onCreate(savedInstanceState);
349		new EmojiService(this).init();
350		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
351		this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
352		this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
353		this.initializeFragments();
354		this.invalidateActionBarTitle();
355		final Intent intent;
356		if (savedInstanceState == null) {
357			intent = getIntent();
358		} else {
359			intent = savedInstanceState.getParcelable("intent");
360		}
361		if (isViewIntent(intent)) {
362			pendingViewIntent.push(intent);
363			setIntent(createLauncherIntent(this));
364		}
365	}
366
367	@Override
368	public boolean onCreateOptionsMenu(Menu menu) {
369		getMenuInflater().inflate(R.menu.activity_conversations, menu);
370		MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
371		if (qrCodeScanMenuItem != null) {
372			Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
373			boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan)
374					&& fragment != null
375					&& fragment instanceof ConversationsOverviewFragment;
376			qrCodeScanMenuItem.setVisible(visible);
377		}
378		return super.onCreateOptionsMenu(menu);
379	}
380
381	@Override
382	public void onConversationSelected(Conversation conversation) {
383		if (ConversationFragment.getConversation(this) == conversation) {
384			Log.d(Config.LOGTAG,"ignore onConversationSelected() because conversation is already open");
385			return;
386		}
387		openConversation(conversation, null);
388	}
389
390	private void openConversation(Conversation conversation, Bundle extras) {
391		ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment);
392		final boolean mainNeedsRefresh;
393		if (conversationFragment == null) {
394			mainNeedsRefresh = false;
395			Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
396			if (mainFragment != null && mainFragment instanceof ConversationFragment) {
397				conversationFragment = (ConversationFragment) mainFragment;
398			} else {
399				conversationFragment = new ConversationFragment();
400				FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
401				fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
402				fragmentTransaction.addToBackStack(null);
403				fragmentTransaction.commit();
404			}
405		} else {
406			mainNeedsRefresh = true;
407		}
408		conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
409		if (mainNeedsRefresh) {
410			refreshFragment(R.id.main_fragment);
411		} else {
412			invalidateActionBarTitle();
413		}
414	}
415
416	@Override
417	public boolean onOptionsItemSelected(MenuItem item) {
418		switch (item.getItemId()) {
419			case android.R.id.home:
420				FragmentManager fm = getFragmentManager();
421				if (fm.getBackStackEntryCount() > 0) {
422					fm.popBackStack();
423					return true;
424				}
425				break;
426			case R.id.action_scan_qr_code:
427				UriHandlerActivity.scan(this);
428				return true;
429		}
430		return super.onOptionsItemSelected(item);
431	}
432
433	@Override
434	public void onSaveInstanceState(Bundle savedInstanceState) {
435		Intent pendingIntent = pendingViewIntent.pop();
436		savedInstanceState.putParcelable("intent", pendingIntent == null ? pendingIntent : getIntent());
437		super.onSaveInstanceState(savedInstanceState);
438	}
439
440	@Override
441	protected void onStart() {
442		final int theme = findTheme();
443		if (this.mTheme != theme) {
444			this.mSkipBackgroundBinding = true;
445			recreate();
446		} else {
447			this.mSkipBackgroundBinding = false;
448		}
449		mRedirectInProcess.set(false);
450		super.onStart();
451	}
452
453	@Override
454	protected void onNewIntent(final Intent intent) {
455		if (isViewIntent(intent)) {
456			if (xmppConnectionService != null) {
457				processViewIntent(intent);
458			} else {
459				pendingViewIntent.push(intent);
460			}
461		}
462		setIntent(createLauncherIntent(this));
463	}
464
465	@Override
466	public void onPause() {
467		this.mActivityPaused = true;
468		super.onPause();
469	}
470
471	@Override
472	public void onResume() {
473		super.onResume();
474		this.mActivityPaused = false;
475	}
476
477	private void initializeFragments() {
478		FragmentTransaction transaction = getFragmentManager().beginTransaction();
479		Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
480		Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
481		if (mainFragment != null) {
482			Log.d(Config.LOGTAG, "initializeFragment(). main fragment exists");
483			if (binding.secondaryFragment != null) {
484				if (mainFragment instanceof ConversationFragment) {
485					Log.d(Config.LOGTAG, "gained secondary fragment. moving...");
486					getFragmentManager().popBackStack();
487					transaction.remove(mainFragment);
488					transaction.commit();
489					getFragmentManager().executePendingTransactions();
490					transaction = getFragmentManager().beginTransaction();
491					transaction.replace(R.id.secondary_fragment, mainFragment);
492					transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
493					transaction.commit();
494					return;
495				}
496			} else {
497				if (secondaryFragment != null && secondaryFragment instanceof ConversationFragment) {
498					Log.d(Config.LOGTAG, "lost secondary fragment. moving...");
499					transaction.remove(secondaryFragment);
500					transaction.commit();
501					getFragmentManager().executePendingTransactions();
502					transaction = getFragmentManager().beginTransaction();
503					transaction.replace(R.id.main_fragment, secondaryFragment);
504					transaction.addToBackStack(null);
505					transaction.commit();
506					return;
507				}
508			}
509		} else {
510			transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
511		}
512		if (binding.secondaryFragment != null && secondaryFragment == null) {
513			transaction.replace(R.id.secondary_fragment, new ConversationFragment());
514		}
515		transaction.commit();
516	}
517
518	private void invalidateActionBarTitle() {
519		final ActionBar actionBar = getSupportActionBar();
520		if (actionBar != null) {
521			Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
522			if (mainFragment != null && mainFragment instanceof ConversationFragment) {
523				final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
524				if (conversation != null) {
525					actionBar.setTitle(conversation.getName());
526					actionBar.setDisplayHomeAsUpEnabled(true);
527					return;
528				}
529			}
530			actionBar.setTitle(R.string.app_name);
531			actionBar.setDisplayHomeAsUpEnabled(false);
532		}
533	}
534
535	@Override
536	public void onConversationArchived(Conversation conversation) {
537		if (performRedirectIfNecessary(conversation, false)) {
538			return;
539		}
540		Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
541		if (mainFragment != null && mainFragment instanceof ConversationFragment) {
542			getFragmentManager().popBackStack();
543			return;
544		}
545		Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
546		if (secondaryFragment != null && secondaryFragment instanceof ConversationFragment) {
547			if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
548				Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
549				if (suggestion != null) {
550					openConversation(suggestion, null);
551				}
552			}
553		}
554	}
555
556	@Override
557	public void onConversationsListItemUpdated() {
558		Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
559		if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
560			((ConversationsOverviewFragment) fragment).refresh();
561		}
562	}
563
564	@Override
565	public void switchToConversation(Conversation conversation) {
566		Log.d(Config.LOGTAG,"override");
567		openConversation(conversation,null);
568	}
569
570	@Override
571	public void onConversationRead(Conversation conversation) {
572		if (!mActivityPaused && pendingViewIntent.peek() == null) {
573			xmppConnectionService.sendReadMarker(conversation);
574		} else {
575			Log.d(Config.LOGTAG,"ignoring read callback. mActivityPaused="+Boolean.toString(mActivityPaused));
576		}
577	}
578
579	@Override
580	public void onAccountUpdate() {
581		this.refreshUi();
582	}
583
584	@Override
585	public void onConversationUpdate() {
586		if (performRedirectIfNecessary(false)) {
587			return;
588		}
589		this.refreshUi();
590	}
591
592	@Override
593	public void onRosterUpdate() {
594		this.refreshUi();
595	}
596
597	@Override
598	public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
599		this.refreshUi();
600	}
601
602	@Override
603	public void onShowErrorToast(int resId) {
604		runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
605	}
606}