package io.lbry.browser;

import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextWatcher;
import android.text.style.TypefaceSpan;
import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.Menu;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.messaging.FirebaseMessaging;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.view.ActionMode;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.GravityCompat;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.media.session.MediaButtonReceiver;
import androidx.preference.PreferenceManager;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ConnectException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLParameters;

import io.lbry.browser.adapter.NavigationMenuAdapter;
import io.lbry.browser.adapter.NotificationListAdapter;
import io.lbry.browser.adapter.StartupStageAdapter;
import io.lbry.browser.adapter.UrlSuggestionListAdapter;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.dialog.ContentScopeDialogFragment;
import io.lbry.browser.exceptions.AuthTokenInvalidatedException;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.listener.CameraPermissionListener;
import io.lbry.browser.listener.DownloadActionListener;
import io.lbry.browser.listener.FetchChannelsListener;
import io.lbry.browser.listener.FetchClaimsListener;
import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.PIPModeListener;
import io.lbry.browser.listener.ScreenOrientationListener;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.ClaimCacheKey;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.model.StartupStage;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.WalletSync;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.model.lbryinc.Reward;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.lbryinc.AndroidPurchaseTask;
import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
import io.lbry.browser.tasks.lbryinc.FetchRewardsTask;
import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
import io.lbry.browser.tasks.MergeSubscriptionsTask;
import io.lbry.browser.tasks.claim.ResolveTask;
import io.lbry.browser.tasks.lbryinc.NotificationDeleteTask;
import io.lbry.browser.tasks.lbryinc.NotificationListTask;
import io.lbry.browser.tasks.lbryinc.NotificationUpdateTask;
import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask;
import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler;
import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask;
import io.lbry.browser.tasks.wallet.SaveSharedUserStateTask;
import io.lbry.browser.tasks.wallet.SyncApplyTask;
import io.lbry.browser.tasks.wallet.SyncGetTask;
import io.lbry.browser.tasks.wallet.SyncSetTask;
import io.lbry.browser.tasks.wallet.UnlockTipsTask;
import io.lbry.browser.tasks.wallet.WalletBalanceTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.channel.ChannelFormFragment;
import io.lbry.browser.ui.channel.ChannelFragment;
import io.lbry.browser.ui.channel.ChannelManagerFragment;
import io.lbry.browser.ui.findcontent.EditorsChoiceFragment;
import io.lbry.browser.ui.findcontent.FileViewFragment;
import io.lbry.browser.ui.findcontent.FollowingFragment;
import io.lbry.browser.ui.library.LibraryFragment;
import io.lbry.browser.ui.other.AboutFragment;
import io.lbry.browser.ui.publish.PublishFormFragment;
import io.lbry.browser.ui.publish.PublishFragment;
import io.lbry.browser.ui.publish.PublishesFragment;
import io.lbry.browser.ui.findcontent.AllContentFragment;
import io.lbry.browser.ui.findcontent.SearchFragment;
import io.lbry.browser.ui.findcontent.ShuffleFragment;
import io.lbry.browser.ui.other.SettingsFragment;
import io.lbry.browser.ui.wallet.InvitesFragment;
import io.lbry.browser.ui.wallet.RewardsFragment;
import io.lbry.browser.ui.wallet.WalletFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
import io.lbry.lbrysdk.DownloadManager;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.ServiceHelper;
import io.lbry.lbrysdk.Utils;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import okhttp3.OkHttpClient;

import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.P;

public class MainActivity extends AppCompatActivity implements SdkStatusListener,
        SharedPreferences.OnSharedPreferenceChangeListener,
        ActionMode.Callback, SelectionModeListener {
    private static final String CHANNEL_ID_PLAYBACK = "io.lbry.browser.LBRY_PLAYBACK_CHANNEL";
    private static final int PLAYBACK_NOTIFICATION_ID = 3;
    private static final String SPECIAL_URL_PREFIX = "lbry://?";
    private static final int REMOTE_NOTIFICATION_REFRESH_TTL = 300000; // 5 minutes
    public static final String SKU_SKIP = "lbryskip";

    public static final int SOURCE_NOW_PLAYING_FILE = 1;
    public static final int SOURCE_NOW_PLAYING_SHUFFLE = 2;
    public static MainActivity instance;

    private boolean shuttingDown;
    private Date remoteNotifcationsLastLoaded;
    private Map<String, Class> specialRouteFragmentClassMap;
    @Getter
    private boolean inPictureInPictureMode;
    @Getter
    private boolean inFullscreenMode;
    // make tip unlock a global operation
    @Getter
    private boolean unlockingTips;

    public static SimpleExoPlayer appPlayer;
    public static Cache playerCache;
    public static boolean playerReassigned;
    public CastContext castContext;
    public static CastPlayer castPlayer;
    public static int nowPlayingSource;
    public static Claim nowPlayingClaim;
    public static String nowPlayingClaimUrl;
    public static boolean startingFilePickerActivity = false;
    public static boolean startingShareActivity = false;
    public static boolean startingPermissionRequest = false;
    public static final boolean startingSignInFlowActivity = false;

    private ActionMode actionMode;
    private BillingClient billingClient;
    @Getter
    private boolean enteringPIPMode = false;
    private boolean fullSyncInProgress = false;
    private int queuedSyncCount = 0;
    private String cameraOutputFilename;
    private Bitmap nowPlayingClaimBitmap;

    @Setter
    private BackPressInterceptor backPressInterceptor;
    private WebSocketClient webSocketClient;

    @Getter
    private String firebaseMessagingToken;

    private NotificationListAdapter notificationListAdapter;

    private Map<String, Fragment> openNavFragments;
    private static final Map<Class, Integer> fragmentClassNavIdMap = new HashMap<>();
    static {
        Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);

        fragmentClassNavIdMap.put(FollowingFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
        fragmentClassNavIdMap.put(EditorsChoiceFragment.class, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
        fragmentClassNavIdMap.put(AllContentFragment.class, NavMenuItem.ID_ITEM_ALL_CONTENT);
        fragmentClassNavIdMap.put(ShuffleFragment.class, NavMenuItem.ID_ITEM_SHUFFLE);

        fragmentClassNavIdMap.put(PublishFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH);
        fragmentClassNavIdMap.put(ChannelManagerFragment.class, NavMenuItem.ID_ITEM_CHANNELS);
        fragmentClassNavIdMap.put(LibraryFragment.class, NavMenuItem.ID_ITEM_LIBRARY);
        fragmentClassNavIdMap.put(PublishesFragment.class, NavMenuItem.ID_ITEM_PUBLISHES);

        fragmentClassNavIdMap.put(WalletFragment.class, NavMenuItem.ID_ITEM_WALLET);
        fragmentClassNavIdMap.put(RewardsFragment.class, NavMenuItem.ID_ITEM_REWARDS);
        fragmentClassNavIdMap.put(InvitesFragment.class, NavMenuItem.ID_ITEM_INVITES);

        fragmentClassNavIdMap.put(SettingsFragment.class, NavMenuItem.ID_ITEM_SETTINGS);
        fragmentClassNavIdMap.put(AboutFragment.class, NavMenuItem.ID_ITEM_ABOUT);

        // Internal (sub-)pages
        fragmentClassNavIdMap.put(FileViewFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
        fragmentClassNavIdMap.put(ChannelFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
        fragmentClassNavIdMap.put(SearchFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
    }

    public static final int REQUEST_STORAGE_PERMISSION = 1001;
    public static final int REQUEST_CAMERA_PERMISSION = 1002;
    public static final int REQUEST_SIMPLE_SIGN_IN = 2001;
    public static final int REQUEST_WALLET_SYNC_SIGN_IN = 2002;
    public static final int REQUEST_REWARDS_VERIFY_SIGN_IN = 2003;

    public static final int REQUEST_FILE_PICKER = 5001;
    public static final int REQUEST_VIDEO_CAPTURE = 5002;
    public static final int REQUEST_TAKE_PHOTO = 5003;

    // broadcast action names
    public static final String ACTION_SDK_READY = "io.lbry.browser.Broadcast.SdkReady";
    public static final String ACTION_AUTH_TOKEN_GENERATED = "io.lbry.browser.Broadcast.AuthTokenGenerated";
    public static final String ACTION_USER_AUTHENTICATION_SUCCESS = "io.lbry.browser.Broadcast.UserAuthenticationSuccess";
    public static final String ACTION_USER_SIGN_IN_SUCCESS = "io.lbry.browser.Broadcast.UserSignInSuccess";
    public static final String ACTION_USER_AUTHENTICATION_FAILED = "io.lbry.browser.Broadcast.UserAuthenticationFailed";
    public static final String ACTION_NOW_PLAYING_CLAIM_UPDATED = "io.lbry.browser.Broadcast.NowPlayingClaimUpdated";
    public static final String ACTION_NOW_PLAYING_CLAIM_CLEARED = "io.lbry.browser.Broadcast.NowPlayingClaimCleared";
    public static final String ACTION_PUBLISH_SUCCESSFUL = "io.lbry.browser.Broadcast.PublishSuccessful";
    public static final String ACTION_OPEN_ALL_CONTENT_TAG = "io.lbry.browser.Broadcast.OpenAllContentTag";
    public static final String ACTION_WALLET_BALANCE_UPDATED = "io.lbry.browser.Broadcast.WalletBalanceUpdated";
    public static final String ACTION_OPEN_CHANNEL_URL = "io.lbry.browser.Broadcast.OpenChannelUrl";
    public static final String ACTION_OPEN_WALLET_PAGE = "io.lbry.browser.Broadcast.OpenWalletPage";
    public static final String ACTION_OPEN_REWARDS_PAGE = "io.lbry.browser.Broadcast.OpenRewardsPage";
    public static final String ACTION_SAVE_SHARED_USER_STATE = "io.lbry.browser.Broadcast.SaveSharedUserState";

    // preference keys
    public static final String PREFERENCE_KEY_BACKGROUND_PLAYBACK = "io.lbry.browser.preference.userinterface.BackgroundPlayback";
    public static final String PREFERENCE_KEY_MEDIA_AUTOPLAY = "io.lbry.browser.preference.userinterface.MediaAutoplay";
    public static final String PREFERENCE_KEY_DARK_MODE = "io.lbry.browser.preference.userinterface.DarkMode";
    public static final String PREFERENCE_KEY_SHOW_MATURE_CONTENT = "io.lbry.browser.preference.userinterface.ShowMatureContent";
    public static final String PREFERENCE_KEY_SHOW_URL_SUGGESTIONS = "io.lbry.browser.preference.userinterface.UrlSuggestions";
    public static final String PREFERENCE_KEY_MINI_PLAYER_BOTTOM_MARGIN = "io.lbry.browser.preference.userinterface.MiniPlayerBottomMargin";
    public static final String PREFERENCE_KEY_NOTIFICATION_COMMENTS = "io.lbry.browser.preference.notifications.Comments";
    public static final String PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS = "io.lbry.browser.preference.notifications.Subscriptions";
    public static final String PREFERENCE_KEY_NOTIFICATION_REWARDS = "io.lbry.browser.preference.notifications.Rewards";
    public static final String PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS = "io.lbry.browser.preference.notifications.ContentInterests";
    public static final String PREFERENCE_KEY_NOTIFICATION_CREATOR = "io.lbry.browser.preference.notifications.Creator";
    public static final String PREFERENCE_KEY_KEEP_SDK_BACKGROUND = "io.lbry.browser.preference.other.KeepSdkInBackground";
    public static final String PREFERENCE_KEY_PARTICIPATE_DATA_NETWORK = "io.lbry.browser.preference.other.ParticipateInDataNetwork";
    public static final String PREFERENCE_KEY_SEND_BUFFERING_EVENTS = "io.lbry.browser.preference.other.SendBufferingEvents";
    public static final String PREFERENCE_KEY_SHARE_USAGE_DATA = "io.lbry.browser.preference.other.ShareUsageData";

    // Internal flags / setting preferences
    public static final String PREFERENCE_KEY_INTERNAL_SKIP_WALLET_ACCOUNT = "io.lbry.browser.preference.internal.WalletSkipAccount";
    public static final String PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED = "io.lbry.browser.preference.internal.WalletSyncEnabled";
    public static final String PREFERENCE_KEY_INTERNAL_WALLET_RECEIVE_ADDRESS = "io.lbry.browser.preference.internal.WalletReceiveAddress";
    public static final String PREFERENCE_KEY_INTERNAL_REWARDS_NOT_INTERESTED = "io.lbry.browser.preference.internal.RewardsNotInterested";
    public static final String PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED = "io.lbry.browser.preference.internal.NewAndroidRewardClaimed";
    public static final String PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE = "io.lbry.browser.preference.internal.InitialSubscriptionMergeDone";

    public static final String PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED = "io.lbry.browser.preference.internal.FirstRunCompleted";
    public static final String PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED = "io.lbry.browser.preference.internal.FirstAuthCompleted";

    private final int CHECK_SDK_READY_INTERVAL = 1000;

    public static final String PREFERENCE_KEY_AUTH_TOKEN = "io.lbry.browser.Preference.AuthToken";

    public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX";
    public static final String SECURE_VALUE_FIRST_RUN_PASSWORD = "firstRunPassword";

    private static final String TAG = "LbryMain";

    private NavigationMenuAdapter navMenuAdapter;
    private UrlSuggestionListAdapter urlSuggestionListAdapter;
    private List<UrlSuggestion> recentUrlHistory;
    private boolean hasLoadedFirstBalance;

    // broadcast receivers
    private BroadcastReceiver serviceActionsReceiver;
    private BroadcastReceiver requestsReceiver;

    private static boolean appStarted;
    private boolean serviceRunning;
    private CheckSdkReadyTask checkSdkReadyTask;
    private PlayerNotificationManager playerNotificationManager;
    private MediaSessionCompat mediaSession;
    private boolean receivedStopService;
    private ActionBarDrawerToggle toggle;
    private SwipeRefreshLayout notificationsSwipeContainer;
    private SyncSetTask syncSetTask = null;
    private List<WalletSync> pendingSyncSetQueue;
    @Getter
    private DatabaseHelper dbHelper;
    private int selectedMenuItemId = -1;
    private List<CameraPermissionListener> cameraPermissionListeners;
    private List<DownloadActionListener> downloadActionListeners;
    private List<FilePickerListener> filePickerListeners;
    private List<PIPModeListener> pipModeListeners;
    private List<ScreenOrientationListener> screenOrientationListeners;
    private List<SdkStatusListener> sdkStatusListeners;
    private List<StoragePermissionListener> storagePermissionListeners;
    private List<WalletBalanceListener> walletBalanceListeners;
    private List<FetchClaimsListener> fetchClaimsListeners;
    private List<FetchChannelsListener> fetchChannelsListeners;
    @Getter
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private boolean walletBalanceUpdateScheduled;
    private boolean shouldOpenUserSelectedMenuItem;
    private boolean walletSyncScheduled;
    private String pendingAllContentTag;
    private String pendingChannelUrl;
    private boolean pendingOpenWalletPage;
    private boolean pendingOpenRewardsPage;
    private boolean pendingFollowingReload;

    // startup stages (to be able to determine how far a user made it if startup fails)
    // and display a more useful message for troubleshooting
    private static final int STARTUP_STAGE_INSTALL_ID_LOADED = 1;
    private static final int STARTUP_STAGE_KNOWN_TAGS_LOADED = 2;
    private static final int STARTUP_STAGE_EXCHANGE_RATE_LOADED = 3;
    private static final int STARTUP_STAGE_USER_AUTHENTICATED = 4;
    private static final int STARTUP_STAGE_NEW_INSTALL_DONE = 5;
    private static final int STARTUP_STAGE_SUBSCRIPTIONS_LOADED = 6;
    private static final int STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED = 7;
    private static final int STARTUP_STAGE_BLOCK_LIST_LOADED = 8;
    private static final int STARTUP_STAGE_FILTER_LIST_LOADED = 9;
    private static final int DEFAULT_MINI_PLAYER_MARGIN = 4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        instance = this;
        // workaround to fix dark theme because https://issuetracker.google.com/issues/37124582
        try {
            new WebView(this);
        } catch (Exception ex) {
            // pass (don't fail initialization on some _weird_ device implementations)
        }
        AppCompatDelegate.setDefaultNightMode(isDarkMode() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);

        initKeyStore();
        loadAuthToken();

        if (Build.VERSION.SDK_INT >= M && !isDarkMode()) {
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
        initSpecialRouteMap();

        LbryAnalytics.init(this);
        try {
            FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener<String>() {
                @Override
                public void onComplete(@NonNull Task<String> task) {
                    if (!task.isSuccessful()) {
                        return;
                    }
                    firebaseMessagingToken = task.getResult();
                }
            });
        } catch (IllegalStateException ex) {
            // pass
        }

        super.onCreate(savedInstanceState);
        dbHelper = new DatabaseHelper(this);
        checkNotificationOpenIntent(getIntent());
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        updateMiniPlayerMargins();

        // setup the billing client in main activity (to handle cases where the verification purchase flow may have been interrupted)
        billingClient = BillingClient.newBuilder(this)
                .setListener(new PurchasesUpdatedListener() {
                    @Override
                    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
                        int responseCode = billingResult.getResponseCode();
                        if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null)
                        {
                            for (Purchase purchase : purchases) {
                                handlePurchase(purchase);
                            }
                        }
                    }
                })
                .enablePendingPurchases()
                .build();
        establishBillingClientConnection();

        playerNotificationManager = new PlayerNotificationManager(
                this, LbrynetService.NOTIFICATION_CHANNEL_ID, PLAYBACK_NOTIFICATION_ID, new PlayerNotificationDescriptionAdapter());

        // TODO: Check Google Play Services availability
        // castContext = CastContext.getSharedInstance(this);

        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.content_main), new OnApplyWindowInsetsListener() {
            @Override
            public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                ViewCompat.onApplyWindowInsets(findViewById(R.id.url_suggestions_container),
                        insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));

                return ViewCompat.onApplyWindowInsets(v,
                        insets.replaceSystemWindowInsets(
                                0,
                                0,
                                0,
                                insets.getSystemWindowInsetBottom()));
            }
        });

        // register receivers
        registerRequestsReceiver();
        registerServiceActionsReceiver();

        View decorView = getWindow().getDecorView();
        decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
            @Override
            public void onSystemUiVisibilityChange(int visibility) {
                if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
                    // not fullscreen
                }
            }
        });

        // setup uri bar
        setupUriBar();
        initNotificationsPage();
        loadUnseenNotificationsCount();

        // other
        pendingSyncSetQueue = new ArrayList<>();
        openNavFragments = new HashMap<>();

        cameraPermissionListeners = new ArrayList<>();
        downloadActionListeners = new ArrayList<>();
        fetchChannelsListeners = new ArrayList<>();
        fetchClaimsListeners = new ArrayList<>();
        filePickerListeners = new ArrayList<>();
        pipModeListeners = new ArrayList<>();
        screenOrientationListeners = new ArrayList<>();
        sdkStatusListeners = new ArrayList<>();
        storagePermissionListeners = new ArrayList<>();
        walletBalanceListeners = new ArrayList<>();

        sdkStatusListeners.add(this);

        FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.addOnBackStackChangedListener(backStackChangedListener);

        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        toggle = new ActionBarDrawerToggle(
                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) {
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
                if (shouldOpenUserSelectedMenuItem) {
                    openSelectedMenuItem();
                    shouldOpenUserSelectedMenuItem = false;
                }
            }

            @Override
            public void onDrawerSlide(View drawerView, float slideOffset) {
                if (slideOffset != 0) {
                    clearWunderbarFocus(findViewById(R.id.wunderbar));
                    hideNotifications();
                }
                super.onDrawerSlide(drawerView, slideOffset);
            }
        };
        drawer.addDrawerListener(toggle);
        toggle.syncState();
        toggle.setToolbarNavigationClickListener((view) -> {
            if (toggle != null && !toggle.isDrawerIndicatorEnabled()) {
                FragmentManager manager = getSupportFragmentManager();
                if (manager != null) {
                    manager.popBackStack();
                    setSelectedNavMenuItemForFragment(getCurrentFragment());
                }
            }
        });

        findViewById(R.id.global_now_playing_close).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                stopExoplayer();
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
                nowPlayingClaim = null;
                nowPlayingClaimUrl = null;
                nowPlayingClaimBitmap = null;
                findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
            }
        });

        findViewById(R.id.wunderbar_notifications).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                View container = findViewById(R.id.notifications_container);
                if (container.getVisibility() != View.VISIBLE) {
                    showNotifications();
                } else {
                    hideNotifications();
                }
            }
        });

        notificationsSwipeContainer = findViewById(R.id.notifications_list_swipe_container);
        notificationsSwipeContainer.setColorSchemeResources(R.color.nextLbryGreen);
        notificationsSwipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                notificationsSwipeContainer.setRefreshing(true);
                loadRemoteNotifications(false);
            }
        });

        findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) {
                    hideNotifications();
                    if (nowPlayingSource == SOURCE_NOW_PLAYING_SHUFFLE) {
                        openFragment(ShuffleFragment.class, true, NavMenuItem.ID_ITEM_SHUFFLE);
                    } else {
                        openFileUrl(nowPlayingClaimUrl);
                    }
                }
            }
        });

        // display custom navigation menu
        LinearLayoutManager llm = new LinearLayoutManager(this);
        RecyclerView navItemsView = findViewById(R.id.nav_view_items);
        navItemsView.setLayoutManager(llm);
        navMenuAdapter = new NavigationMenuAdapter(flattenNavMenu(buildNavMenu(this)), this);
        navMenuAdapter.setListener(new NavigationMenuAdapter.NavigationMenuItemClickListener() {
            @Override
            public void onNavigationMenuItemClicked(NavMenuItem menuItem) {
                if (navMenuAdapter.getCurrentItemId() == menuItem.getId() && !Arrays.asList(
                        NavMenuItem.ID_ITEM_FOLLOWING, NavMenuItem.ID_ITEM_ALL_CONTENT, NavMenuItem.ID_ITEM_WALLET).contains(menuItem.getId())) {
                    // already open
                    navMenuAdapter.setCurrentItem(menuItem);
                    closeDrawer();
                    return;
                }

                navMenuAdapter.setCurrentItem(menuItem);
                shouldOpenUserSelectedMenuItem = true;
                selectedMenuItemId = menuItem.getId();
                closeDrawer();
            }
        });
        navItemsView.setAdapter(navMenuAdapter);

        findViewById(R.id.sign_in_button_container).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                walletSyncSignIn();
            }
        });
    }

    private void updateMiniPlayerMargins() {
        // mini-player bottom margin setting
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        int miniPlayerBottomMargin = Helper.parseInt(
                sp.getString(PREFERENCE_KEY_MINI_PLAYER_BOTTOM_MARGIN, String.valueOf(DEFAULT_MINI_PLAYER_MARGIN)), DEFAULT_MINI_PLAYER_MARGIN);
        ConstraintLayout.LayoutParams lp = (ConstraintLayout.LayoutParams) findViewById(R.id.global_now_playing_card).getLayoutParams();
        int scaledMiniPlayerMargin = getScaledValue(DEFAULT_MINI_PLAYER_MARGIN);
        int scaledMiniPlayerBottomMargin = getScaledValue(miniPlayerBottomMargin);
        lp.setMargins(scaledMiniPlayerMargin, 0, scaledMiniPlayerMargin, scaledMiniPlayerBottomMargin);
    }

    public boolean isDarkMode() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(PREFERENCE_KEY_DARK_MODE, false);
    }

    public boolean isBackgroundPlaybackEnabled() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(PREFERENCE_KEY_BACKGROUND_PLAYBACK, true);
    }

    public boolean isMediaAutoplayEnabled() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(PREFERENCE_KEY_MEDIA_AUTOPLAY, true);
    }

    public boolean initialSubscriptionMergeDone() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, false);
    }

    private void initSpecialRouteMap() {
        specialRouteFragmentClassMap = new HashMap<>();
        specialRouteFragmentClassMap.put("about", AboutFragment.class);
        specialRouteFragmentClassMap.put("allcontent", AllContentFragment.class);
        specialRouteFragmentClassMap.put("channels", ChannelManagerFragment.class);
        specialRouteFragmentClassMap.put("invite", InvitesFragment.class);
        specialRouteFragmentClassMap.put("invites", InvitesFragment.class);
        specialRouteFragmentClassMap.put("library", LibraryFragment.class);
        specialRouteFragmentClassMap.put("publish", PublishFragment.class);
        specialRouteFragmentClassMap.put("publishes", PublishesFragment.class);
        specialRouteFragmentClassMap.put("following", FollowingFragment.class);
        specialRouteFragmentClassMap.put("rewards", RewardsFragment.class);
        specialRouteFragmentClassMap.put("settings", SettingsFragment.class);
        specialRouteFragmentClassMap.put("subscription", FollowingFragment.class);
        specialRouteFragmentClassMap.put("subscriptions", FollowingFragment.class);
        specialRouteFragmentClassMap.put("surf", ShuffleFragment.class);
        specialRouteFragmentClassMap.put("wallet", WalletFragment.class);
        specialRouteFragmentClassMap.put("discover", FollowingFragment.class);
    }

    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        checkSendToIntent(intent);
        checkUrlIntent(intent);
        checkNotificationOpenIntent(intent);
    }

    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        switch (newConfig.orientation) {
            case Configuration.ORIENTATION_PORTRAIT:
                for (ScreenOrientationListener listener : screenOrientationListeners) {
                    listener.onPortraitOrientationEntered();
                }
                break;
            case  Configuration.ORIENTATION_LANDSCAPE:
                for (ScreenOrientationListener listener : screenOrientationListeners) {
                    listener.onLandscapeOrientationEntered();
                }
                break;
        }
    }

    public void setActionBarTitle(int stringResourceId) {
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            SpannableString spannable = new SpannableString(getString(stringResourceId));
            spannable.setSpan(new TypefaceSpan("inter"), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            actionBar.setTitle(spannable);
        }
    }

    public void addScreenOrientationListener(ScreenOrientationListener listener) {
        if (!screenOrientationListeners.contains(listener)) {
            screenOrientationListeners.add(listener);
        }
    }

    public void removeScreenOrientationListener(ScreenOrientationListener listener) {
        screenOrientationListeners.remove(listener);
    }

    public void addDownloadActionListener(DownloadActionListener listener) {
        if (!downloadActionListeners.contains(listener)) {
            downloadActionListeners.add(listener);
        }
    }

    public void removeDownloadActionListener(DownloadActionListener listener) {
        downloadActionListeners.remove(listener);
    }

    public void addFilePickerListener(FilePickerListener listener) {
        if (!filePickerListeners.contains(listener)) {
            filePickerListeners.add(listener);
        }
    }

    public void removeFilePickerListener(FilePickerListener listener) {
        filePickerListeners.remove(listener);
    }

    public void addPIPModeListener(PIPModeListener listener) {
        if (!pipModeListeners.contains(listener)) {
            pipModeListeners.add(listener);
        }
    }

    public void removePIPModeListener(PIPModeListener listener) {
        pipModeListeners.remove(listener);
    }

    public void addCameraPermissionListener(CameraPermissionListener listener) {
        if (!cameraPermissionListeners.contains(listener)) {
            cameraPermissionListeners.add(listener);
        }
    }

    public void removeCameraPermissionListener(CameraPermissionListener listener) {
        cameraPermissionListeners.remove(listener);
    }


    public void addSdkStatusListener(SdkStatusListener listener) {
        if (!sdkStatusListeners.contains(listener)) {
            sdkStatusListeners.add(listener);
        }
    }

    public void removeSdkStatusListener(SdkStatusListener listener) {
        sdkStatusListeners.remove(listener);
    }

    public void addStoragePermissionListener(StoragePermissionListener listener) {
        if (!storagePermissionListeners.contains(listener)) {
            storagePermissionListeners.add(listener);
        }
    }

    public void removeStoragePermissionListener(StoragePermissionListener listener) {
        storagePermissionListeners.remove(listener);
    }

    public void addWalletBalanceListener(WalletBalanceListener listener) {
        if (!walletBalanceListeners.contains(listener)) {
            walletBalanceListeners.add(listener);
        }
    }

    public void removeWalletBalanceListener(WalletBalanceListener listener) {
        walletBalanceListeners.remove(listener);
    }

    public void restoreWalletContainerPosition() {
        View floatingBalance = findViewById(R.id.floating_balance_main_container);

        ObjectAnimator animation = ObjectAnimator.ofFloat(floatingBalance, "translationY", 0f);
        animation.setDuration(250).start();
    }

    public void translateFloatingWallet(float initialY) {
        if (findViewById(R.id.floating_balance_main_container).getY() == initialY) {
            ObjectAnimator animation = ObjectAnimator.ofFloat(findViewById(R.id.floating_balance_main_container), "translationY", 2 * findViewById(R.id.floating_balance_main_container).getHeight());
            animation.setDuration(300).start();
        }
    }

    public void removeNavFragment(Class fragmentClass, int navItemId) {
        String key = buildNavFragmentKey(fragmentClass, navItemId, null);
        openNavFragments.remove(key);
    }

    public void addFetchChannelsListener(FetchChannelsListener listener) {
        if (!fetchChannelsListeners.contains(listener)) {
            fetchChannelsListeners.add(listener);
        }
    }
    public void removeFetchChannelsListener(FetchChannelsListener listener) {
        fetchChannelsListeners.remove(listener);
    }

    public void addFetchClaimsListener(FetchClaimsListener listener) {
        if (!fetchClaimsListeners.contains(listener)) {
            fetchClaimsListeners.add(listener);
        }
    }
    public void removeFetchClaimsListener(FetchClaimsListener listener) {
        fetchClaimsListeners.remove(listener);
    }

    private void openSelectedMenuItem() {
        hideNotifications();

        switch (selectedMenuItemId) {
            case NavMenuItem.ID_ITEM_FOLLOWING:
                openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING);
                break;
            case NavMenuItem.ID_ITEM_EDITORS_CHOICE:
                openFragment(EditorsChoiceFragment.class, true, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
                break;
            case NavMenuItem.ID_ITEM_ALL_CONTENT:
                openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT);
                break;
            case NavMenuItem.ID_ITEM_SHUFFLE:
                openFragment(ShuffleFragment.class, true, NavMenuItem.ID_ITEM_SHUFFLE);
                break;

            case NavMenuItem.ID_ITEM_NEW_PUBLISH:
                openFragment(PublishFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH);
                break;
            case NavMenuItem.ID_ITEM_CHANNELS:
                openFragment(ChannelManagerFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS);
                break;
            case NavMenuItem.ID_ITEM_LIBRARY:
                openFragment(LibraryFragment.class, true, NavMenuItem.ID_ITEM_LIBRARY);
                break;
            case NavMenuItem.ID_ITEM_PUBLISHES:
                openFragment(PublishesFragment.class,  true, NavMenuItem.ID_ITEM_PUBLISHES);
                break;

            case NavMenuItem.ID_ITEM_WALLET:
                openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
                break;
            case NavMenuItem.ID_ITEM_REWARDS:
                openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
                break;
            case NavMenuItem.ID_ITEM_INVITES:
                openFragment(InvitesFragment.class, true, NavMenuItem.ID_ITEM_INVITES);
                break;

            case NavMenuItem.ID_ITEM_SETTINGS:
                openFragment(SettingsFragment.class, true, NavMenuItem.ID_ITEM_SETTINGS);
                break;
            case NavMenuItem.ID_ITEM_ABOUT:
                openFragment(AboutFragment.class, true, NavMenuItem.ID_ITEM_ABOUT);
                break;
        }
    }

    public void openChannelClaim(Claim claim) {
        Map<String, Object> params = new HashMap<>();
        params.put("url", !Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
        params.put("claim", getCachedClaimForUrl(claim.getPermanentUrl()));
        openFragment(ChannelFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
    }

    public void openChannelForm(Claim claim) {
        Map<String, Object> params = new HashMap<>();
        if (claim != null) {
            params.put("claim", claim);
        }
        openFragment(ChannelFormFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS, params);
    }

    public void openPublishesOnSuccessfulPublish() {
        // close publish form
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    getSupportFragmentManager().popBackStack();
                    openFragment(PublishesFragment.class, true, NavMenuItem.ID_ITEM_PUBLISHES);
                } catch (IllegalStateException ex) {
                    // pass
                    try {
                        onBackPressed();
                    } catch (IllegalStateException iex) {
                        // if this fails on some devices. what's the solution?
                    }
                }
            }
        });
    }

    public void openPublishForm(Claim claim) {
        Map<String, Object> params = new HashMap<>();
        if (claim != null) {
            params.put("claim", claim);
        }
        openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
    }

    public void openChannelUrl(String url, String source) {
        Map<String, Object> params = new HashMap<>();
        params.put("url", url);
        params.put("claim", getCachedClaimForUrl(url));
        if (!Helper.isNullOrEmpty(source)) {
            params.put("source", source);
        }
        openFragment(ChannelFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
    }
    public void openChannelUrl(String url) {
        openChannelUrl(url, null);
    }

    private Claim getCachedClaimForUrl(String url) {
        ClaimCacheKey key = new ClaimCacheKey();
        key.setUrl(url);
        return Lbry.claimCache.containsKey(key) ? Lbry.claimCache.get(key) : null;
    }

    public void setWunderbarValue(String value) {
        EditText wunderbar = findViewById(R.id.wunderbar);
        wunderbar.setText(value);
        wunderbar.setSelection(0);
    }

    public void openAllContentFragmentWithTag(String tag) {
        Map<String, Object> params = new HashMap<>();
        params.put("singleTag", tag);
        openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT, params);
    }

    public void openFileUrl(String url) {
        openFileUrl(url, null);
    }
    public void openFileUrl(String url, String source) {
        Map<String, Object> params = new HashMap<>();
        params.put("url", url);
        if (!Helper.isNullOrEmpty(source)) {
            params.put("source", source);
        }
        openFragment(FileViewFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
    }

    private void openSpecialUrl(String url, String source) {
        String specialPath = url.substring(8).toLowerCase();
        if (specialRouteFragmentClassMap.containsKey(specialPath)) {
            Class fragmentClass = specialRouteFragmentClassMap.get(specialPath);
            if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
                Map<String, Object> params = null;
                if (!Helper.isNullOrEmpty(source)) {
                    params = new HashMap<>();
                    params.put("source",  source);
                }

                openFragment(
                        specialRouteFragmentClassMap.get(specialPath),
                        true,
                        fragmentClassNavIdMap.get(fragmentClass),
                        params
                );
            }
        }
    }

    public void openSendTo(String path) {
        Map<String, Object> params = new HashMap<>();
        params.put("directFilePath", path);
        openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
    }

    public void openFileClaim(Claim claim) {
        Map<String, Object> params = new HashMap<>();
        params.put("claimId", claim.getClaimId());
        params.put("url", !Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
        openFragment(FileViewFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
    }

    public void openRewards() {
        openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
    }

    private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
        @Override
        public void onBackStackChanged() {
            FragmentManager manager = getSupportFragmentManager();
            if (manager != null) {
                Fragment currentFragment = getCurrentFragment();

            }
        }
    };

    public void setSelectedMenuItemForFragment(Fragment fragment) {
        if (fragment != null) {
            Class fragmentClass = fragment.getClass();
            if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
                navMenuAdapter.setCurrentItem(fragmentClassNavIdMap.get(fragmentClass));
            }
        }
    }

    private void renderPictureInPictureMode() {
        findViewById(R.id.content_main).setVisibility(View.GONE);
        findViewById(R.id.floating_balance_main_container).setVisibility(View.GONE);
        findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
        //findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
        findViewById(R.id.app_bar_main_container).setFitsSystemWindows(true);
        hideNotifications();
        hideActionBar();
        dismissActiveDialogs();

        for (PIPModeListener listener : pipModeListeners) {
            listener.onEnterPIPMode();
        }

        View pipPlayerContainer = findViewById(R.id.pip_player_container);
        PlayerView pipPlayer = findViewById(R.id.pip_player);
        pipPlayer.setPlayer(appPlayer);
        pipPlayer.setUseController(false);
        pipPlayerContainer.setVisibility(View.VISIBLE);
        playerReassigned = true;
    }

    private void dismissActiveDialogs() {
        for( Fragment fragment: getSupportFragmentManager().getFragments() ){
            if (fragment instanceof DialogFragment){
                ((DialogFragment) fragment).dismiss();
            }
        }
    }

    private void renderFullMode() {
        if (!inFullscreenMode) {
            showActionBar();
        } else {
            findViewById(R.id.app_bar_main_container).setFitsSystemWindows(false);
        }

        findViewById(R.id.content_main).setVisibility(View.VISIBLE);
        Fragment fragment = getCurrentFragment();
        boolean canShowFloatingBalance = fragment == null ||
                fragment instanceof FollowingFragment ||
                fragment instanceof FileViewFragment ||
                fragment instanceof ChannelFragment ||
                fragment instanceof EditorsChoiceFragment ||
                fragment instanceof AllContentFragment ||
                fragment instanceof LibraryFragment ||
                fragment instanceof SearchFragment;
        findViewById(R.id.floating_balance_main_container).setVisibility(!canShowFloatingBalance || inFullscreenMode ? View.INVISIBLE : View.VISIBLE);
        if (!(fragment instanceof FileViewFragment) && !(fragment instanceof ShuffleFragment) && !inFullscreenMode && nowPlayingClaim != null) {
            findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
        }
        /*if (!Lbry.SDK_READY && !inFullscreenMode) {
            findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
        }*/
        for (PIPModeListener listener : pipModeListeners) {
            listener.onExitPIPMode();
        }

        View pipPlayerContainer = findViewById(R.id.pip_player_container);
        PlayerView pipPlayer = findViewById(R.id.pip_player);
        pipPlayer.setPlayer(null);
        pipPlayerContainer.setVisibility(View.INVISIBLE);
        playerReassigned = true;
    }

    @Override
    protected void onDestroy() {
        shuttingDown = true;
        unregisterReceivers();
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
        if (receivedStopService || !isServiceRunning(this, LbrynetService.class)) {
            notificationManager.cancelAll();
        }
        if (webSocketClient != null) {
            webSocketClient.close();
        }
        if (dbHelper != null) {
            dbHelper.close();
        }
        if (mediaSession != null && !isBackgroundPlaybackEnabled()) {
            mediaSession.release();
        }
        if (!isBackgroundPlaybackEnabled()) {
            playerNotificationManager.setPlayer(null);
            stopExoplayer();
            nowPlayingClaim = null;
            nowPlayingClaimUrl = null;
            nowPlayingClaimBitmap = null;
        }
        appStarted = false;

        if (!keepSdkBackground()) {
            sendBroadcast(new Intent(LbrynetService.ACTION_STOP_SERVICE));
        }

        super.onDestroy();
    }

    public static void stopExoplayer() {
        if (appPlayer != null) {
            appPlayer.stop(true);
            appPlayer.release();
            appPlayer = null;
        }
        if (playerCache != null) {
            playerCache.release();
            playerCache = null;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        //getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    public void updateWalletBalance() {
        WalletBalanceTask task = new WalletBalanceTask(new WalletBalanceTask.WalletBalanceHandler() {
            @Override
            public void onSuccess(WalletBalance walletBalance) {
                for (WalletBalanceListener listener : walletBalanceListeners) {
                    if (listener != null) {
                        listener.onWalletBalanceUpdated(walletBalance);
                    }
                }
                Lbry.walletBalance = walletBalance;
                updateFloatingWalletBalance();
                updateUsdWalletBalanceInNav();

                sendBroadcast(new Intent(ACTION_WALLET_BALANCE_UPDATED));
            }

            @Override
            public void onError(Exception error) {

            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }



    @SneakyThrows
    private void checkWebSocketClient() {
        if ((webSocketClient == null || webSocketClient.isClosed()) && !Helper.isNullOrEmpty(Lbryio.AUTH_TOKEN)) {
            webSocketClient = new WebSocketClient(new URI(String.format("%s%s", Lbryio.WS_CONNECTION_BASE_URL, Lbryio.AUTH_TOKEN))) {
                @Override
                public void onOpen(ServerHandshake handshakedata) { }

                @Override
                public void onMessage(String message) {
                    loadRemoteNotifications(false);
                }

                @Override
                public void onClose(int code, String reason, boolean remote) {
                    if (!shuttingDown) {
                        // attempt to re-establish the connection if the app isn't being closed
                        checkWebSocketClient();
                    }
                }

                @Override
                public void onError(Exception ex) { }

                protected void onSetSSLParameters(SSLParameters sslParameters) {
                    // don't call setEndpointIdentificationAlgorithm for API level < 24
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
                    }
                }
            };
            webSocketClient.connect();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);

        checkPurchases();
        checkWebSocketClient();
        updateMiniPlayerMargins();
        enteringPIPMode = false;

        applyNavbarSigninPadding();
        checkFirstRun();
        checkNowPlaying();

        if (isFirstRunCompleted()) {
            // check (and start) the LBRY SDK service
            serviceRunning = isServiceRunning(this, LbrynetService.class);
            if (!serviceRunning) {
                Lbry.SDK_READY = false;
                //findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
                ServiceHelper.start(this, "", LbrynetService.class, "lbrynetservice");
            }
        }
        checkSdkReady();
        showSignedInUser();
        checkPendingOpens();

        /*if (Lbry.SDK_READY) {
            findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
        }*/
    }

    public boolean isFirstRunCompleted() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, false);
    }

    public void checkPurchases() {
        if (billingClient != null) {
            Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
            if (result.getPurchasesList() != null) {
                for (Purchase purchase : result.getPurchasesList()) {
                    handlePurchase(purchase);
                }
            }
        }
    }

    public void checkPurchases(GenericTaskHandler handler) {
        boolean purchaseFound = false;
        if (billingClient != null) {
            Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
            if (result.getPurchasesList() != null) {
                for (Purchase purchase : result.getPurchasesList()) {
                    handlePurchase(purchase, handler);
                    purchaseFound = true;
                    return;
                }
            }
        }

        if (!purchaseFound) {
            handler.onError(new Exception(getString(R.string.skip_queue_purchase_not_found)));
        }
    }

    private void handlePurchase(Purchase purchase) {
        handleBillingPurchase(purchase, billingClient, MainActivity.this, null, new RewardVerifiedHandler() {
            @Override
            public void onSuccess(RewardVerified rewardVerified) {
                if (Lbryio.currentUser != null) {
                    Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
                }
            }

            @Override
            public void onError(Exception error) {
                // pass
            }
        });
    }

    private void handlePurchase(Purchase purchase, GenericTaskHandler handler) {
        handleBillingPurchase(purchase, billingClient, MainActivity.this, null, new RewardVerifiedHandler() {
            @Override
            public void onSuccess(RewardVerified rewardVerified) {
                if (Lbryio.currentUser != null) {
                    Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
                }

                if (handler != null) {
                    handler.onSuccess();
                }
            }

            @Override
            public void onError(Exception error) {
                if (handler != null) {
                    handler.onError(error);
                }
            }
        });
    }

    private void checkPendingOpens() {
        if (pendingFollowingReload) {
            loadFollowingContent();
            pendingFollowingReload = false;
        }
        if (!Helper.isNullOrEmpty(pendingAllContentTag)) {
            openAllContentFragmentWithTag(pendingAllContentTag);
            pendingAllContentTag = null;
        } else if (!Helper.isNullOrEmpty(pendingChannelUrl)) {
            openChannelUrl(pendingChannelUrl);
            pendingChannelUrl = null;
        } else if (pendingOpenWalletPage) {
            openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
        } else if (pendingOpenRewardsPage) {
            openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
        }
    }

    @Override
    protected void onPause() {
        PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
        if (!enteringPIPMode && !inPictureInPictureMode && appPlayer != null && !isBackgroundPlaybackEnabled()) {
            appPlayer.setPlayWhenReady(false);
        }
        super.onPause();
    }

    public static void suspendGlobalPlayer(Context context) {
        if (MainActivity.appPlayer != null) {
            MainActivity.appPlayer.setPlayWhenReady(false);
        }
        if (context instanceof MainActivity) {
            ((MainActivity) context).hideGlobalNowPlaying();
        }
    }
    public static void resumeGlobalPlayer(Context context) {
        if (context instanceof MainActivity) {
            ((MainActivity) context).checkNowPlaying();
        }
    }

    private void toggleUrlSuggestions(boolean visible) {
        View container = findViewById(R.id.url_suggestions_container);
        View closeIcon = findViewById(R.id.wunderbar_close);
        //EditText wunderbar = findViewById(R.id.wunderbar);
        //wunderbar.setPadding(0, 0, visible ? getScaledValue(36) : 0, 0);

        container.setVisibility(visible ? View.VISIBLE : View.GONE);
        closeIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
    }

    public int getScaledValue(int value) {
        float scale = getResources().getDisplayMetrics().density;
        return Helper.getScaledValue(value, scale);
    }


    public boolean canShowUrlSuggestions() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_URL_SUGGESTIONS, true);
    }

    public boolean keepSdkBackground() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        return sp.getBoolean(MainActivity.PREFERENCE_KEY_KEEP_SDK_BACKGROUND, true);
    }

    private void setupUriBar() {
        findViewById(R.id.wunderbar_close).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                clearWunderbarFocus(view);
            }
        });

        EditText wunderbar = findViewById(R.id.wunderbar);
        wunderbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View view, boolean hasFocus) {
                if (hasFocus) {
                    hideNotifications();
                    findViewById(R.id.wunderbar_notifications).setVisibility(View.INVISIBLE);

                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.showSoftInput(view, 0);
                } else {
                    findViewById(R.id.wunderbar_notifications).setVisibility(View.VISIBLE);
                }

                if (canShowUrlSuggestions()) {
                    toggleUrlSuggestions(hasFocus);
                    if (hasFocus && Helper.isNullOrEmpty(Helper.getValue(((EditText) view).getText()))) {
                        displayUrlSuggestionsForNoInput();
                    }
                }
            }
        });

        wunderbar.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                if (charSequence != null && canShowUrlSuggestions()) {
                    handleUriInputChanged(charSequence.toString().trim());
                }
            }

            @Override
            public void afterTextChanged(Editable editable) {

            }
        });
        wunderbar.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
                if (actionId == EditorInfo.IME_ACTION_GO) {
                    String input = Helper.getValue(wunderbar.getText());
                    boolean handled = false;
                    if (input.startsWith(LbryUri.PROTO_DEFAULT) && !input.equalsIgnoreCase(LbryUri.PROTO_DEFAULT)) {
                        try {
                            LbryUri uri = LbryUri.parse(input);
                            if (uri.isChannel()) {
                                openChannelUrl(uri.toString());
                                clearWunderbarFocus(wunderbar);
                                handled = true;
                            } else {
                                openFileUrl(uri.toString());
                                clearWunderbarFocus(wunderbar);
                                handled = true;
                            }
                        } catch (LbryUriException ex) {
                            // pass
                        }
                    }
                    if (!handled) {
                        // search
                        launchSearch(input);
                        clearWunderbarFocus(wunderbar);
                    }

                    return true;
                }

                return false;
            }
        });

        urlSuggestionListAdapter = new UrlSuggestionListAdapter(this);
        urlSuggestionListAdapter.setListener(new UrlSuggestionListAdapter.UrlSuggestionClickListener() {
            @Override
            public void onUrlSuggestionClicked(UrlSuggestion urlSuggestion) {
                switch (urlSuggestion.getType()) {
                    case UrlSuggestion.TYPE_CHANNEL:
                        // open channel page
                        if (urlSuggestion.getClaim() != null) {
                            openChannelClaim(urlSuggestion.getClaim());
                        } else {
                            openChannelUrl(urlSuggestion.getUri().toString());
                        }
                        break;
                    case UrlSuggestion.TYPE_FILE:
                        if (urlSuggestion.getClaim() != null) {
                            openFileClaim(urlSuggestion.getClaim());
                        } else {
                            openFileUrl(urlSuggestion.getUri().toString());
                        }
                        break;
                    case UrlSuggestion.TYPE_SEARCH:
                        launchSearch(urlSuggestion.getText());
                        break;
                    case UrlSuggestion.TYPE_TAG:
                        // open tag page
                        openAllContentFragmentWithTag(urlSuggestion.getText());
                        break;
                }
                clearWunderbarFocus(findViewById(R.id.wunderbar));
            }
        });

        RecyclerView urlSuggestionList = findViewById(R.id.url_suggestions);
        LinearLayoutManager llm = new LinearLayoutManager(this);
        urlSuggestionList.setLayoutManager(llm);
        urlSuggestionList.setAdapter(urlSuggestionListAdapter);
    }

    public void clearWunderbarFocus(View view) {
        findViewById(R.id.wunderbar).clearFocus();
        findViewById(R.id.app_bar_main_container).requestFocus();
        InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
    }
    public View getWunderbar() {
        return findViewById(R.id.wunderbar);
    }

    private void launchSearch(String text) {
        Fragment currentFragment = getCurrentFragment();
        if (currentFragment instanceof SearchFragment) {
            ((SearchFragment) currentFragment).search(text, 0);
        } else {
            try {
                SearchFragment fragment = SearchFragment.class.newInstance();
                fragment.setCurrentQuery(text);
                openFragment(fragment, true);
            } catch (Exception ex) {
                // pass
            }
        }
    }

    private void resolveUrlSuggestions(List<String> urls) {
        ResolveTask task = new ResolveTask(urls, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimListResultHandler() {
            @Override
            public void onSuccess(List<Claim> claims) {
                if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) {
                    for (int i = 0; i < claims.size(); i++) {
                        // build a simple url from the claim for matching
                        Claim claim = claims.get(i);
                        Claim actualClaim = claim;
                        boolean isRepost = false;
                        if (Claim.TYPE_REPOST.equalsIgnoreCase(claim.getValueType())) {
                            actualClaim = claim.getRepostedClaim();
                            isRepost = true;
                        }
                        if (Helper.isNullOrEmpty(claim.getName())) {
                            continue;
                        }

                        LbryUri simpleUrl = new LbryUri();
                        if (actualClaim.getName().startsWith("@") && !isRepost) {
                            // channel
                            simpleUrl.setChannelName(actualClaim.getName());
                        } else {
                            simpleUrl.setStreamName(claim.getName());
                        }

                        urlSuggestionListAdapter.setClaimForUrl(simpleUrl, actualClaim);
                    }
                    urlSuggestionListAdapter.notifyDataSetChanged();
                }
            }

            @Override
            public void onError(Exception error) {

            }
        });
        task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
    }

    private void displayUrlSuggestionsForNoInput() {
        urlSuggestionListAdapter.clear();
        loadDefaultSuggestionsForBlankUrl();
    }

    private void handleUriInputChanged(String text) {
        // build the default suggestions
        urlSuggestionListAdapter.clear();
        if (Helper.isNullOrEmpty(text) || text.trim().equals("@")) {
            displayUrlSuggestionsForNoInput();
            return;
        }

        List<UrlSuggestion> defaultSuggestions = buildDefaultSuggestions(text);
        urlSuggestionListAdapter.addUrlSuggestions(defaultSuggestions);
        if (LbryUri.PROTO_DEFAULT.equalsIgnoreCase(text)) {
            return;
        }

        LighthouseAutoCompleteTask task = new LighthouseAutoCompleteTask(text, null, new LighthouseAutoCompleteTask.AutoCompleteResultHandler() {
            @Override
            public void onSuccess(List<UrlSuggestion> suggestions) {
                String wunderBarText = Helper.getValue(((EditText) findViewById(R.id.wunderbar)).getText());
                if (wunderBarText.equalsIgnoreCase(text)) {
                    urlSuggestionListAdapter.addUrlSuggestions(suggestions);
                    List<String> urls = urlSuggestionListAdapter.getItemUrls();
                    resolveUrlSuggestions(urls);
                }
            }

            @Override
            public void onError(Exception error) {

            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void loadDefaultSuggestionsForBlankUrl() {
        if (recentUrlHistory != null && recentUrlHistory.size() > 0) {
            urlSuggestionListAdapter.addUrlSuggestions(recentUrlHistory);
        }

        FetchRecentUrlHistoryTask task = new FetchRecentUrlHistoryTask(DatabaseHelper.getInstance(), new FetchRecentUrlHistoryTask.FetchRecentUrlHistoryHandler() {
            @Override
            public void onSuccess(List<UrlSuggestion> recentHistory) {
                List<UrlSuggestion> suggestions = new ArrayList<>(recentHistory);
                List<UrlSuggestion> lbrySuggestions = buildLbryUrlSuggestions();
                if (suggestions.size() < 10) {
                    for (int i = suggestions.size(), j = 0; i < 10 && j < lbrySuggestions.size(); i++, j++) {
                        suggestions.add(lbrySuggestions.get(j));
                    }
                } else if (suggestions.size() == 0) {
                    suggestions.addAll(lbrySuggestions);
                }

                for (UrlSuggestion suggestion : suggestions) {
                    suggestion.setUseTextAsDescription(true);
                }

                recentUrlHistory = new ArrayList<>(suggestions);
                urlSuggestionListAdapter.clear();
                urlSuggestionListAdapter.addUrlSuggestions(recentUrlHistory);
                List<String> urls = urlSuggestionListAdapter.getItemUrls();
                resolveUrlSuggestions(urls);
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private List<UrlSuggestion> buildLbryUrlSuggestions() {
        List<UrlSuggestion> suggestions = new ArrayList<>();
        suggestions.add(new UrlSuggestion(
                UrlSuggestion.TYPE_FILE, "What is LBRY?", LbryUri.tryParse("lbry://what#19b9c243bea0c45175e6a6027911abbad53e983e")));
        suggestions.add(new UrlSuggestion(
                UrlSuggestion.TYPE_CHANNEL, "LBRYCast", LbryUri.tryParse("lbry://@lbrycast#4c29f8b013adea4d5cca1861fb2161d5089613ea")));
        suggestions.add(new UrlSuggestion(
                UrlSuggestion.TYPE_CHANNEL, "The LBRY Channel", LbryUri.tryParse("lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a")));
        for (UrlSuggestion suggestion : suggestions) {
            suggestion.setUseTextAsDescription(true);
        }
        return suggestions;
    }

    private List<UrlSuggestion> buildDefaultSuggestions(String text) {
        List<UrlSuggestion> suggestions = new ArrayList<UrlSuggestion>();

        if (LbryUri.PROTO_DEFAULT.equalsIgnoreCase(text)) {
            loadDefaultSuggestionsForBlankUrl();
            return recentUrlHistory != null ? recentUrlHistory : new ArrayList<>();
        }

        // First item is always search
        if (!text.startsWith(LbryUri.PROTO_DEFAULT)) {
            UrlSuggestion searchSuggestion = new UrlSuggestion(UrlSuggestion.TYPE_SEARCH, text);
            suggestions.add(searchSuggestion);
        }

        if (!text.matches(LbryUri.REGEX_INVALID_URI)) {
            boolean isUrlWithScheme = text.startsWith(LbryUri.PROTO_DEFAULT);
            boolean isChannel = text.startsWith("@");
            LbryUri uri = null;
            if (isUrlWithScheme && text.length() > 7) {
                try {
                    uri = LbryUri.parse(text);
                    isChannel = uri.isChannel();
                } catch (LbryUriException ex) {
                    // pass
                }
            }

            if (!isChannel) {
                if (uri == null) {
                    uri = new LbryUri();
                    uri.setStreamName(text);
                }
                UrlSuggestion fileSuggestion = new UrlSuggestion(UrlSuggestion.TYPE_FILE, text);
                fileSuggestion.setUri(uri);
                suggestions.add(fileSuggestion);
            }

            if (text.indexOf(' ') == -1) {
                // channels should not contain spaces
                if (isChannel) {
                    if (uri == null) {
                        uri = new LbryUri();
                        uri.setChannelName(text);
                    }
                    UrlSuggestion suggestion = new UrlSuggestion(UrlSuggestion.TYPE_CHANNEL, text);
                    suggestion.setUri(uri);
                    suggestions.add(suggestion);
                }
            }

            if (!isUrlWithScheme && !isChannel) {
                UrlSuggestion suggestion = new UrlSuggestion(UrlSuggestion.TYPE_TAG, text);
                suggestions.add(suggestion);
            }
        }

        return suggestions;
    }

    public void checkNowPlaying() {
        Fragment fragment = getCurrentFragment();
        if (fragment instanceof FileViewFragment) {
            return;
        }

        if (nowPlayingClaim != null) {
            findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
            ((TextView) findViewById(R.id.global_now_playing_title)).setText(nowPlayingClaim.getTitle());
            ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(nowPlayingClaim.getPublisherTitle());
        }
        if (appPlayer != null) {
            PlayerView playerView = findViewById(R.id.global_now_playing_player_view);
            playerView.setPlayer(null);
            playerView.setPlayer(appPlayer);
            playerView.setUseController(false);
            playerReassigned = true;

            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
    }

    public void hideGlobalNowPlaying() {
        findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
    }

    public void unsetFitsSystemWindows(View view) {
        view.setFitsSystemWindows(false);
    }

    public void enterFullScreenMode() {
        inFullscreenMode = true;
        hideFloatingWalletBalance();
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }
        //findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
        findViewById(R.id.app_bar_main_container).setFitsSystemWindows(false);
        lockDrawer();

        View decorView = getWindow().getDecorView();
        decorView.setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
                View.SYSTEM_UI_FLAG_FULLSCREEN |
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    }

    public int getStatusBarHeight() {
        int height = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            height = getResources().getDimensionPixelSize(resourceId);
        }
        return height;
    }

    public void exitFullScreenMode() {
        View appBarMainContainer = findViewById(R.id.app_bar_main_container);
        View decorView = getWindow().getDecorView();
        int flags = isDarkMode() ? (View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE) :
                (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE);
        appBarMainContainer.setFitsSystemWindows(true);
        decorView.setSystemUiVisibility(flags);

        /*if (!Lbry.SDK_READY) {
            findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
        }*/
        showFloatingWalletBalance();
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.show();
        }
        unlockDrawer();
        inFullscreenMode = false;
    }

    private void initKeyStore() {
        try {
            Lbry.KEYSTORE = Utils.initKeyStore(this);
        } catch (Exception ex) {
            // This shouldn't happen, but in case it does.
            Toast.makeText(this, "The keystore could not be initialized. The app requires a secure keystore to run properly.", Toast.LENGTH_LONG).show();
            finish();
        }
    }

    private void checkFirstRun() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean firstRunCompleted = sp.getBoolean(PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, false);
        if (!firstRunCompleted) {
            findViewById(R.id.drawer_layout).setVisibility(View.INVISIBLE);
            startActivity(new Intent(this, FirstRunActivity.class));
            return;
        }

        if (!appStarted) {
            // first run completed, startup
            startup();
            return;
        }

        fetchRewards();
        if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
            openFragment(FollowingFragment.class, false, NavMenuItem.ID_ITEM_FOLLOWING);
        }
    }

    public static boolean isServiceRunning(Context context, Class<?> serviceClass) {
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
                return true;
            }
        }

        return false;
    }

    private void loadAuthToken() {
        // Check if an auth token is present and then set it for Lbryio
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        String encryptedAuthToken = sp.getString(PREFERENCE_KEY_AUTH_TOKEN, null);
        if (!Helper.isNullOrEmpty(encryptedAuthToken)) {
            try {
                Lbryio.AUTH_TOKEN = new String(Utils.decrypt(
                        Base64.decode(encryptedAuthToken, Base64.NO_WRAP), this, Lbry.KEYSTORE), StandardCharsets.UTF_8);
            } catch (Exception ex) {
                // pass. A new auth token would have to be generated if the old one cannot be decrypted
                Log.e(TAG, "Could not decrypt existing auth token.", ex);
            }
        }
    }

    private void checkSdkReady() {
        if (!Lbry.SDK_READY) {
            new Handler().postDelayed(() -> {
                if (checkSdkReadyTask != null && checkSdkReadyTask.getStatus() != AsyncTask.Status.FINISHED) {
                    // task already running
                    return;
                }
                checkSdkReadyTask = new CheckSdkReadyTask(MainActivity.this, sdkStatusListeners);
                checkSdkReadyTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }, CHECK_SDK_READY_INTERVAL);
        } else {
            scheduleWalletBalanceUpdate();
            scheduleWalletSyncTask();
            initFloatingWalletBalance();
        }
    }

    public void onSdkReady() {
        if (Lbryio.isSignedIn()) {
            checkSyncedWallet();
        }

        //findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
        checkAndEnableShareUsageData();

        scheduleWalletBalanceUpdate();
        scheduleWalletSyncTask();
        fetchOwnChannels();
        fetchOwnClaims();

        initFloatingWalletBalance();

        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean firstRunCompleted = sp.getBoolean(PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, false);
        if (firstRunCompleted) {
            checkAndClaimNewAndroidReward();
        }
    }

    public void checkAndClaimNewAndroidReward() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean rewardClaimed = sp.getBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, false);
        if (!rewardClaimed) {
            ClaimRewardTask task = new ClaimRewardTask(
                    Reward.TYPE_NEW_ANDROID,
                    null,
                    null,
                    this,
                    new ClaimRewardTask.ClaimRewardHandler() {
                @Override
                public void onSuccess(double amountClaimed, String message) {
                    if (Helper.isNullOrEmpty(message)) {
                        message = getResources().getQuantityString(
                                R.plurals.claim_reward_message,
                                amountClaimed == 1 ? 1 : 2,
                                new DecimalFormat(Helper.LBC_CURRENCY_FORMAT_PATTERN).format(amountClaimed));
                    }
                    Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).show();
                    sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, true).apply();
                }

                @Override
                public void onError(Exception error) {
                    // pass. fail silently
                    sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, true).apply();
                }
            });
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

    public void initMediaSession() {
        ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
        mediaSession = new MediaSessionCompat(getApplicationContext(), "LBRYMediaSession", mediaButtonReceiver, null);
        MediaSessionConnector connector = new MediaSessionConnector(mediaSession);
        connector.setPlayer(MainActivity.appPlayer);
        mediaSession.setActive(true);
    }

    public void initNotificationsPage() {
        findViewById(R.id.notification_list_empty_container).setVisibility(View.VISIBLE);

        RecyclerView notificationsList = findViewById(R.id.notifications_list);
        LinearLayoutManager llm = new LinearLayoutManager(this);
        notificationsList.setLayoutManager(llm);
    }

    public void initPlaybackNotification() {
        if (isBackgroundPlaybackEnabled()) {
            playerNotificationManager.setPlayer(MainActivity.appPlayer);
            if (mediaSession != null) {
                playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
            }
        }
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (PREFERENCE_KEY_MINI_PLAYER_BOTTOM_MARGIN.equalsIgnoreCase(key)) {
            updateMiniPlayerMargins();
        }
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        this.actionMode = mode;
        if (isDarkMode()) {
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
        }

        actionMode.getMenuInflater().inflate(R.menu.menu_notification, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return true;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        if (R.id.action_delete == item.getItemId()) {
            if (notificationListAdapter != null && notificationListAdapter.getSelectedCount() > 0) {

                final List<LbryNotification> selectedNotifications = new ArrayList<>(notificationListAdapter.getSelectedItems());
                String message = getResources().getQuantityString(R.plurals.confirm_delete_notifications, selectedNotifications.size());
                AlertDialog.Builder builder = new AlertDialog.Builder(this).
                        setTitle(R.string.delete_selection).
                        setMessage(message)
                        .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                handleDeleteSelectedNotifications(selectedNotifications);
                            }
                        }).setNegativeButton(R.string.no, null);
                builder.show();
                return true;
            }
        }
        return false;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        if (notificationListAdapter != null) {
            notificationListAdapter.clearSelectedItems();
            notificationListAdapter.setInSelectionMode(false);
            notificationListAdapter.notifyDataSetChanged();
        }
        if (Build.VERSION.SDK_INT >= M && isDarkMode()) {
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
        this.actionMode = null;
    }

    @Override
    public void onEnterSelectionMode() {
        startSupportActionMode(this);
    }

    @Override
    public void onExitSelectionMode() {
        if (actionMode != null) {
            actionMode.finish();
        }
    }

    @Override
    public void onItemSelectionToggled() {
        if (actionMode != null) {
            actionMode.setTitle(notificationListAdapter != null ? String.valueOf(notificationListAdapter.getSelectedCount()) : "");
            actionMode.invalidate();
        }
    }

    private void handleDeleteSelectedNotifications(List<LbryNotification> notifications) {
        List<Long> remoteIds = new ArrayList<>();
        for (LbryNotification notification : notifications) {
            remoteIds.add(notification.getRemoteId());
        }
        (new AsyncTask<Void, Void, Void>() {
            protected Void doInBackground(Void... params) {
                try {
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
                    DatabaseHelper.deleteNotifications(notifications, db);
                } catch (Exception ex) {
                    // pass
                }
                return null;
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        new NotificationDeleteTask(remoteIds).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        if (notificationListAdapter != null) {
            notificationListAdapter.removeNotifications(notifications);
        }
        if (actionMode != null) {
            actionMode.finish();
        }
    }

    private class PlayerNotificationDescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter {

        @Override
        public CharSequence getCurrentContentTitle(Player player) {
            return nowPlayingClaim != null ? nowPlayingClaim.getTitle() : "";
        }

        @Nullable
        @Override
        public PendingIntent createCurrentContentIntent(Player player) {
            if (nowPlayingClaimUrl != null) {
                Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(nowPlayingClaimUrl));
                launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                return PendingIntent.getActivity(MainActivity.this, 0, launchIntent, 0);
            }
            return null;
        }

        @Nullable
        @Override
        public CharSequence getCurrentContentText(Player player) {
            return nowPlayingClaim != null && nowPlayingClaim.getSigningChannel() != null ? nowPlayingClaim.getSigningChannel().getTitleOrName() : null;
        }

        @Nullable
        @Override
        public Bitmap getCurrentLargeIcon(Player player, PlayerNotificationManager.BitmapCallback callback) {
            if (nowPlayingClaimBitmap == null &&
                    nowPlayingClaim != null &&
                    !Helper.isNullOrEmpty(nowPlayingClaim.getThumbnailUrl())) {
                Glide.with(getApplicationContext()).asBitmap().load(nowPlayingClaim.getThumbnailUrl()).into(new CustomTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                        nowPlayingClaimBitmap = resource;
                        callback.onBitmap(resource);
                    }

                    @Override
                    public void onLoadCleared(@Nullable Drawable placeholder) {

                    }
                });
            }
            return nowPlayingClaimBitmap;
        }
    }

    public void showFloatingWalletBalance() {
        findViewById(R.id.floating_balance_main_container).setVisibility(View.VISIBLE);
    }
    public void hideFloatingWalletBalance() {
        findViewById(R.id.floating_balance_main_container).setVisibility(View.GONE);
    }
    public void hideFloatingRewardsValue() {
        findViewById(R.id.floating_reward_container).setVisibility(View.INVISIBLE);
    }

    private void initFloatingWalletBalance() {
        findViewById(R.id.floating_balance_container).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
            }
        });
        findViewById(R.id.floating_reward_container).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
            }
        });
    }

    private void updateUsdWalletBalanceInNav() {
        double usdBalance = Lbry.walletBalance.getAvailable().doubleValue() * Lbryio.LBCUSDRate;
        if (navMenuAdapter != null) {
            navMenuAdapter.setExtraLabelForItem(
                    NavMenuItem.ID_ITEM_WALLET,
                    Lbryio.LBCUSDRate > 0 ? String.format("$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(usdBalance)) : null
            );
        }
    }

    private void updateFloatingWalletBalance() {
        if (!hasLoadedFirstBalance) {
            findViewById(R.id.floating_balance_loading).setVisibility(View.GONE);
            findViewById(R.id.floating_balance_value).setVisibility(View.VISIBLE);
            hasLoadedFirstBalance = true;
        }

        ((TextView) findViewById(R.id.floating_balance_value)).setText(Helper.shortCurrencyFormat(
                Lbry.walletBalance == null ? 0 : Lbry.walletBalance.getAvailable().doubleValue()));
    }

    private void scheduleWalletBalanceUpdate() {
        if (scheduler != null && !walletBalanceUpdateScheduled) {
            scheduler.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    updateWalletBalance();
                }
            }, 0, 5, TimeUnit.SECONDS);
            walletBalanceUpdateScheduled = true;
        }
    }

    private void scheduleWalletSyncTask() {
        if (scheduler != null && !walletSyncScheduled) {
            scheduler.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    syncWalletAndLoadPreferences();
                }
            }, 0, 5, TimeUnit.MINUTES);
            walletSyncScheduled = true;
        }
    }

    public void saveSharedUserState() {
        if (!userSyncEnabled()) {
            return;
        }
        SaveSharedUserStateTask saveTask = new SaveSharedUserStateTask(new SaveSharedUserStateTask.SaveSharedUserStateHandler() {
            @Override
            public void onSuccess() {
                // push wallet sync changes
                pushCurrentWalletSync();
            }

            @Override
            public void onError(Exception error) {
                // pass
            }
        });
        saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void loadSharedUserState() {
        // load wallet preferences
        LoadSharedUserStateTask loadTask = new LoadSharedUserStateTask(MainActivity.this, new LoadSharedUserStateTask.LoadSharedUserStateHandler() {
            @Override
            public void onSuccess(List<Subscription> subscriptions, List<Tag> followedTags) {
                if (subscriptions != null && subscriptions.size() > 0) {
                    // reload subscriptions if wallet fragment is FollowingFragment
                    //openNavFragments.get
                    MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
                            subscriptions,
                            initialSubscriptionMergeDone(),
                            MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
                        @Override
                        public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) {
                            Lbryio.subscriptions = new ArrayList<>(subscriptions);
                            if (diff != null && diff.size() > 0) {
                                saveSharedUserState();
                            }

                            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                            sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, true).apply();
                            Lbryio.cacheResolvedSubscriptions.clear();

                            for (Fragment fragment : openNavFragments.values()) {
                                if (fragment instanceof FollowingFragment) {
                                    // reload local subscriptions
                                    FollowingFragment followingFragment = (FollowingFragment) fragment;
                                    followingFragment.fetchLoadedSubscriptions(true);
                                }
                            }
                        }

                        @Override
                        public void onError(Exception error) {
                            Log.e(TAG, String.format("merge subscriptions failed: %s", error.getMessage()), error);
                        }
                    });
                    mergeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                }

                if (followedTags != null && followedTags.size() > 0) {
                    List<Tag> previousTags = new ArrayList<>(Lbry.followedTags);
                    Lbry.followedTags = new ArrayList<>(followedTags);
                    for (Fragment fragment : openNavFragments.values()) {
                        if (fragment instanceof AllContentFragment) {
                            AllContentFragment acFragment = (AllContentFragment) fragment;
                            if (!acFragment.isSingleTagView() &&
                                    acFragment.getCurrentContentScope() == ContentScopeDialogFragment.ITEM_TAGS &&
                                    !previousTags.equals(followedTags)) {
                                acFragment.fetchClaimSearchContent(true);
                            }
                        }
                    }
                }
            }

            @Override
            public void onError(Exception error) {
                Log.e(TAG, String.format("load shared user state failed: %s", error != null ? error.getMessage() : "no error message"), error);
            }
        });
        loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void pushCurrentWalletSync() {
        String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
        SyncApplyTask fetchTask = new SyncApplyTask(true, password, new DefaultSyncTaskHandler() {
            @Override
            public void onSyncApplySuccess(String hash, String data) {
                SyncSetTask setTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, null);
                setTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
            @Override
            public void onSyncApplyError(Exception error) { }
        });
        fetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private boolean userSyncEnabled() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean walletSyncEnabled = sp.getBoolean(PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, false);
        return walletSyncEnabled && Lbryio.isSignedIn();
    }

    public void syncSet(String hash, String data) {
        if (syncSetTask == null || syncSetTask.getStatus() == AsyncTask.Status.FINISHED) {
            syncSetTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, new DefaultSyncTaskHandler() {
                @Override
                public void onSyncSetSuccess(String hash) {
                    Lbryio.lastRemoteHash = hash;
                    Lbryio.lastWalletSync = new WalletSync(hash, data);

                    if (pendingSyncSetQueue.size() > 0) {
                        fullSyncInProgress = true;
                        WalletSync nextSync = pendingSyncSetQueue.remove(0);
                        syncSet(nextSync.getHash(), nextSync.getData());
                    } else if (queuedSyncCount > 0) {
                        queuedSyncCount--;
                        syncApplyAndSet();
                    }

                    fullSyncInProgress = false;
                }
                @Override
                public void onSyncSetError(Exception error) {
                    // log app exceptions
                    if (pendingSyncSetQueue.size() > 0) {
                        WalletSync nextSync = pendingSyncSetQueue.remove(0);
                        syncSet(nextSync.getHash(), nextSync.getData());
                    } else if (queuedSyncCount > 0) {
                        queuedSyncCount--;
                        syncApplyAndSet();
                    }

                    fullSyncInProgress = false;
                }
            });
            syncSetTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            WalletSync pending = new WalletSync(hash, data);
            pendingSyncSetQueue.add(pending);
        }
    }

    public void syncApplyAndSet() {
        fullSyncInProgress = true;
        String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
        SyncApplyTask fetchTask = new SyncApplyTask(true, password, new DefaultSyncTaskHandler() {
            @Override
            public void onSyncApplySuccess(String hash, String data) {
                if (!hash.equalsIgnoreCase(Lbryio.lastRemoteHash)) {
                    syncSet(hash, data);
                } else {
                    fullSyncInProgress = false;
                    queuedSyncCount = 0;
                }
            }
            @Override
            public void onSyncApplyError(Exception error) {
                fullSyncInProgress = false;
                if (queuedSyncCount > 0) {
                    queuedSyncCount--;
                    syncApplyAndSet();
                }
            }
        });
        fetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void syncWalletAndLoadPreferences() {
        if (!userSyncEnabled()) {
            return;
        }
        if (fullSyncInProgress) {
            queuedSyncCount++;
        }

        fullSyncInProgress = true;
        String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
        SyncGetTask task = new SyncGetTask(password, true, null, new DefaultSyncTaskHandler() {
            @Override
            public void onSyncGetSuccess(WalletSync walletSync) {
                Lbryio.lastWalletSync = walletSync;
                Lbryio.lastRemoteHash = walletSync.getHash();
            }

            @Override
            public void onSyncGetWalletNotFound() {
                // pass. This actually shouldn't happen at this point.
                // But if it does, send what we have
                if (Lbryio.isSignedIn() && userSyncEnabled()) {
                    syncApplyAndSet();
                }
            }

            @Override
            public void onSyncGetError(Exception error) {
                // pass
                Log.e(TAG, String.format("sync get failed: %s", error != null ? error.getMessage() : "no error message"), error);

                fullSyncInProgress = false;
                if (queuedSyncCount > 0) {
                    queuedSyncCount--;
                    syncApplyAndSet();
                }
            }

            @Override
            public void onSyncApplySuccess(String hash, String data) {
                if (!hash.equalsIgnoreCase(Lbryio.lastRemoteHash)) {
                    syncSet(hash, data);
                } else {
                    fullSyncInProgress = false;
                    queuedSyncCount = 0;
                }

                loadSharedUserState();
            }

            @Override
            public void onSyncApplyError(Exception error) {
                // pass
                Log.e(TAG, String.format("sync apply failed: %s", error != null ? error.getMessage() : "no error message"), error);
                fullSyncInProgress = false;
                if (queuedSyncCount > 0) {
                    queuedSyncCount--;
                    syncApplyAndSet();
                }
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void registerRequestsReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_AUTH_TOKEN_GENERATED);
        intentFilter.addAction(ACTION_USER_SIGN_IN_SUCCESS);
        intentFilter.addAction(ACTION_OPEN_ALL_CONTENT_TAG);
        intentFilter.addAction(ACTION_OPEN_CHANNEL_URL);
        intentFilter.addAction(ACTION_OPEN_WALLET_PAGE);
        intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE);
        intentFilter.addAction(ACTION_PUBLISH_SUCCESSFUL);
        intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE);
        intentFilter.addAction(LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED);
        requestsReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (ACTION_AUTH_TOKEN_GENERATED.equalsIgnoreCase(action)) {
                    handleAuthTokenGenerated(intent);
                } else if (ACTION_USER_SIGN_IN_SUCCESS.equalsIgnoreCase(action)) {
                    handleUserSignInSuccess(intent);
                } else if (ACTION_OPEN_ALL_CONTENT_TAG.equalsIgnoreCase(action)) {
                    handleOpenContentTag(intent);
                } else if (ACTION_OPEN_CHANNEL_URL.equalsIgnoreCase(action)) {
                    handleOpenChannelUrl(intent);
                } else if (ACTION_OPEN_WALLET_PAGE.equalsIgnoreCase(action)) {
                    pendingOpenWalletPage = true;
                } else if (ACTION_OPEN_REWARDS_PAGE.equalsIgnoreCase(action)) {
                    pendingOpenRewardsPage = true;
                } else if (ACTION_SAVE_SHARED_USER_STATE.equalsIgnoreCase(action)) {
                    saveSharedUserState();
                } else if (ACTION_PUBLISH_SUCCESSFUL.equalsIgnoreCase(action)) {
                    openPublishesOnSuccessfulPublish();
                } else if (LbrynetMessagingService.ACTION_NOTIFICATION_RECEIVED.equalsIgnoreCase(action)) {
                    handleNotificationReceived(intent);
                }
            }

            private void handleNotificationReceived(Intent intent) {
                loadRemoteNotifications(false);
            }

            private void handleAuthTokenGenerated(Intent intent) {
                // store the value
                String encryptedAuthToken = intent.getStringExtra("authToken");
                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                sp.edit().putString(PREFERENCE_KEY_AUTH_TOKEN, encryptedAuthToken).apply();
            }

            private void handleOpenContentTag(Intent intent) {
                String tag = intent.getStringExtra("tag");
                if (!Helper.isNullOrEmpty(tag)) {
                    pendingAllContentTag = tag;
                }
            }
            private void handleUserSignInSuccess(Intent intent) {
                pendingFollowingReload = true;
            }
            private void handleOpenChannelUrl(Intent intent) {
                pendingChannelUrl = intent.getStringExtra("url");
            }
        };
        registerReceiver(requestsReceiver, intentFilter);
    }

    private void loadFollowingContent() {
        for (Fragment fragment : openNavFragments.values()) {
            if (fragment instanceof FollowingFragment) {
                ((FollowingFragment) fragment).loadFollowing();
            }
        }
    }
    public void showMessage(int stringResourceId) {
        Snackbar.make(findViewById(R.id.content_main), stringResourceId, Snackbar.LENGTH_LONG).show();
    }
    public void showMessage(String message) {
        Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).show();
    }
    public void showError(String message) {
        Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).
                setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
    }

    public void showNotifications() {
        clearWunderbarFocus(findViewById(R.id.wunderbar));
        findViewById(R.id.notifications_container).setVisibility(View.VISIBLE);
        ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.lbryGreen));
        if (remoteNotifcationsLastLoaded == null ||
                (System.currentTimeMillis() - remoteNotifcationsLastLoaded.getTime() > REMOTE_NOTIFICATION_REFRESH_TTL)) {
            loadRemoteNotifications(true);
        }

        if (notificationListAdapter != null) {
            markNotificationsSeen();
        }
    }

    public void hideNotifications() {
        ((ImageView) findViewById(R.id.notifications_toggle_icon)).setColorFilter(ContextCompat.getColor(this, R.color.actionBarForeground));
        findViewById(R.id.notifications_container).setVisibility(View.GONE);
    }

    private void markNotificationsSeen() {
        List<LbryNotification> all = notificationListAdapter != null ? notificationListAdapter.getItems() : null;
        if (all != null) {
            List<Long> unseenIds = new ArrayList<>();
            for (LbryNotification notification : all) {
                if (!notification.isSeen() && notification.getRemoteId() > 0) {
                    unseenIds.add(notification.getRemoteId());
                }
            }
            if (unseenIds.size() > 0) {
                NotificationUpdateTask task = new NotificationUpdateTask(unseenIds, true);
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        }


        (new AsyncTask<Void, Void, Void>() {
            protected Void doInBackground(Void... params) {
                try {
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
                    DatabaseHelper.markNotificationsSeen(db);
                } catch (Exception ex) {
                    // pass
                }
                return null;
            }
            protected void onPostExecute(Void result) {
                loadUnseenNotificationsCount();
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void navigateBackToNotifications() {
        showNotifications();
    }

    @Override
    public void onBackPressed() {
        if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) {
            clearWunderbarFocus(findViewById(R.id.wunderbar));
            return;
        }
        if (findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) {
            hideNotifications();
            return;
        }

        if (backPressInterceptor != null && backPressInterceptor.onBackPressed()) {
            return;
        }

        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START);
        } else {
            boolean handled = false;
            // TODO: Refactor both forms as back press interceptors?
            ChannelFormFragment channelFormFragment = null;
            PublishFormFragment publishFormFragment = null;
            for (Fragment fragment : openNavFragments.values()) {
                if (fragment instanceof ChannelFormFragment) {
                    channelFormFragment = ((ChannelFormFragment) fragment);
                    break;
                }
                if (fragment instanceof PublishFormFragment) {
                    publishFormFragment = ((PublishFormFragment) fragment);
                    break;
                }
            }
            if (channelFormFragment != null && channelFormFragment.isSaveInProgress()) {
                handled = true;
                return;
            }
            if (publishFormFragment != null && (publishFormFragment.isSaveInProgress() || publishFormFragment.isTranscodeInProgress())) {
                if (publishFormFragment.isTranscodeInProgress()) {
                    showMessage(R.string.transcode_in_progress);
                }
                handled = true;
                return;
            }

            if (!handled) {
                // check fragment and nav history
                FragmentManager manager = getSupportFragmentManager();
                int backCount = getSupportFragmentManager().getBackStackEntryCount();
                if (backCount > 0) {
                    // we can pop the stack
                    manager.popBackStack();
                    setSelectedNavMenuItemForFragment(getCurrentFragment());
                } else if (!enterPIPMode()) {
                    // we're at the top of the stack
                    moveTaskToBack(true);
                    return;
                }
            }
        }
    }

    public void simpleSignIn() {
        Intent intent = new Intent(this, VerificationActivity.class);
        intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_SIGN_IN);
        startActivityForResult(intent, REQUEST_SIMPLE_SIGN_IN);
    }

    public void walletSyncSignIn() {
        Intent intent = new Intent(this, VerificationActivity.class);
        intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_WALLET);
        startActivityForResult(intent, REQUEST_WALLET_SYNC_SIGN_IN);
    }

    public void rewardsSignIn() {
        Intent intent = new Intent(this, VerificationActivity.class);
        intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_REWARDS);
        startActivityForResult(intent, REQUEST_REWARDS_VERIFY_SIGN_IN);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_STORAGE_PERMISSION:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    for (StoragePermissionListener listener : storagePermissionListeners) {
                        listener.onStoragePermissionGranted();
                    }
                } else {
                    for (StoragePermissionListener listener : storagePermissionListeners) {
                        listener.onStoragePermissionRefused();
                    }
                }
                startingPermissionRequest = false;
                break;

            case REQUEST_CAMERA_PERMISSION:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    for (CameraPermissionListener listener : cameraPermissionListeners) {
                        listener.onCameraPermissionGranted();
                    }
                } else {
                    for (CameraPermissionListener listener : cameraPermissionListeners) {
                        listener.onCameraPermissionRefused();
                    }
                }
                startingPermissionRequest = false;
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_FILE_PICKER) {
            startingFilePickerActivity = false;
            if (resultCode == RESULT_OK && data != null) {
                Uri fileUri = data.getData();
                String filePath = Helper.getRealPathFromURI_API19(this, fileUri);
                for (FilePickerListener listener : filePickerListeners) {
                    listener.onFilePicked(filePath);
                }
            } else {
                for (FilePickerListener listener : filePickerListeners) {
                    listener.onFilePickerCancelled();
                }
            }
        } else if (requestCode == REQUEST_SIMPLE_SIGN_IN || requestCode == REQUEST_WALLET_SYNC_SIGN_IN) {
            if (resultCode == RESULT_OK) {
                // user signed in
                showSignedInUser();

                if (requestCode == REQUEST_WALLET_SYNC_SIGN_IN) {
                    SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
                    sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, true).apply();

                    for (Fragment fragment : openNavFragments.values()) {
                        if (fragment instanceof WalletFragment) {
                            ((WalletFragment) fragment).onWalletSyncEnabled();
                        }
                    }
                    scheduleWalletSyncTask();
                }
            }
        } else if (requestCode == REQUEST_VIDEO_CAPTURE || requestCode == REQUEST_TAKE_PHOTO) {
            if (resultCode == RESULT_OK) {
                PublishFragment publishFragment = null;
                for (Fragment fragment : openNavFragments.values()) {
                    if (fragment instanceof PublishFragment) {
                        publishFragment = (PublishFragment) fragment;
                        break;
                    }
                }

                Map<String, Object> params = new HashMap<>();
                params.put("directFilePath", cameraOutputFilename);
                if (publishFragment != null) {
                    params.put("suggestedUrl", publishFragment.getSuggestedPublishUrl());
                }
                openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
            }
            cameraOutputFilename = null;
        }
    }

    public void requestVideoCapture() {
        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        if (intent.resolveActivity(getPackageManager()) != null) {
            String outputPath = String.format("%s/record", Utils.getAppInternalStorageDir(this));
            File dir = new File(outputPath);
            if (!dir.isDirectory()) {
                dir.mkdirs();
            }

            cameraOutputFilename = String.format("%s/VID_%s.mp4", outputPath, Helper.FILESTAMP_FORMAT.format(new Date()));
            Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename));
            intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
            startActivityForResult(intent, REQUEST_VIDEO_CAPTURE);
            return;
        }

        showError(getString(R.string.cannot_capture_video));
    }

    public void requestTakePhoto() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(getPackageManager()) != null) {
            String outputPath = String.format("%s/photos", Utils.getAppInternalStorageDir(this));
            File dir = new File(outputPath);
            if (!dir.isDirectory()) {
                dir.mkdirs();
            }

            cameraOutputFilename = String.format("%s/IMG_%s.jpg", outputPath, Helper.FILESTAMP_FORMAT.format(new Date()));
            Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename));
            intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
            startActivityForResult(intent, REQUEST_TAKE_PHOTO);
            return;
        }

        showError(getString(R.string.cannot_take_photo));
    }



    private void applyNavbarSigninPadding() {
        int statusBarHeight = getStatusBarHeight();

        View signInButton = findViewById(R.id.sign_in_button_container);
        View signedInEmailContainer = findViewById(R.id.signed_in_email_container);
        signInButton.setPadding(0, statusBarHeight, 0, 0);
        signedInEmailContainer.setPadding(0, statusBarHeight, 0, 0);
    }

    private void showSignedInUser() {
        if (Lbryio.isSignedIn()) {
            findViewById(R.id.sign_in_button_container).setVisibility(View.GONE);
            findViewById(R.id.signed_in_email_container).setVisibility(View.VISIBLE);
            ((TextView) findViewById(R.id.signed_in_email)).setText(Lbryio.getSignedInEmail());
            findViewById(R.id.sign_in_header_divider).setBackgroundColor(getResources().getColor(R.color.lightDivider));
        }
    }

    private Fragment getCurrentFragment() {
        return getSupportFragmentManager().findFragmentById(R.id.content_main);
    }

    public void hideActionBar() {
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }
    }
    public void showActionBar() {
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.show();
        }
    }
    private void renderStartupFailed(List<StartupStage> startupStages) {
        ListView listView = findViewById(R.id.startup_stage_error_listview);
        StartupStageAdapter adapter = new StartupStageAdapter(this, startupStages);
        listView.setAdapter(adapter);

        // Add 1 pixel to listview height
        int listHeight = Math.round(getResources().getDisplayMetrics().density);

        for (int i = 0; i < startupStages.size(); i++) {
            View item = adapter.getView(i, null, listView);
            item.measure(0, 0);
            listHeight += item.getMeasuredHeight();
        }

        // Properly set listview height by adding all seven items and the divider heights
        // and the additional 1 pixel so no vertical scroll bar is shown
        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = listHeight + (listView.getCount() + 1) * listView.getDividerHeight();
        listView.setLayoutParams(params);
        listView.invalidate();
        listView.requestLayout();

        findViewById(R.id.splash_view_loading_container).setVisibility(View.GONE);
        findViewById(R.id.splash_view_error_container).setVisibility(View.VISIBLE);
    }

    @SuppressLint("StaticFieldLeak")
    private void startup() {
        final Context context = this;
        Lbry.startupInit();

        // perform some tasks before launching
        (new AsyncTask<Void, Void, Boolean>() {
            private final List<StartupStage> startupStages = new ArrayList<>(7);

            private void initStartupStages() {
                startupStages.add(new StartupStage(STARTUP_STAGE_INSTALL_ID_LOADED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_KNOWN_TAGS_LOADED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_EXCHANGE_RATE_LOADED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_USER_AUTHENTICATED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_NEW_INSTALL_DONE, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_BLOCK_LIST_LOADED, false));
                startupStages.add(new StartupStage(STARTUP_STAGE_FILTER_LIST_LOADED, false));
            }
            protected void onPreExecute() {
                hideActionBar();
                lockDrawer();
                findViewById(R.id.splash_view).setVisibility(View.VISIBLE);
                findViewById(R.id.drawer_layout).setVisibility(View.VISIBLE);
                LbryAnalytics.setCurrentScreen(MainActivity.this, "Splash", "Splash");
                initStartupStages();
            }
            protected Boolean doInBackground(Void... params) {
                BufferedReader reader = null;
                try {
                    // Load the installation id from the file system
                    String lbrynetDir = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet");
                    String installIdPath = String.format("%s/install_id", lbrynetDir);
                    reader = new BufferedReader(new InputStreamReader(new FileInputStream(installIdPath)));
                    String installId = reader.readLine();
                    if (Helper.isNullOrEmpty(installId)) {
                        // no install_id found (first run didn't start the sdk successfully?)
                        startupStages.set(STARTUP_STAGE_INSTALL_ID_LOADED - 1, new StartupStage(STARTUP_STAGE_INSTALL_ID_LOADED, false));
                        return false;
                    }

                    Lbry.INSTALLATION_ID = installId;
                    startupStages.set(STARTUP_STAGE_INSTALL_ID_LOADED - 1, new StartupStage(STARTUP_STAGE_INSTALL_ID_LOADED, true));

                    SQLiteDatabase db = dbHelper.getReadableDatabase();
                    List<Tag> fetchedTags = DatabaseHelper.getTags(db);
                    Lbry.knownTags = Helper.mergeKnownTags(fetchedTags);
                    Collections.sort(Lbry.knownTags, new Tag());
                    Lbry.followedTags = Helper.filterFollowedTags(Lbry.knownTags);
                    startupStages.set(STARTUP_STAGE_KNOWN_TAGS_LOADED - 1, new StartupStage(STARTUP_STAGE_KNOWN_TAGS_LOADED, true));

                    // load the exchange rate
                    Lbryio.loadExchangeRate();
                    if (Lbryio.LBCUSDRate == 0) {
                        return false;
                    }
                    startupStages.set(STARTUP_STAGE_EXCHANGE_RATE_LOADED - 1, new StartupStage(STARTUP_STAGE_EXCHANGE_RATE_LOADED, true));

                    try {
                        Lbryio.authenticate(context);
                    } catch (AuthTokenInvalidatedException ex) {
                        // if this happens, attempt to authenticate again, so that we can obtain a new auth token
                        // this will also result in the user having to sign in again
                        Lbryio.authenticate(context);
                    }
                    if (Lbryio.currentUser == null) {
                        throw new Exception("Did not retrieve authenticated user.");
                    }
                    startupStages.set(STARTUP_STAGE_USER_AUTHENTICATED - 1, new StartupStage(STARTUP_STAGE_USER_AUTHENTICATED, true));

                    Lbryio.newInstall(context);
                    startupStages.set(STARTUP_STAGE_NEW_INSTALL_DONE - 1, new StartupStage(STARTUP_STAGE_NEW_INSTALL_DONE, true));

                    // (light) fetch subscriptions
                    if (Lbryio.subscriptions.size() == 0) {
                        List<Subscription> subscriptions = new ArrayList<>();
                        List<String> subUrls = new ArrayList<>();
                        JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
                        if (array != null) {
                            for (int i = 0; i < array.length(); i++) {
                                JSONObject item = array.getJSONObject(i);
                                String claimId = item.getString("claim_id");
                                String channelName = item.getString("channel_name");
                                boolean isNotificationsDisabled = item.getBoolean("is_notifications_disabled");

                                LbryUri url = new LbryUri();
                                url.setChannelName(channelName);
                                url.setClaimId(claimId);
                                subscriptions.add(new Subscription(channelName, url.toString(), isNotificationsDisabled));
                                subUrls.add(url.toString());
                            }
                            Lbryio.subscriptions = subscriptions;
                            startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_LOADED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true));

                            // resolve subscriptions
                            if (subUrls.size() > 0 && Lbryio.cacheResolvedSubscriptions.size() != Lbryio.subscriptions.size()) {
                                Lbryio.cacheResolvedSubscriptions = Lbry.resolve(subUrls, Lbry.LBRY_TV_CONNECTION_STRING);
                            }
                            // if no exceptions occurred here, subscriptions have been loaded and resolved
                            startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true));
                        } else {
                            // user has not subscribed to anything
                            startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_LOADED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true));
                            startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true));
                        }
                    } else {
                        startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_LOADED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true));
                        startupStages.set(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED - 1, new StartupStage(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true));
                    }

                    JSONObject blockedObject = (JSONObject) Lbryio.parseResponse(Lbryio.call("file", "list_blocked", context));
                    JSONArray blockedArray = blockedObject.getJSONArray("outpoints");
                    Lbryio.populateOutpointList(Lbryio.blockedOutpoints, blockedArray);
                    startupStages.set(STARTUP_STAGE_BLOCK_LIST_LOADED - 1, new StartupStage(STARTUP_STAGE_BLOCK_LIST_LOADED, true));

                    JSONObject filteredObject = (JSONObject) Lbryio.parseResponse(Lbryio.call("file", "list_filtered", context));
                    JSONArray filteredArray = filteredObject.getJSONArray("outpoints");
                    Lbryio.populateOutpointList(Lbryio.filteredOutpoints, filteredArray);
                    startupStages.set(STARTUP_STAGE_FILTER_LIST_LOADED - 1, new StartupStage(STARTUP_STAGE_FILTER_LIST_LOADED, true));
                } catch (Exception ex) {
                    // nope
                    Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex);
                    return false;
                } finally {
                    Helper.closeCloseable(reader);
                }

                return true;
            }
            protected void onPostExecute(Boolean startupSuccessful) {
                if (!startupSuccessful) {
                    // show which startup stage failed
                    renderStartupFailed(startupStages);
                    appStarted = false;
                    return;
                }

                findViewById(R.id.splash_view).setVisibility(View.GONE);
                unlockDrawer();
                showActionBar();

                if (navMenuAdapter != null) {
                    navMenuAdapter.setCurrentItem(NavMenuItem.ID_ITEM_FOLLOWING);
                }

                loadLastFragment();
                showSignedInUser();
                fetchRewards();
                loadRemoteNotifications(false);

                checkUrlIntent(getIntent());
                checkWebSocketClient();
                LbryAnalytics.logEvent(LbryAnalytics.EVENT_APP_LAUNCH);
                appStarted = true;
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void fetchRewards() {
        FetchRewardsTask task = new FetchRewardsTask(null, new FetchRewardsTask.FetchRewardsHandler() {
            @Override
            public void onSuccess(List<Reward> rewards) {
                Lbryio.updateRewardsLists(rewards);
                for (Fragment fragment : openNavFragments.values()) {
                    if (fragment instanceof RewardsFragment) {
                        ((RewardsFragment) fragment).updateUnclaimedRewardsValue();
                    }
                }

                if (Lbryio.totalUnclaimedRewardAmount > 0) {
                    showFloatingUnclaimedRewards();
                    updateRewardsUsdVale();
                }
            }

            @Override
            public void onError(Exception error) {
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void updateRewardsUsdVale() {
        if (Lbryio.totalUnclaimedRewardAmount > 0) {
            double usdRewardAmount = Lbryio.totalUnclaimedRewardAmount * Lbryio.LBCUSDRate;
            if (navMenuAdapter != null) {
                navMenuAdapter.setExtraLabelForItem(
                        NavMenuItem.ID_ITEM_REWARDS,
                        Lbryio.LBCUSDRate > 0 ? String.format("$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(usdRewardAmount)) : null
                );
            }
        }
    }

    public void showFloatingUnclaimedRewards() {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean notInterestedInRewards = sp.getBoolean(PREFERENCE_KEY_INTERNAL_REWARDS_NOT_INTERESTED, false);
        if (notInterestedInRewards) {
            return;
        }

        ((TextView) findViewById(R.id.floating_reward_value)).setText(Helper.shortCurrencyFormat(Lbryio.totalUnclaimedRewardAmount));
        findViewById(R.id.floating_reward_container).setVisibility(View.VISIBLE);
    }

    private void checkUrlIntent(Intent intent) {
        if (intent != null) {
            Uri data = intent.getData();
            if (data != null) {
                String url = data.toString();
                // check special urls
                if (url.startsWith(SPECIAL_URL_PREFIX)) {
                    String specialPath = url.substring(8).toLowerCase();
                    if (specialRouteFragmentClassMap.containsKey(specialPath)) {
                        Class fragmentClass = specialRouteFragmentClassMap.get(specialPath);
                        if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
                            Map<String, Object> params = new HashMap<>();
                            String tag = intent.getStringExtra("tag");
                            params.put("singleTag", tag);

                            openFragment(
                                    specialRouteFragmentClassMap.get(specialPath),
                                    true,
                                    fragmentClassNavIdMap.get(fragmentClass),
                                    !Helper.isNullOrEmpty(tag) ? params : null
                            );
                        }
                    }

                    // unrecognised path will open the following by default
                } else {
                    try {
                        LbryUri uri = LbryUri.parse(url);
                        String checkedURL = url.startsWith(LbryUri.PROTO_DEFAULT) ? url : uri.toString();
                        if (uri.isChannel()) {
                            openChannelUrl(checkedURL);
                        } else {
                            openFileUrl(checkedURL);
                        }
                    } catch (LbryUriException ex) {
                        // pass
                    }
                }

                inPictureInPictureMode = false;
                renderFullMode();
            }
        }
    }

    private void loadLastFragment() {
        Fragment fragment = getCurrentFragment();

        if (fragment != null) {
            openFragment(fragment, true);
        } else {
            openFragment(FollowingFragment.class, false, NavMenuItem.ID_ITEM_FOLLOWING);
        }
    }

    private void setSelectedNavMenuItemForFragment(Fragment fragment) {
        if (fragment == null) {
            // assume the first fragment is selected
            navMenuAdapter.setCurrentItem(NavMenuItem.ID_ITEM_FOLLOWING);
            return;
        }

        Class fragmentClass = fragment.getClass();
        if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
            navMenuAdapter.setCurrentItem(fragmentClassNavIdMap.get(fragmentClass));
        }
    }

    @Override
    protected void onUserLeaveHint() {
        if (startingShareActivity) {
            // share activity triggered this, so reset the flag at this point
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    startingShareActivity = false;
                }
            }, 1000);
            return;
        }
        if (startingPermissionRequest) {
            return;
        }
        enterPIPMode();
    }

    protected boolean enterPIPMode() {
        if (enteringPIPMode) {
            return true;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
                appPlayer != null &&
                !startingFilePickerActivity &&
                !startingSignInFlowActivity) {
            enteringPIPMode = true;
            PictureInPictureParams params = new PictureInPictureParams.Builder().build();

            try {
                enterPictureInPictureMode(params);
                return true;
            } catch (IllegalStateException ex) {
                // pass
                enteringPIPMode = false;
            }
        }

        return false;
    }

    private void checkNotificationOpenIntent(Intent intent) {
        if (intent != null) {
            String notificationName = intent.getStringExtra("notification_name");
            if (notificationName != null) {
                logNotificationOpen(notificationName);
            }
        }
    }

    private void logNotificationOpen(String name) {
        Bundle bundle = new Bundle();
        bundle.putString("name", name);
        LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle);
    }

    private void checkSendToIntent(Intent intent) {
        String intentAction = intent.getAction();
        if (intentAction != null && intentAction.equals("android.intent.action.SEND")) {
            ClipData clipData = intent.getClipData();
            if (clipData != null) {
                Uri uri = clipData.getItemAt(0).getUri();

                String path = Helper.getRealPathFromURI_API19(this, uri);
                openSendTo(path);
            }
        }
    }

    private void registerServiceActionsReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
        intentFilter.addAction(LbrynetService.LBRY_SDK_SERVICE_STARTED);
        intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE);
        serviceActionsReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (LbrynetService.ACTION_STOP_SERVICE.equals(action)) {
                    MainActivity.this.receivedStopService = true;

                    // STOP is meant to close everything,
                    // So destroy the player at this point (even with background play enabled)
                    if (appPlayer != null) {
                        playerNotificationManager.setPlayer(null);
                        stopExoplayer();
                        nowPlayingClaim = null;
                        nowPlayingClaimUrl = null;
                        nowPlayingClaimBitmap = null;
                    }

                    MainActivity.this.finish();
                } else if (LbrynetService.LBRY_SDK_SERVICE_STARTED.equals(action)) {
                    // Rebuild the service notification
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            Notification svcNotification = buildServiceNotification();
                            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
                            notificationManager.notify(1, svcNotification);
                        }
                    }, 1000);
                } else if (DownloadManager.ACTION_DOWNLOAD_EVENT.equalsIgnoreCase(action)) {
                    String downloadAction = intent.getStringExtra("action");
                    String uri = intent.getStringExtra("uri");
                    String outpoint = intent.getStringExtra("outpoint");
                    String fileInfoJson = intent.getStringExtra("file_info");
                    double progress = intent.getDoubleExtra("progress", 0);
                    if (uri == null || outpoint == null || (fileInfoJson == null && !"abort".equals(downloadAction))) {
                        return;
                    }

                    for (DownloadActionListener listener : downloadActionListeners) {
                        listener.onDownloadAction(downloadAction, uri, outpoint, fileInfoJson, progress);
                    }
                }
            }
        };
        registerReceiver(serviceActionsReceiver, intentFilter);
    }

    private void unregisterReceivers() {
        Helper.unregisterReceiver(requestsReceiver, this);
        Helper.unregisterReceiver(serviceActionsReceiver, this);
    }

    private Notification buildServiceNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, LbrynetService.NOTIFICATION_CHANNEL_ID);
        Intent contextIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, contextIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        Intent stopIntent = new Intent(LbrynetService.ACTION_STOP_SERVICE);
        PendingIntent stopPendingIntent = PendingIntent.getBroadcast(this, 0, stopIntent, 0);

        String serviceDescription = "The LBRY service is running in the background.";
        Notification notification = builder.setColor(ContextCompat.getColor(this, R.color.lbryGreen))
                .setContentIntent(pendingIntent)
                .setContentText(serviceDescription)
                .setGroup(LbrynetService.GROUP_SERVICE)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.ic_lbry)
                .setOngoing(true)
                .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent)
                .build();

        return notification;
    }

    private static List<NavMenuItem> buildNavMenu(Context context) {
        NavMenuItem findContentGroup = new NavMenuItem(NavMenuItem.ID_GROUP_FIND_CONTENT, R.string.find_content, true, context);
        NavMenuItem yourContentGroup = new NavMenuItem(NavMenuItem.ID_GROUP_YOUR_CONTENT, R.string.your_content, true, context);
        NavMenuItem walletGroup = new NavMenuItem(NavMenuItem.ID_GROUP_WALLET, R.string.wallet, true, context);
        NavMenuItem otherGroup = new NavMenuItem(NavMenuItem.ID_GROUP_OTHER, 0, true, context);

        findContentGroup.setItems(Arrays.asList(
                new NavMenuItem(NavMenuItem.ID_ITEM_FOLLOWING, R.string.fa_heart, R.string.following, "Following", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_EDITORS_CHOICE, R.string.fa_star, R.string.editors_choice, "EditorsChoice", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_ALL_CONTENT, R.string.fa_globe_americas, R.string.all_content, "AllContent", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_SHUFFLE, R.string.fa_random, R.string.shuffle, "Shuffle",context)
        ));

        yourContentGroup.setItems(Arrays.asList(
                new NavMenuItem(NavMenuItem.ID_ITEM_NEW_PUBLISH, R.string.fa_upload, R.string.new_publish, "NewPublish", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_CHANNELS, R.string.fa_at, R.string.channels, "Channels", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_LIBRARY, R.string.fa_download, R.string.library, "Library", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_PUBLISHES, R.string.fa_cloud_upload, R.string.publishes, "Publishes", context)
        ));

        walletGroup.setItems(Arrays.asList(
                new NavMenuItem(NavMenuItem.ID_ITEM_WALLET, R.string.fa_wallet, R.string.wallet, "Wallet", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_REWARDS, R.string.fa_award, R.string.rewards, "Rewards", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_INVITES, R.string.fa_user_friends, R.string.invites, "Invites", context)
        ));

        otherGroup.setItems(Arrays.asList(
                new NavMenuItem(NavMenuItem.ID_ITEM_SETTINGS, R.string.fa_cog, R.string.settings, "Settings", context),
                new NavMenuItem(NavMenuItem.ID_ITEM_ABOUT, R.string.fa_mobile_alt, R.string.about, "About", context)
        ));

        return Arrays.asList(findContentGroup, yourContentGroup, walletGroup, otherGroup);
    }

    // Flatten the structure into a single list for the RecyclerView
    private static List<NavMenuItem> flattenNavMenu(List<NavMenuItem> navMenuItems) {
        List<NavMenuItem> flatMenu = new ArrayList<>();
        for (NavMenuItem item : navMenuItems) {
            flatMenu.add(item);
            if (item.getItems() != null) {
                for (NavMenuItem subItem : item.getItems()) {
                    flatMenu.add(subItem);
                }
            }
        }

        return flatMenu;
    }

    public void setNowPlayingClaim(Claim claim, String url) {
        nowPlayingClaim = claim;
        nowPlayingClaimUrl = url;
        if (claim != null) {
            ((TextView) findViewById(R.id.global_now_playing_title)).setText(nowPlayingClaim.getTitle());
            ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(nowPlayingClaim.getPublisherTitle());
        }
    }

    public void clearNowPlayingClaim() {
        nowPlayingClaim = null;
        nowPlayingClaimUrl = null;
        nowPlayingClaimBitmap = null;
        findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
        ((TextView) findViewById(R.id.global_now_playing_title)).setText(null);
        ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(null);
        if (appPlayer != null) {
            appPlayer.setPlayWhenReady(false);
        }
    }

    private static class CheckSdkReadyTask extends AsyncTask<Void, Void, Boolean> {
        private final Context context;
        private final List<SdkStatusListener> listeners;

        public CheckSdkReadyTask(Context context, List<SdkStatusListener> listeners) {
            this.context = context;
            this.listeners = new ArrayList<>(listeners);
        }

        public Boolean doInBackground(Void... params) {
            boolean sdkReady = false;
            try {
                String response = Utils.sdkCall("status");
                if (response != null) {
                    JSONObject result = new JSONObject(response);
                    JSONObject status = result.getJSONObject("result");
                    if (!Lbry.IS_STATUS_PARSED) {
                        Lbry.parseStatus(status.toString());
                    }

                    // TODO: Broadcast startup status changes
                    JSONObject startupStatus = status.getJSONObject("startup_status");
                    sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet");
                }
            } catch (ConnectException | JSONException ex) {
                // pass
            }

            return sdkReady;
        }
        protected void onPostExecute(Boolean sdkReady) {
            Lbry.SDK_READY = sdkReady;
            if (context != null) {
                if (sdkReady) {
                    context.sendBroadcast(new Intent(ACTION_SDK_READY));

                    // update listeners
                    for (SdkStatusListener listener : listeners) {
                        if (listener != null) {
                            listener.onSdkReady();
                        }
                    }
                } else if (context instanceof MainActivity) {
                    ((MainActivity) context).checkSdkReady();
                }
            }
        }
    }

    public void showNavigationBackIcon() {
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        if (toggle != null) {
            TypedArray a = getTheme().obtainStyledAttributes(R.style.AppTheme, new int[] {R.attr.homeAsUpIndicator});
            int attributeResourceId = a.getResourceId(0, 0);
            Drawable drawable = ResourcesCompat.getDrawable(getResources(), attributeResourceId, null);
            DrawableCompat.setTint(drawable, ContextCompat.getColor(this, R.color.actionBarForeground));

            toggle.setDrawerIndicatorEnabled(false);
            toggle.setHomeAsUpIndicator(drawable);
        }
    }

    private void closeDrawer() {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.closeDrawer(GravityCompat.START);
    }

    public void lockDrawer() {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
    }

    public void unlockDrawer() {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
    }

    public void restoreToggle() {
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(false);
            actionBar.setTitle(null);
        }
        if (toggle != null) {
            toggle.setDrawerIndicatorEnabled(true);
        }
        unlockDrawer();
        showSearchBar();
    }

    public void hideSearchBar() {
        findViewById(R.id.wunderbar_container).setVisibility(View.GONE);
    }

    public void showSearchBar() {
        findViewById(R.id.wunderbar_container).setVisibility(View.VISIBLE);
        clearWunderbarFocus(findViewById(R.id.wunderbar));
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        inPictureInPictureMode = isInPictureInPictureMode;
        enteringPIPMode = false;
        if (isInPictureInPictureMode) {
            // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
            renderPictureInPictureMode();
        } else {
            // Restore the full-screen UI.
            renderFullMode();
        }
    }

    protected void onStop() {
        if (appPlayer != null && inPictureInPictureMode && !isBackgroundPlaybackEnabled()) {
            appPlayer.setPlayWhenReady(false);
        }
        if (inPictureInPictureMode) {
            MainActivity.playerReassigned = true;
        }
        super.onStop();
    }

    public void openFragment(Fragment fragment, boolean allowNavigateBack) {
        Fragment currentFragment = getCurrentFragment();
        if (currentFragment != null && currentFragment.equals(fragment)) {
            return;
        }

        try {
            FragmentManager manager = getSupportFragmentManager();
            FragmentTransaction transaction = manager.beginTransaction().replace(R.id.content_main, fragment);
            if (allowNavigateBack) {
                transaction.addToBackStack(null);
            }
            transaction.commit();
        } catch (Exception ex) {
            // pass
        }
    }

    public void openFragment(Class fragmentClass, boolean allowNavigateBack, int navItemId) {
        openFragment(fragmentClass, allowNavigateBack, navItemId, null);
    }

    private static String buildNavFragmentKey(Class fragmentClass, int navItemId, Map<String, Object> params) {
        if (params != null && params.containsKey("url")) {
            return String.format("%s-%d-%s", fragmentClass.getName(), navItemId, params.get("url").toString());
        }

        return String.format("%s-%d", fragmentClass.getName(), navItemId);
    }

    public void popFragment(Class fragmentClass, int navItemId, Map<String, Object> params) {
        String key = buildNavFragmentKey(fragmentClass, navItemId, params);
        openNavFragments.remove(key);
    }

    public void openFragment(Class fragmentClass, boolean allowNavigateBack, int navItemId, Map<String, Object> params) {
        try {
            String key = buildNavFragmentKey(fragmentClass, navItemId, params);
            Fragment fragment = openNavFragments.containsKey(key) ? openNavFragments.get(key) : (Fragment) fragmentClass.newInstance();
            if (fragment instanceof BaseFragment) {
                ((BaseFragment) fragment).setParams(params);
            }
            Fragment currentFragment = getCurrentFragment();
            if (currentFragment != null && currentFragment.equals(fragment)) {
                return;
            }

            if (currentFragment != null && ((BaseFragment) currentFragment).getParams() != null
                    && ((BaseFragment) currentFragment).getParams().containsKey("source")
                    && ((BaseFragment) currentFragment).getParams().get("source").equals("notification")) {

                Map<String, Object> currentParams = new HashMap<>(1);

                if (((BaseFragment) currentFragment).getParams().containsKey("url"))
                    currentParams.put("url", ((BaseFragment) currentFragment).getParams().get("url"));

                ((BaseFragment) currentFragment).setParams(currentParams);
            }

            //fragment.setRetainInstance(true);
            FragmentManager manager = getSupportFragmentManager();
            FragmentTransaction transaction = manager.beginTransaction().replace(R.id.content_main, fragment);
            if (allowNavigateBack) {
                transaction.addToBackStack(null);
            }
            transaction.commit();

            if (navItemId > -1) {
                openNavFragments.put(key, fragment);
            }
        } catch (Exception ex) {
            // pass
        }
    }

    public void fetchOwnChannels() {
        ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, null, new ClaimListResultHandler() {
            @Override
            public void onSuccess(List<Claim> claims) {
                Lbry.ownChannels = Helper.filterDeletedClaims(new ArrayList<>(claims));
                for (FetchChannelsListener listener : fetchChannelsListeners) {
                    listener.onChannelsFetched(claims);
                }
            }

            @Override
            public void onError(Exception error) {
                // pass
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void fetchOwnClaims() {
        ClaimListTask task = new ClaimListTask(Arrays.asList(Claim.TYPE_STREAM, Claim.TYPE_REPOST), null, new ClaimListResultHandler() {
            @Override
            public void onSuccess(List<Claim> claims) {
                Lbry.ownClaims = Helper.filterDeletedClaims(new ArrayList<>(claims));
                for (FetchClaimsListener listener : fetchClaimsListeners) {
                    listener.onClaimsFetched(claims);
                }
            }

            @Override
            public void onError(Exception error) { }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void unlockTips() {
        if (unlockingTips) {
            return;
        }
        UnlockTipsTask task = new UnlockTipsTask(new GenericTaskHandler() {
            @Override
            public void beforeStart() {
                unlockingTips = true;
            }

            @Override
            public void onSuccess() {
                unlockingTips = false;
                for (Fragment fragment : openNavFragments.values()) {
                    if (fragment instanceof WalletFragment) {
                        ((WalletFragment) fragment).checkTips(true);
                    }
                }
            }

            @Override
            public void onError(Exception error) {
                unlockingTips = false;
                for (Fragment fragment : openNavFragments.values()) {
                    if (fragment instanceof WalletFragment) {
                        ((WalletFragment) fragment).checkTips();
                    }
                }
                // fail silently?
                //showError(error.getMessage());
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void displayUnseenNotificationCount(int count) {
        String text = count > 99 ? "99+" : String.valueOf(count);

        TextView badge = findViewById(R.id.notifications_badge_count);
        badge.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE);
        badge.setText(text);
    }

    private void loadUnseenNotificationsCount() {
        (new AsyncTask<Void, Void, Integer>() {
            @Override
            protected Integer doInBackground(Void... params) {
                try {
                    SQLiteDatabase db = dbHelper.getReadableDatabase();
                    return DatabaseHelper.getUnseenNotificationsCount(db);
                } catch (Exception ex) {
                    return 0;
                }
            }
            protected void onPostExecute(Integer count) {
                displayUnseenNotificationCount(count);
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void loadRemoteNotifications(boolean markRead) {
        findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE);
        NotificationListTask task = new NotificationListTask(this, findViewById(R.id.notifications_progress), new NotificationListTask.ListNotificationsHandler() {
            @Override
            public void onSuccess(List<LbryNotification> notifications) {
                remoteNotifcationsLastLoaded = new Date();

                loadUnseenNotificationsCount();
                loadLocalNotifications();
                if (markRead && findViewById(R.id.notifications_container).getVisibility() == View.VISIBLE) {
                    markNotificationsSeen();
                }

                if (notificationsSwipeContainer != null) {
                    notificationsSwipeContainer.setRefreshing(false);
                }
            }

            @Override
            public void onError(Exception exception) {
                // pass
                Log.e(TAG, "error loading remote notifications", exception);
                loadLocalNotifications();
                if (notificationsSwipeContainer != null) {
                    notificationsSwipeContainer.setRefreshing(false);
                }
            }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void loadLocalNotifications() {
        (new AsyncTask<Void, Void, List<LbryNotification>>() {
            protected void onPreExecute() {
                findViewById(R.id.notification_list_empty_container).setVisibility(View.GONE);
                findViewById(R.id.notifications_progress).setVisibility(View.VISIBLE);
            }
            @Override
            protected List<LbryNotification> doInBackground(Void... params) {
                List<LbryNotification> notifications = new ArrayList<>();
                try {
                    SQLiteDatabase db = dbHelper.getReadableDatabase();
                    notifications = DatabaseHelper.getNotifications(db);
                } catch (Exception ex) {
                    // pass
                }
                return notifications;
            }
            protected void onPostExecute(List<LbryNotification> notifications) {
                findViewById(R.id.notification_list_empty_container).setVisibility(notifications.size() == 0 ? View.VISIBLE : View.GONE);
                findViewById(R.id.notifications_progress).setVisibility(View.GONE);
                loadUnseenNotificationsCount();

                if (notificationListAdapter == null) {
                    notificationListAdapter = new NotificationListAdapter(notifications, MainActivity.this);
                    notificationListAdapter.setSelectionModeListener(MainActivity.this);
                    ((RecyclerView) findViewById(R.id.notifications_list)).setAdapter(notificationListAdapter);
                } else {
                    notificationListAdapter.addNotifications(notifications);
                }

                resolveCommentAuthors(notificationListAdapter.getAuthorUrls());
                notificationListAdapter.setClickListener(new NotificationListAdapter.NotificationClickListener() {
                    @Override
                    public void onNotificationClicked(LbryNotification notification) {
                        // set as seen and read
                        NotificationUpdateTask task = new NotificationUpdateTask(Arrays.asList(notification.getRemoteId()), true, true, true);
                        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                        markNotificationReadAndSeen(notification.getId());
                        notification.setSeen(true);
                        if (notificationListAdapter != null) {
                            notificationListAdapter.notifyDataSetChanged();
                        }

                        String targetUrl = notification.getTargetUrl();
                        if (targetUrl.startsWith(SPECIAL_URL_PREFIX)) {
                            openSpecialUrl(targetUrl, "notification");
                        } else {
                            LbryUri target = LbryUri.tryParse(notification.getTargetUrl());
                            if (target != null) {
                                if (target.isChannel()) {
                                    openChannelUrl(notification.getTargetUrl(), "notification");
                                } else {
                                    openFileUrl(notification.getTargetUrl(), "notification");
                                }
                            }
                        }
                        hideNotifications();
                    }
                });
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void markNotificationReadAndSeen(long notificationId) {
        (new AsyncTask<Void, Void, Void>() {
            protected Void doInBackground(Void... params) {
                if (dbHelper != null) {
                    try {
                        SQLiteDatabase db = dbHelper.getWritableDatabase();
                        DatabaseHelper.markNotificationReadAndSeen(notificationId, db);
                    } catch (Exception ex) {
                        // pass
                    }
                }
                return null;
            }
            protected void onPostExecute() {
                loadUnseenNotificationsCount();
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void resolveCommentAuthors(List<String> urls) {
        if (urls != null && urls.size() > 0) {
            ResolveTask task = new ResolveTask(urls, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimListResultHandler() {
                @Override
                public void onSuccess(List<Claim> claims) {
                    if (notificationListAdapter != null) {
                        notificationListAdapter.updateAuthorClaims(claims);
                    }
                }

                @Override
                public void onError(Exception error) {
                    // pass
                }
            });
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

    private void checkSyncedWallet() {
        String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
        // Just check if the current user has a synced wallet, no need to do anything else here
        SyncGetTask task = new SyncGetTask(password, false, null, new DefaultSyncTaskHandler() {
            @Override
            public void onSyncGetSuccess(WalletSync walletSync) {
                Lbryio.userHasSyncedWallet = true;
                Lbryio.setLastWalletSync(walletSync);
                Lbryio.setLastRemoteHash(walletSync.getHash());
            }

            @Override
            public void onSyncGetWalletNotFound() { }
            @Override
            public void onSyncGetError(Exception error) { }
        });
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public static void requestPermission(String permission, int requestCode, String rationale, Context context, boolean forceRequest) {
        if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) {
                if (context instanceof MainActivity) {
                    ((MainActivity) context).showMessage(rationale);
                }
            } else if (forceRequest) {
                startingPermissionRequest = true;
                ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
            } else {
                if (context instanceof MainActivity) {
                    ((MainActivity) context).showError(rationale);
                }
            }
        }
    }

    public static boolean hasPermission(String permission, Context context) {
        if (context == null) {
            return false;
        }
        return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
    }

    private void establishBillingClientConnection() {
        if (billingClient != null) {
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        // no need to do anything here. purchases are always checked server-side
                        checkPurchases();
                    }
                }

                @Override
                public void onBillingServiceDisconnected() {
                    establishBillingClientConnection();
                }
            });
        }
    }

    public static void handleBillingPurchase(
            Purchase purchase,
            BillingClient billingClient,
            Context context,
            View progressView,
            RewardVerifiedHandler handler) {
        String sku = purchase.getSku();
        if (SKU_SKIP.equalsIgnoreCase(sku)) {
            // send purchase token for verification
            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
                /*&& isSignatureValid(purchase)*/) {
                // consume the purchase
                String purchaseToken = purchase.getPurchaseToken();
                ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build();
                billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
                    @Override
                    public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) {

                    }
                });

                // send the purchase token to the backend to complete verification
                AndroidPurchaseTask task = new AndroidPurchaseTask(purchaseToken, progressView, context, handler);
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        }
    }

    private void checkAndEnableShareUsageData() {
        final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        boolean shareUsageData = sp.getBoolean(PREFERENCE_KEY_SHARE_USAGE_DATA, false);
        if (shareUsageData) {
            return;
        }

        (new AsyncTask<Void, Void, Void>() {
            protected Void doInBackground(Void... params) {
                PrintStream out = null;
                try {
                    String fileContent = "true";
                    String path = String.format("%s/sud", Utils.getAppInternalStorageDir(MainActivity.this));
                    out = new PrintStream(new FileOutputStream(path));
                    out.print(fileContent);
                } catch (Exception ex) {
                    // pass
                } finally {
                    Helper.closeCloseable(out);
                }
                return null;
            }

            protected void onPostExecute(Void result) {
                updateSdkSetting("share_usage_data", true);
                sp.edit().putBoolean(PREFERENCE_KEY_SHARE_USAGE_DATA, true).apply();
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void updateSdkSetting(String key, Object value) {
        (new Thread() {
            public void run() {
                Map<String, Object> params = new HashMap<>();
                params.put(key, value);
                try {
                    Lbry.parseResponse(Lbry.apiCall("settings_set", params, Lbry.SDK_CONNECTION_STRING));
                } catch (Exception ex) {
                    // pass
                    Log.d(TAG, ex.getMessage(), ex);
                }
            }
        }).start();
    }

    public interface BackPressInterceptor {
        boolean onBackPressed();
    }
}
