- * DownloadActivity.java is part of NewPipe.
- *
- * NewPipe is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * NewPipe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with NewPipe. If not, see .
- */
-
-package org.schabi.newpipe;
-
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.FrameLayout;
-import android.widget.Spinner;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.ActionBarDrawerToggle;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.view.GravityCompat;
-import androidx.drawerlayout.widget.DrawerLayout;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentContainerView;
-import androidx.fragment.app.FragmentManager;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.bottomsheet.BottomSheetBehavior;
-
-import org.schabi.newpipe.databinding.ActivityMainBinding;
-import org.schabi.newpipe.databinding.DrawerHeaderBinding;
-import org.schabi.newpipe.databinding.DrawerLayoutBinding;
-import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
-import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
-import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.StreamingService;
-import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
-import org.schabi.newpipe.extractor.exceptions.ExtractionException;
-import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
-import org.schabi.newpipe.fragments.BackPressable;
-import org.schabi.newpipe.fragments.MainFragment;
-import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
-import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
-import org.schabi.newpipe.fragments.list.search.SearchFragment;
-import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
-import org.schabi.newpipe.player.Player;
-import org.schabi.newpipe.player.event.OnKeyDownListener;
-import org.schabi.newpipe.player.helper.PlayerHolder;
-import org.schabi.newpipe.player.playqueue.PlayQueue;
-import org.schabi.newpipe.settings.UpdateSettingsFragment;
-import org.schabi.newpipe.util.Constants;
-import org.schabi.newpipe.util.DeviceUtils;
-import org.schabi.newpipe.util.KioskTranslator;
-import org.schabi.newpipe.util.Localization;
-import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.PeertubeHelper;
-import org.schabi.newpipe.util.PermissionHelper;
-import org.schabi.newpipe.util.ReleaseVersionUtil;
-import org.schabi.newpipe.util.SerializedCache;
-import org.schabi.newpipe.util.ServiceHelper;
-import org.schabi.newpipe.util.StateSaver;
-import org.schabi.newpipe.util.ThemeHelper;
-import org.schabi.newpipe.views.FocusOverlayView;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class MainActivity extends AppCompatActivity {
- private static final String TAG = "MainActivity";
- @SuppressWarnings("ConstantConditions")
- public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
-
- private ActivityMainBinding mainBinding;
- private DrawerHeaderBinding drawerHeaderBinding;
- private DrawerLayoutBinding drawerLayoutBinding;
- private ToolbarLayoutBinding toolbarLayoutBinding;
-
- private ActionBarDrawerToggle toggle;
-
- private boolean servicesShown = false;
-
- private BroadcastReceiver broadcastReceiver;
-
- private static final int ITEM_ID_SUBSCRIPTIONS = -1;
- private static final int ITEM_ID_FEED = -2;
- private static final int ITEM_ID_BOOKMARKS = -3;
- private static final int ITEM_ID_DOWNLOADS = -4;
- private static final int ITEM_ID_HISTORY = -5;
- private static final int ITEM_ID_SETTINGS = 0;
- private static final int ITEM_ID_ABOUT = 1;
-
- private static final int ORDER = 0;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Activity's LifeCycle
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- if (DEBUG) {
- Log.d(TAG, "onCreate() called with: "
- + "savedInstanceState = [" + savedInstanceState + "]");
- }
-
- ThemeHelper.setDayNightMode(this);
- ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
-
- assureCorrectAppLanguage(this);
- super.onCreate(savedInstanceState);
-
- mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
- drawerLayoutBinding = mainBinding.drawerLayout;
- drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
- .getHeaderView(0));
- toolbarLayoutBinding = mainBinding.toolbarLayout;
- setContentView(mainBinding.getRoot());
-
- if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
- initFragments();
- }
-
- setSupportActionBar(toolbarLayoutBinding.toolbar);
- try {
- setupDrawer();
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Setting up drawer", e);
- }
- if (DeviceUtils.isTv(this)) {
- FocusOverlayView.setupFocusObserver(this);
- }
- openMiniPlayerUponPlayerStarted();
-
- if (PermissionHelper.checkPostNotificationsPermission(this,
- PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE)) {
- // Schedule worker for checking for new streams and creating corresponding notifications
- // if this is enabled by the user.
- NotificationWorker.initialize(this);
- }
- if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
- && !App.getApp().isFirstRun()
- && ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
- UpdateSettingsFragment.askForConsentToUpdateChecks(this);
- }
- }
-
- @Override
- protected void onPostCreate(final Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
-
- final App app = App.getApp();
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
-
- if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
- && prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
- // Start the worker which is checking all conditions
- // and eventually searching for a new version.
- NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
- }
- }
-
- private void setupDrawer() throws ExtractionException {
- addDrawerMenuForCurrentService();
-
- toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
- toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
- toggle.syncState();
- mainBinding.getRoot().addDrawerListener(toggle);
- mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
- private int lastService;
-
- @Override
- public void onDrawerOpened(final View drawerView) {
- lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
- }
-
- @Override
- public void onDrawerClosed(final View drawerView) {
- if (servicesShown) {
- toggleServices();
- }
- if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
- ActivityCompat.recreate(MainActivity.this);
- }
- }
- });
-
- drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
- setupDrawerHeader();
- }
-
- /**
- * Builds the drawer menu for the current service.
- *
- * @throws ExtractionException if the service didn't provide available kiosks
- */
- private void addDrawerMenuForCurrentService() throws ExtractionException {
- //Tabs
- final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
- final StreamingService service = NewPipe.getService(currentServiceId);
-
- int kioskMenuItemId = 0;
-
- for (final String ks : service.getKioskList().getAvailableKiosks()) {
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
- .getTranslatedKioskName(ks, this))
- .setIcon(KioskTranslator.getKioskIcon(ks));
- kioskMenuItemId++;
- }
-
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
- R.string.tab_subscriptions)
- .setIcon(R.drawable.ic_tv);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
- .setIcon(R.drawable.ic_subscriptions);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
- .setIcon(R.drawable.ic_bookmark);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
- .setIcon(R.drawable.ic_file_download);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
- .setIcon(R.drawable.ic_history);
-
- //Settings and About
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
- .setIcon(R.drawable.ic_settings);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
- .setIcon(R.drawable.ic_info_outline);
- }
-
- private boolean drawerItemSelected(final MenuItem item) {
- switch (item.getGroupId()) {
- case R.id.menu_services_group:
- changeService(item);
- break;
- case R.id.menu_tabs_group:
- try {
- tabSelected(item);
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
- }
- break;
- case R.id.menu_options_about_group:
- optionsAboutSelected(item);
- break;
- default:
- return false;
- }
-
- mainBinding.getRoot().closeDrawers();
- return true;
- }
-
- private void changeService(final MenuItem item) {
- drawerLayoutBinding.navigation.getMenu()
- .getItem(ServiceHelper.getSelectedServiceId(this))
- .setChecked(false);
- ServiceHelper.setSelectedServiceId(this, item.getItemId());
- drawerLayoutBinding.navigation.getMenu()
- .getItem(ServiceHelper.getSelectedServiceId(this))
- .setChecked(true);
- }
-
- private void tabSelected(final MenuItem item) throws ExtractionException {
- switch (item.getItemId()) {
- case ITEM_ID_SUBSCRIPTIONS:
- NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
- break;
- case ITEM_ID_FEED:
- NavigationHelper.openFeedFragment(getSupportFragmentManager());
- break;
- case ITEM_ID_BOOKMARKS:
- NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
- break;
- case ITEM_ID_DOWNLOADS:
- NavigationHelper.openDownloads(this);
- break;
- case ITEM_ID_HISTORY:
- NavigationHelper.openStatisticFragment(getSupportFragmentManager());
- break;
- default:
- final StreamingService currentService = ServiceHelper.getSelectedService(this);
- int kioskMenuItemId = 0;
- for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
- if (kioskMenuItemId == item.getItemId()) {
- NavigationHelper.openKioskFragment(getSupportFragmentManager(),
- currentService.getServiceId(), kioskId);
- break;
- }
- kioskMenuItemId++;
- }
- break;
- }
- }
-
- private void optionsAboutSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case ITEM_ID_SETTINGS:
- NavigationHelper.openSettings(this);
- break;
- case ITEM_ID_ABOUT:
- NavigationHelper.openAbout(this);
- break;
- }
- }
-
- private void setupDrawerHeader() {
- drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices());
-
- // If the current app name is bigger than the default "NewPipe" (7 chars),
- // let the text view grow a little more as well.
- if (getString(R.string.app_name).length() > "NewPipe".length()) {
- final ViewGroup.LayoutParams layoutParams =
- drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
- layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
- drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
- drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
- drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
- .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
- drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
- .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
- }
- }
-
- private void toggleServices() {
- servicesShown = !servicesShown;
-
- drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
- drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
- drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
-
- // Show up or down arrow
- drawerHeaderBinding.drawerArrow.setImageResource(
- servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
-
- if (servicesShown) {
- showServices();
- } else {
- try {
- addDrawerMenuForCurrentService();
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Showing main page tabs", e);
- }
- }
- }
-
- private void showServices() {
- for (final StreamingService s : NewPipe.getServices()) {
- final String title = s.getServiceInfo().getName();
-
- final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
- .setIcon(ServiceHelper.getIcon(s.getServiceId()));
-
- // peertube specifics
- if (s.getServiceId() == 3) {
- enhancePeertubeMenu(menuItem);
- }
- }
- drawerLayoutBinding.navigation.getMenu()
- .getItem(ServiceHelper.getSelectedServiceId(this))
- .setChecked(true);
- }
-
- private void enhancePeertubeMenu(final MenuItem menuItem) {
- final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
- menuItem.setTitle(currentInstance.getName());
- final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
- .getRoot();
- final List instances = PeertubeHelper.getInstanceList(this);
- final List items = new ArrayList<>();
- int defaultSelect = 0;
- for (final PeertubeInstance instance : instances) {
- items.add(instance.getName());
- if (instance.getUrl().equals(currentInstance.getUrl())) {
- defaultSelect = items.size() - 1;
- }
- }
- final ArrayAdapter adapter = new ArrayAdapter<>(this,
- R.layout.instance_spinner_item, items);
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- spinner.setAdapter(adapter);
- spinner.setSelection(defaultSelect, false);
- spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(final AdapterView> parent, final View view,
- final int position, final long id) {
- final PeertubeInstance newInstance = instances.get(position);
- if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
- return;
- }
- PeertubeHelper.selectInstance(newInstance, getApplicationContext());
- changeService(menuItem);
- mainBinding.getRoot().closeDrawers();
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- getSupportFragmentManager().popBackStack(null,
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- ActivityCompat.recreate(MainActivity.this);
- }, 300);
- }
-
- @Override
- public void onNothingSelected(final AdapterView> parent) {
-
- }
- });
- menuItem.setActionView(spinner);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (!isChangingConfigurations()) {
- StateSaver.clearStateFiles();
- }
- if (broadcastReceiver != null) {
- unregisterReceiver(broadcastReceiver);
- }
- }
-
- @Override
- protected void onResume() {
- assureCorrectAppLanguage(this);
- // Change the date format to match the selected language on resume
- Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
- super.onResume();
-
- // Close drawer on return, and don't show animation,
- // so it looks like the drawer isn't open when the user returns to MainActivity
- mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
- try {
- final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
- final String selectedServiceName = NewPipe.getService(selectedServiceId)
- .getServiceInfo().getName();
- drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
- drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
- .getIcon(selectedServiceId));
-
- drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
- .drawerHeaderServiceView.setSelected(true));
- drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
- getString(R.string.drawer_header_description) + selectedServiceName);
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
- }
-
- final SharedPreferences sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(this);
- if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
- if (DEBUG) {
- Log.d(TAG, "Theme has changed, recreating activity...");
- }
- sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
- ActivityCompat.recreate(this);
- }
-
- if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
- if (DEBUG) {
- Log.d(TAG, "main page has changed, recreating main fragment...");
- }
- sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
- NavigationHelper.openMainActivity(this);
- }
-
- final boolean isHistoryEnabled = sharedPreferences.getBoolean(
- getString(R.string.enable_watch_history_key), true);
- drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
- .setVisible(isHistoryEnabled);
- }
-
- @Override
- protected void onNewIntent(final Intent intent) {
- if (DEBUG) {
- Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
- }
- if (intent != null) {
- // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
- // to not destroy the already created backstack
- final String action = intent.getAction();
- if ((action != null && action.equals(Intent.ACTION_MAIN))
- && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
- return;
- }
- }
-
- super.onNewIntent(intent);
- setIntent(intent);
- handleIntent(intent);
- }
-
- @Override
- public boolean onKeyDown(final int keyCode, final KeyEvent event) {
- final Fragment fragment = getSupportFragmentManager()
- .findFragmentById(R.id.fragment_player_holder);
- if (fragment instanceof OnKeyDownListener
- && !bottomSheetHiddenOrCollapsed()) {
- // Provide keyDown event to fragment which then sends this event
- // to the main player service
- return ((OnKeyDownListener) fragment).onKeyDown(keyCode)
- || super.onKeyDown(keyCode, event);
- }
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public void onBackPressed() {
- if (DEBUG) {
- Log.d(TAG, "onBackPressed() called");
- }
-
- if (DeviceUtils.isTv(this)) {
- if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
- mainBinding.getRoot().closeDrawers();
- return;
- }
- }
-
- // In case bottomSheet is not visible on the screen or collapsed we can assume that the user
- // interacts with a fragment inside fragment_holder so all back presses should be
- // handled by it
- if (bottomSheetHiddenOrCollapsed()) {
- final FragmentManager fm = getSupportFragmentManager();
- final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
- // If current fragment implements BackPressable (i.e. can/wanna handle back press)
- // delegate the back press to it
- if (fragment instanceof BackPressable) {
- if (((BackPressable) fragment).onBackPressed()) {
- return;
- }
- } else if (fragment instanceof CommentRepliesFragment) {
- // expand DetailsFragment if CommentRepliesFragment was opened
- // to show the top level comments again
- // Expand DetailsFragment if CommentRepliesFragment was opened
- // and no other CommentRepliesFragments are on top of the back stack
- // to show the top level comments again.
- openDetailFragmentFromCommentReplies(fm, false);
- }
-
- } else {
- final Fragment fragmentPlayer = getSupportFragmentManager()
- .findFragmentById(R.id.fragment_player_holder);
- // If current fragment implements BackPressable (i.e. can/wanna handle back press)
- // delegate the back press to it
- if (fragmentPlayer instanceof BackPressable) {
- if (!((BackPressable) fragmentPlayer).onBackPressed()) {
- BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
- .setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- return;
- }
- }
-
- if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
- finish();
- } else {
- super.onBackPressed();
- }
- }
-
- @Override
- public void onRequestPermissionsResult(final int requestCode,
- @NonNull final String[] permissions,
- @NonNull final int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- for (final int i : grantResults) {
- if (i == PackageManager.PERMISSION_DENIED) {
- return;
- }
- }
- switch (requestCode) {
- case PermissionHelper.DOWNLOADS_REQUEST_CODE:
- NavigationHelper.openDownloads(this);
- break;
- case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
- final Fragment fragment = getSupportFragmentManager()
- .findFragmentById(R.id.fragment_player_holder);
- if (fragment instanceof VideoDetailFragment) {
- ((VideoDetailFragment) fragment).openDownloadDialog();
- }
- break;
- case PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE:
- NotificationWorker.initialize(this);
- break;
- }
- }
-
- /**
- * Implement the following diagram behavior for the up button:
- *
- * +---------------+
- * | Main Screen +----+
- * +-------+-------+ |
- * | |
- * â–² Up | Search Button
- * | |
- * +----+-----+ |
- * +------------+ Search |â—„-----+
- * | +----+-----+
- * | Open |
- * | something â–² Up
- * | |
- * | +------------+-------------+
- * | | |
- * | | Video <-> Channel |
- * +---â–º| Channel <-> Playlist |
- * | Video <-> .... |
- * | |
- * +--------------------------+
- *
- */
- private void onHomeButtonPressed() {
- final FragmentManager fm = getSupportFragmentManager();
- final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
-
- if (fragment instanceof CommentRepliesFragment) {
- // Expand DetailsFragment if CommentRepliesFragment was opened
- // and no other CommentRepliesFragments are on top of the back stack
- // to show the top level comments again.
- openDetailFragmentFromCommentReplies(fm, true);
- } else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
- // If search fragment wasn't found in the backstack go to the main fragment
- NavigationHelper.gotoMainFragment(fm);
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Menu
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- if (DEBUG) {
- Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
- }
- super.onCreateOptionsMenu(menu);
-
- final Fragment fragment =
- getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
- if (!(fragment instanceof SearchFragment)) {
- toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
- }
-
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(false);
- }
-
- updateDrawerNavigation();
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
- if (DEBUG) {
- Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
- }
-
- if (item.getItemId() == android.R.id.home) {
- onHomeButtonPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Init
- //////////////////////////////////////////////////////////////////////////*/
-
- private void initFragments() {
- if (DEBUG) {
- Log.d(TAG, "initFragments() called");
- }
- StateSaver.clearStateFiles();
- if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
- // When user watch a video inside popup and then tries to open the video in main player
- // while the app is closed he will see a blank fragment on place of kiosk.
- // Let's open it first
- if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
- NavigationHelper.openMainFragment(getSupportFragmentManager());
- }
-
- handleIntent(getIntent());
- } else {
- NavigationHelper.gotoMainFragment(getSupportFragmentManager());
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- private void updateDrawerNavigation() {
- if (getSupportActionBar() == null) {
- return;
- }
-
- final Fragment fragment = getSupportFragmentManager()
- .findFragmentById(R.id.fragment_holder);
- if (fragment instanceof MainFragment) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(false);
- if (toggle != null) {
- toggle.syncState();
- toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
- .open());
- mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
- }
- } else {
- mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
- }
- }
-
- private void handleIntent(final Intent intent) {
- try {
- if (DEBUG) {
- Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
- }
-
- if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
- final String url = intent.getStringExtra(Constants.KEY_URL);
- final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
- String title = intent.getStringExtra(Constants.KEY_TITLE);
- if (title == null) {
- title = "";
- }
-
- final StreamingService.LinkType linkType = ((StreamingService.LinkType) intent
- .getSerializableExtra(Constants.KEY_LINK_TYPE));
- assert linkType != null;
- switch (linkType) {
- case STREAM:
- final String intentCacheKey = intent.getStringExtra(
- Player.PLAY_QUEUE_KEY);
- final PlayQueue playQueue = intentCacheKey != null
- ? SerializedCache.getInstance()
- .take(intentCacheKey, PlayQueue.class)
- : null;
-
- final boolean switchingPlayers = intent.getBooleanExtra(
- VideoDetailFragment.KEY_SWITCHING_PLAYERS, false);
- NavigationHelper.openVideoDetailFragment(
- getApplicationContext(), getSupportFragmentManager(),
- serviceId, url, title, playQueue, switchingPlayers);
- break;
- case CHANNEL:
- NavigationHelper.openChannelFragment(getSupportFragmentManager(),
- serviceId, url, title);
- break;
- case PLAYLIST:
- NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
- serviceId, url, title);
- break;
- }
- } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
- String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
- if (searchString == null) {
- searchString = "";
- }
- final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
- NavigationHelper.openSearchFragment(
- getSupportFragmentManager(),
- serviceId,
- searchString);
-
- } else {
- NavigationHelper.gotoMainFragment(getSupportFragmentManager());
- }
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Handling intent", e);
- }
- }
-
- private void openMiniPlayerIfMissing() {
- final Fragment fragmentPlayer = getSupportFragmentManager()
- .findFragmentById(R.id.fragment_player_holder);
- if (fragmentPlayer == null) {
- // We still don't have a fragment attached to the activity. It can happen when a user
- // started popup or background players without opening a stream inside the fragment.
- // Adding it in a collapsed state (only mini player will be visible).
- NavigationHelper.showMiniPlayer(getSupportFragmentManager());
- }
- }
-
- private void openMiniPlayerUponPlayerStarted() {
- if (getIntent().getSerializableExtra(Constants.KEY_LINK_TYPE)
- == StreamingService.LinkType.STREAM) {
- // handleIntent() already takes care of opening video detail fragment
- // due to an intent containing a STREAM link
- return;
- }
-
- if (PlayerHolder.getInstance().isPlayerOpen()) {
- // if the player is already open, no need for a broadcast receiver
- openMiniPlayerIfMissing();
- } else {
- // listen for player start intent being sent around
- broadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- if (Objects.equals(intent.getAction(),
- VideoDetailFragment.ACTION_PLAYER_STARTED)) {
- openMiniPlayerIfMissing();
- // At this point the player is added 100%, we can unregister. Other actions
- // are useless since the fragment will not be removed after that.
- unregisterReceiver(broadcastReceiver);
- broadcastReceiver = null;
- }
- }
- };
- final IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
- registerReceiver(broadcastReceiver, intentFilter);
- }
- }
-
- private void openDetailFragmentFromCommentReplies(
- @NonNull final FragmentManager fm,
- final boolean popBackStack
- ) {
- // obtain the name of the fragment under the replies fragment that's going to be popped
- @Nullable final String fragmentUnderEntryName;
- if (fm.getBackStackEntryCount() < 2) {
- fragmentUnderEntryName = null;
- } else {
- fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
- .getName();
- }
-
- // the root comment is the comment for which the user opened the replies page
- @Nullable final CommentRepliesFragment repliesFragment =
- (CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
- @Nullable final CommentsInfoItem rootComment =
- repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
-
- // sometimes this function pops the backstack, other times it's handled by the system
- if (popBackStack) {
- fm.popBackStackImmediate();
- }
-
- // only expand the bottom sheet back if there are no more nested comment replies fragments
- // stacked under the one that is currently being popped
- if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
- return;
- }
-
- final BottomSheetBehavior behavior = BottomSheetBehavior
- .from(mainBinding.fragmentPlayerHolder);
- // do not return to the comment if the details fragment was closed
- if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
- return;
- }
-
- // scroll to the root comment once the bottom sheet expansion animation is finished
- behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull final View bottomSheet,
- final int newState) {
- if (newState == BottomSheetBehavior.STATE_EXPANDED) {
- final Fragment detailFragment = fm.findFragmentById(
- R.id.fragment_player_holder);
- if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
- // should always be the case
- ((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
- }
- behavior.removeBottomSheetCallback(this);
- }
- }
-
- @Override
- public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
- // not needed, listener is removed once the sheet is expanded
- }
- });
-
- behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
- }
-
- private boolean bottomSheetHiddenOrCollapsed() {
- final BottomSheetBehavior bottomSheetBehavior =
- BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
-
- final int sheetState = bottomSheetBehavior.getState();
- return sheetState == BottomSheetBehavior.STATE_HIDDEN
- || sheetState == BottomSheetBehavior.STATE_COLLAPSED;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.kt b/app/src/main/java/org/schabi/newpipe/MainActivity.kt
new file mode 100644
index 00000000000..f113326ded5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.kt
@@ -0,0 +1,820 @@
+/*
+ * Created by Christian Schabesberger on 02.08.16.
+ *
+ * Copyright (C) Christian Schabesberger 2016
+ * DownloadActivity.java is part of NewPipe.
+ *
+ * NewPipe is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * NewPipe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with NewPipe. If not, see .
+ */
+package org.schabi.newpipe
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.FrameLayout
+import android.widget.Spinner
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.view.GravityCompat
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.FragmentManager
+import androidx.preference.PreferenceManager
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.navigation.NavigationView
+import org.schabi.newpipe.NewVersionWorker.Companion.enqueueNewVersionCheckingWork
+import org.schabi.newpipe.databinding.ActivityMainBinding
+import org.schabi.newpipe.databinding.DrawerHeaderBinding
+import org.schabi.newpipe.databinding.DrawerLayoutBinding
+import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding
+import org.schabi.newpipe.databinding.ToolbarLayoutBinding
+import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
+import org.schabi.newpipe.extractor.NewPipe
+import org.schabi.newpipe.extractor.StreamingService
+import org.schabi.newpipe.extractor.StreamingService.LinkType
+import org.schabi.newpipe.extractor.comments.CommentsInfoItem
+import org.schabi.newpipe.extractor.exceptions.ExtractionException
+import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
+import org.schabi.newpipe.fragments.BackPressable
+import org.schabi.newpipe.fragments.MainFragment
+import org.schabi.newpipe.fragments.detail.VideoDetailFragment
+import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment
+import org.schabi.newpipe.fragments.list.search.SearchFragment
+import org.schabi.newpipe.local.feed.notifications.NotificationWorker.Companion.initialize
+import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.event.OnKeyDownListener
+import org.schabi.newpipe.player.helper.PlayerHolder
+import org.schabi.newpipe.player.playqueue.PlayQueue
+import org.schabi.newpipe.settings.UpdateSettingsFragment
+import org.schabi.newpipe.util.DeviceUtils
+import org.schabi.newpipe.util.KioskTranslator
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.NavigationHelper
+import org.schabi.newpipe.util.PeertubeHelper
+import org.schabi.newpipe.util.PermissionHelper
+import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
+import org.schabi.newpipe.util.SerializedCache
+import org.schabi.newpipe.util.ServiceHelper
+import org.schabi.newpipe.util.StateSaver
+import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.views.FocusOverlayView
+import java.util.Objects
+
+class MainActivity() : AppCompatActivity() {
+ private var mainBinding: ActivityMainBinding? = null
+ private var drawerHeaderBinding: DrawerHeaderBinding? = null
+ private var drawerLayoutBinding: DrawerLayoutBinding? = null
+ private var toolbarLayoutBinding: ToolbarLayoutBinding? = null
+ private var toggle: ActionBarDrawerToggle? = null
+ private var servicesShown: Boolean = false
+ private var broadcastReceiver: BroadcastReceiver? = null
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Activity's LifeCycle
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ if (DEBUG) {
+ Log.d(TAG, ("onCreate() called with: "
+ + "savedInstanceState = [" + savedInstanceState + "]"))
+ }
+ ThemeHelper.setDayNightMode(this)
+ ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this))
+ Localization.assureCorrectAppLanguage(this)
+ super.onCreate(savedInstanceState)
+ mainBinding = ActivityMainBinding.inflate(getLayoutInflater())
+ drawerLayoutBinding = mainBinding!!.drawerLayout
+ drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding!!.navigation
+ .getHeaderView(0))
+ toolbarLayoutBinding = mainBinding!!.toolbarLayout
+ setContentView(mainBinding!!.getRoot())
+ if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+ initFragments()
+ }
+ setSupportActionBar(toolbarLayoutBinding!!.toolbar)
+ try {
+ setupDrawer()
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Setting up drawer", e)
+ }
+ if (DeviceUtils.isTv(this)) {
+ FocusOverlayView.Companion.setupFocusObserver(this)
+ }
+ openMiniPlayerUponPlayerStarted()
+ if (PermissionHelper.checkPostNotificationsPermission(this,
+ PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE)) {
+ // Schedule worker for checking for new streams and creating corresponding notifications
+ // if this is enabled by the user.
+ initialize(this)
+ }
+ if ((!UpdateSettingsFragment.Companion.wasUserAskedForConsent(this)
+ && !App.Companion.getApp().isFirstRun()
+ && isReleaseApk)) {
+ UpdateSettingsFragment.Companion.askForConsentToUpdateChecks(this)
+ }
+ }
+
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ super.onPostCreate(savedInstanceState)
+ val app: App = App.Companion.getApp()
+ val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(app)
+ if ((prefs.getBoolean(app.getString(R.string.update_app_key), false)
+ && prefs.getBoolean(app.getString(R.string.update_check_consent_key), false))) {
+ // Start the worker which is checking all conditions
+ // and eventually searching for a new version.
+ enqueueNewVersionCheckingWork(app, false)
+ }
+ }
+
+ @Throws(ExtractionException::class)
+ private fun setupDrawer() {
+ addDrawerMenuForCurrentService()
+ toggle = ActionBarDrawerToggle(this, mainBinding!!.getRoot(),
+ toolbarLayoutBinding!!.toolbar, R.string.drawer_open, R.string.drawer_close)
+ toggle!!.syncState()
+ mainBinding!!.getRoot().addDrawerListener(toggle!!)
+ mainBinding!!.getRoot().addDrawerListener(object : SimpleDrawerListener() {
+ private var lastService: Int = 0
+ public override fun onDrawerOpened(drawerView: View) {
+ lastService = ServiceHelper.getSelectedServiceId(this@MainActivity)
+ }
+
+ public override fun onDrawerClosed(drawerView: View) {
+ if (servicesShown) {
+ toggleServices()
+ }
+ if (lastService != ServiceHelper.getSelectedServiceId(this@MainActivity)) {
+ ActivityCompat.recreate(this@MainActivity)
+ }
+ }
+ })
+ drawerLayoutBinding!!.navigation.setNavigationItemSelectedListener(NavigationView.OnNavigationItemSelectedListener({ item: MenuItem -> drawerItemSelected(item) }))
+ setupDrawerHeader()
+ }
+
+ /**
+ * Builds the drawer menu for the current service.
+ *
+ * @throws ExtractionException if the service didn't provide available kiosks
+ */
+ @Throws(ExtractionException::class)
+ private fun addDrawerMenuForCurrentService() {
+ //Tabs
+ val currentServiceId: Int = ServiceHelper.getSelectedServiceId(this)
+ val service: StreamingService = NewPipe.getService(currentServiceId)
+ var kioskMenuItemId: Int = 0
+ for (ks: String in service.getKioskList().getAvailableKiosks()) {
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
+ .setIcon(KioskTranslator.getKioskIcon(ks))
+ kioskMenuItemId++
+ }
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
+ R.string.tab_subscriptions)
+ .setIcon(R.drawable.ic_tv)
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
+ .setIcon(R.drawable.ic_subscriptions)
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
+ .setIcon(R.drawable.ic_bookmark)
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
+ .setIcon(R.drawable.ic_file_download)
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
+ .setIcon(R.drawable.ic_history)
+
+ //Settings and About
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
+ .setIcon(R.drawable.ic_settings)
+ drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
+ .setIcon(R.drawable.ic_info_outline)
+ }
+
+ private fun drawerItemSelected(item: MenuItem): Boolean {
+ when (item.getGroupId()) {
+ R.id.menu_services_group -> changeService(item)
+ R.id.menu_tabs_group -> try {
+ tabSelected(item)
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Selecting main page tab", e)
+ }
+
+ R.id.menu_options_about_group -> optionsAboutSelected(item)
+ else -> return false
+ }
+ mainBinding!!.getRoot().closeDrawers()
+ return true
+ }
+
+ private fun changeService(item: MenuItem) {
+ drawerLayoutBinding!!.navigation.getMenu()
+ .getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(false)
+ ServiceHelper.setSelectedServiceId(this, item.getItemId())
+ drawerLayoutBinding!!.navigation.getMenu()
+ .getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(true)
+ }
+
+ @Throws(ExtractionException::class)
+ private fun tabSelected(item: MenuItem) {
+ when (item.getItemId()) {
+ ITEM_ID_SUBSCRIPTIONS -> NavigationHelper.openSubscriptionFragment(getSupportFragmentManager())
+ ITEM_ID_FEED -> openFeedFragment(getSupportFragmentManager())
+ ITEM_ID_BOOKMARKS -> NavigationHelper.openBookmarksFragment(getSupportFragmentManager())
+ ITEM_ID_DOWNLOADS -> NavigationHelper.openDownloads(this)
+ ITEM_ID_HISTORY -> NavigationHelper.openStatisticFragment(getSupportFragmentManager())
+ else -> {
+ val currentService: StreamingService? = ServiceHelper.getSelectedService(this)
+ var kioskMenuItemId: Int = 0
+ for (kioskId: String in currentService!!.getKioskList().getAvailableKiosks()) {
+ if (kioskMenuItemId == item.getItemId()) {
+ NavigationHelper.openKioskFragment(getSupportFragmentManager(),
+ currentService.getServiceId(), kioskId)
+ break
+ }
+ kioskMenuItemId++
+ }
+ }
+ }
+ }
+
+ private fun optionsAboutSelected(item: MenuItem) {
+ when (item.getItemId()) {
+ ITEM_ID_SETTINGS -> NavigationHelper.openSettings(this)
+ ITEM_ID_ABOUT -> NavigationHelper.openAbout(this)
+ }
+ }
+
+ private fun setupDrawerHeader() {
+ drawerHeaderBinding!!.drawerHeaderActionButton.setOnClickListener(View.OnClickListener({ view: View? -> toggleServices() }))
+
+ // If the current app name is bigger than the default "NewPipe" (7 chars),
+ // let the text view grow a little more as well.
+ if (getString(R.string.app_name).length > "NewPipe".length) {
+ val layoutParams: ViewGroup.LayoutParams = drawerHeaderBinding!!.drawerHeaderNewpipeTitle.getLayoutParams()
+ layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
+ drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams)
+ drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMaxLines(2)
+ drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMinWidth(getResources()
+ .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width))
+ drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
+ .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width))
+ }
+ }
+
+ private fun toggleServices() {
+ servicesShown = !servicesShown
+ drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_services_group)
+ drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_tabs_group)
+ drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_options_about_group)
+
+ // Show up or down arrow
+ drawerHeaderBinding!!.drawerArrow.setImageResource(
+ if (servicesShown) R.drawable.ic_arrow_drop_up else R.drawable.ic_arrow_drop_down)
+ if (servicesShown) {
+ showServices()
+ } else {
+ try {
+ addDrawerMenuForCurrentService()
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Showing main page tabs", e)
+ }
+ }
+ }
+
+ private fun showServices() {
+ for (s: StreamingService in NewPipe.getServices()) {
+ val title: String = s.getServiceInfo().getName()
+ val menuItem: MenuItem = drawerLayoutBinding!!.navigation.getMenu()
+ .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
+ .setIcon(ServiceHelper.getIcon(s.getServiceId()))
+
+ // peertube specifics
+ if (s.getServiceId() == 3) {
+ enhancePeertubeMenu(menuItem)
+ }
+ }
+ drawerLayoutBinding!!.navigation.getMenu()
+ .getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(true)
+ }
+
+ private fun enhancePeertubeMenu(menuItem: MenuItem) {
+ val currentInstance: PeertubeInstance? = PeertubeHelper.getCurrentInstance()
+ menuItem.setTitle(currentInstance!!.getName())
+ val spinner: Spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
+ .getRoot()
+ val instances: List? = PeertubeHelper.getInstanceList(this)
+ val items: MutableList = ArrayList()
+ var defaultSelect: Int = 0
+ for (instance: PeertubeInstance? in instances!!) {
+ items.add(instance!!.getName())
+ if ((instance.getUrl() == currentInstance.getUrl())) {
+ defaultSelect = items.size - 1
+ }
+ }
+ val adapter: ArrayAdapter = ArrayAdapter(this,
+ R.layout.instance_spinner_item, items)
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ spinner.setAdapter(adapter)
+ spinner.setSelection(defaultSelect, false)
+ spinner.setOnItemSelectedListener(object : AdapterView.OnItemSelectedListener {
+ public override fun onItemSelected(parent: AdapterView<*>?, view: View,
+ position: Int, id: Long) {
+ val newInstance: PeertubeInstance? = instances.get(position)
+ if ((newInstance!!.getUrl() == PeertubeHelper.getCurrentInstance().getUrl())) {
+ return
+ }
+ PeertubeHelper.selectInstance(newInstance, getApplicationContext())
+ changeService(menuItem)
+ mainBinding!!.getRoot().closeDrawers()
+ Handler(Looper.getMainLooper()).postDelayed(Runnable({
+ getSupportFragmentManager().popBackStack(null,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ ActivityCompat.recreate(this@MainActivity)
+ }), 300)
+ }
+
+ public override fun onNothingSelected(parent: AdapterView<*>?) {}
+ })
+ menuItem.setActionView(spinner)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!isChangingConfigurations()) {
+ StateSaver.clearStateFiles()
+ }
+ if (broadcastReceiver != null) {
+ unregisterReceiver(broadcastReceiver)
+ }
+ }
+
+ override fun onResume() {
+ Localization.assureCorrectAppLanguage(this)
+ // Change the date format to match the selected language on resume
+ Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()))
+ super.onResume()
+
+ // Close drawer on return, and don't show animation,
+ // so it looks like the drawer isn't open when the user returns to MainActivity
+ mainBinding!!.getRoot().closeDrawer(GravityCompat.START, false)
+ try {
+ val selectedServiceId: Int = ServiceHelper.getSelectedServiceId(this)
+ val selectedServiceName: String = NewPipe.getService(selectedServiceId)
+ .getServiceInfo().getName()
+ drawerHeaderBinding!!.drawerHeaderServiceView.setText(selectedServiceName)
+ drawerHeaderBinding!!.drawerHeaderServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId))
+ drawerHeaderBinding!!.drawerHeaderServiceView.post(Runnable({ drawerHeaderBinding!!.drawerHeaderServiceView.setSelected(true) }))
+ drawerHeaderBinding!!.drawerHeaderActionButton.setContentDescription(
+ getString(R.string.drawer_header_description) + selectedServiceName)
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Setting up service toggle", e)
+ }
+ val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ if (sharedPreferences.getBoolean(KEY_THEME_CHANGE, false)) {
+ if (DEBUG) {
+ Log.d(TAG, "Theme has changed, recreating activity...")
+ }
+ sharedPreferences.edit().putBoolean(KEY_THEME_CHANGE, false).apply()
+ ActivityCompat.recreate(this)
+ }
+ if (sharedPreferences.getBoolean(KEY_MAIN_PAGE_CHANGE, false)) {
+ if (DEBUG) {
+ Log.d(TAG, "main page has changed, recreating main fragment...")
+ }
+ sharedPreferences.edit().putBoolean(KEY_MAIN_PAGE_CHANGE, false).apply()
+ NavigationHelper.openMainActivity(this)
+ }
+ val isHistoryEnabled: Boolean = sharedPreferences.getBoolean(
+ getString(R.string.enable_watch_history_key), true)
+ drawerLayoutBinding!!.navigation.getMenu().findItem(ITEM_ID_HISTORY)
+ .setVisible(isHistoryEnabled)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ if (DEBUG) {
+ Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]")
+ }
+ if (intent != null) {
+ // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
+ // to not destroy the already created backstack
+ val action: String? = intent.getAction()
+ if (((action != null && (action == Intent.ACTION_MAIN))
+ && intent.hasCategory(Intent.CATEGORY_LAUNCHER))) {
+ return
+ }
+ }
+ super.onNewIntent(intent)
+ setIntent(intent)
+ handleIntent(intent)
+ }
+
+ public override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ val fragment: Fragment? = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_player_holder)
+ if ((fragment is OnKeyDownListener
+ && !bottomSheetHiddenOrCollapsed())) {
+ // Provide keyDown event to fragment which then sends this event
+ // to the main player service
+ return ((fragment as OnKeyDownListener).onKeyDown(keyCode)
+ || super.onKeyDown(keyCode, event))
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ public override fun onBackPressed() {
+ if (DEBUG) {
+ Log.d(TAG, "onBackPressed() called")
+ }
+ if (DeviceUtils.isTv(this)) {
+ if (mainBinding!!.getRoot().isDrawerOpen(drawerLayoutBinding!!.navigation)) {
+ mainBinding!!.getRoot().closeDrawers()
+ return
+ }
+ }
+
+ // In case bottomSheet is not visible on the screen or collapsed we can assume that the user
+ // interacts with a fragment inside fragment_holder so all back presses should be
+ // handled by it
+ if (bottomSheetHiddenOrCollapsed()) {
+ val fm: FragmentManager = getSupportFragmentManager()
+ val fragment: Fragment? = fm.findFragmentById(R.id.fragment_holder)
+ // If current fragment implements BackPressable (i.e. can/wanna handle back press)
+ // delegate the back press to it
+ if (fragment is BackPressable) {
+ if ((fragment as BackPressable).onBackPressed()) {
+ return
+ }
+ } else if (fragment is CommentRepliesFragment) {
+ // expand DetailsFragment if CommentRepliesFragment was opened
+ // to show the top level comments again
+ // Expand DetailsFragment if CommentRepliesFragment was opened
+ // and no other CommentRepliesFragments are on top of the back stack
+ // to show the top level comments again.
+ openDetailFragmentFromCommentReplies(fm, false)
+ }
+ } else {
+ val fragmentPlayer: Fragment? = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_player_holder)
+ // If current fragment implements BackPressable (i.e. can/wanna handle back press)
+ // delegate the back press to it
+ if (fragmentPlayer is BackPressable) {
+ if (!(fragmentPlayer as BackPressable).onBackPressed()) {
+ BottomSheetBehavior.from(mainBinding!!.fragmentPlayerHolder)
+ .setState(BottomSheetBehavior.STATE_COLLAPSED)
+ }
+ return
+ }
+ }
+ if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
+ finish()
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ public override fun onRequestPermissionsResult(requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ for (i: Int in grantResults) {
+ if (i == PackageManager.PERMISSION_DENIED) {
+ return
+ }
+ }
+ when (requestCode) {
+ PermissionHelper.DOWNLOADS_REQUEST_CODE -> NavigationHelper.openDownloads(this)
+ PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE -> {
+ val fragment: Fragment? = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_player_holder)
+ if (fragment is VideoDetailFragment) {
+ fragment.openDownloadDialog()
+ }
+ }
+
+ PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE -> initialize(this)
+ }
+ }
+
+ /**
+ * Implement the following diagram behavior for the up button:
+ *
+ * +---------------+
+ * | Main Screen +----+
+ * +-------+-------+ |
+ * | |
+ * â–² Up | Search Button
+ * | |
+ * +----+-----+ |
+ * +------------+ Search |â—„-----+
+ * | +----+-----+
+ * | Open |
+ * | something â–² Up
+ * | |
+ * | +------------+-------------+
+ * | | |
+ * | | Video <-> Channel |
+ * +---â–º| Channel <-> Playlist |
+ * | Video <-> .... |
+ * | |
+ * +--------------------------+
+
*
+ */
+ private fun onHomeButtonPressed() {
+ val fm: FragmentManager = getSupportFragmentManager()
+ val fragment: Fragment? = fm.findFragmentById(R.id.fragment_holder)
+ if (fragment is CommentRepliesFragment) {
+ // Expand DetailsFragment if CommentRepliesFragment was opened
+ // and no other CommentRepliesFragments are on top of the back stack
+ // to show the top level comments again.
+ openDetailFragmentFromCommentReplies(fm, true)
+ } else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
+ // If search fragment wasn't found in the backstack go to the main fragment
+ NavigationHelper.gotoMainFragment(fm)
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Menu
+ ////////////////////////////////////////////////////////////////////////// */
+ public override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ if (DEBUG) {
+ Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]")
+ }
+ super.onCreateOptionsMenu(menu)
+ val fragment: Fragment? = getSupportFragmentManager().findFragmentById(R.id.fragment_holder)
+ if (!(fragment is SearchFragment)) {
+ toolbarLayoutBinding!!.toolbarSearchContainer.getRoot().setVisibility(View.GONE)
+ }
+ val actionBar: ActionBar? = getSupportActionBar()
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(false)
+ }
+ updateDrawerNavigation()
+ return true
+ }
+
+ public override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (DEBUG) {
+ Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]")
+ }
+ if (item.getItemId() == android.R.id.home) {
+ onHomeButtonPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Init
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun initFragments() {
+ if (DEBUG) {
+ Log.d(TAG, "initFragments() called")
+ }
+ StateSaver.clearStateFiles()
+ if (getIntent() != null && getIntent().hasExtra(KEY_LINK_TYPE)) {
+ // When user watch a video inside popup and then tries to open the video in main player
+ // while the app is closed he will see a blank fragment on place of kiosk.
+ // Let's open it first
+ if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+ NavigationHelper.openMainFragment(getSupportFragmentManager())
+ }
+ handleIntent(getIntent())
+ } else {
+ NavigationHelper.gotoMainFragment(getSupportFragmentManager())
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun updateDrawerNavigation() {
+ if (getSupportActionBar() == null) {
+ return
+ }
+ val fragment: Fragment? = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_holder)
+ if (fragment is MainFragment) {
+ getSupportActionBar()!!.setDisplayHomeAsUpEnabled(false)
+ if (toggle != null) {
+ toggle!!.syncState()
+ toolbarLayoutBinding!!.toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? ->
+ mainBinding!!.getRoot()
+ .open()
+ }))
+ mainBinding!!.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
+ }
+ } else {
+ mainBinding!!.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+ getSupportActionBar()!!.setDisplayHomeAsUpEnabled(true)
+ toolbarLayoutBinding!!.toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? -> onHomeButtonPressed() }))
+ }
+ }
+
+ private fun handleIntent(intent: Intent) {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]")
+ }
+ if (intent.hasExtra(KEY_LINK_TYPE)) {
+ val url: String? = intent.getStringExtra(KEY_URL)
+ val serviceId: Int = intent.getIntExtra(KEY_SERVICE_ID, 0)
+ var title: String? = intent.getStringExtra(KEY_TITLE)
+ if (title == null) {
+ title = ""
+ }
+ val linkType: LinkType? = (intent
+ .getSerializableExtra(KEY_LINK_TYPE) as LinkType?)
+ assert(linkType != null)
+ when (linkType) {
+ LinkType.STREAM -> {
+ val intentCacheKey: String? = intent.getStringExtra(
+ Player.Companion.PLAY_QUEUE_KEY)
+ val playQueue: PlayQueue? = if (intentCacheKey != null) SerializedCache.Companion.getInstance()
+ .take(intentCacheKey, PlayQueue::class.java) else null
+ val switchingPlayers: Boolean = intent.getBooleanExtra(
+ VideoDetailFragment.Companion.KEY_SWITCHING_PLAYERS, false)
+ NavigationHelper.openVideoDetailFragment(
+ getApplicationContext(), getSupportFragmentManager(),
+ serviceId, url, title, playQueue, switchingPlayers)
+ }
+
+ LinkType.CHANNEL -> NavigationHelper.openChannelFragment(getSupportFragmentManager(),
+ serviceId, url, title)
+
+ LinkType.PLAYLIST -> NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
+ serviceId, url, title)
+ }
+ } else if (intent.hasExtra(KEY_OPEN_SEARCH)) {
+ var searchString: String? = intent.getStringExtra(KEY_SEARCH_STRING)
+ if (searchString == null) {
+ searchString = ""
+ }
+ val serviceId: Int = intent.getIntExtra(KEY_SERVICE_ID, 0)
+ NavigationHelper.openSearchFragment(
+ getSupportFragmentManager(),
+ serviceId,
+ searchString)
+ } else {
+ NavigationHelper.gotoMainFragment(getSupportFragmentManager())
+ }
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Handling intent", e)
+ }
+ }
+
+ private fun openMiniPlayerIfMissing() {
+ val fragmentPlayer: Fragment? = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_player_holder)
+ if (fragmentPlayer == null) {
+ // We still don't have a fragment attached to the activity. It can happen when a user
+ // started popup or background players without opening a stream inside the fragment.
+ // Adding it in a collapsed state (only mini player will be visible).
+ NavigationHelper.showMiniPlayer(getSupportFragmentManager())
+ }
+ }
+
+ private fun openMiniPlayerUponPlayerStarted() {
+ if ((getIntent().getSerializableExtra(KEY_LINK_TYPE)
+ === LinkType.STREAM)) {
+ // handleIntent() already takes care of opening video detail fragment
+ // due to an intent containing a STREAM link
+ return
+ }
+ if (PlayerHolder.Companion.getInstance().isPlayerOpen()) {
+ // if the player is already open, no need for a broadcast receiver
+ openMiniPlayerIfMissing()
+ } else {
+ // listen for player start intent being sent around
+ broadcastReceiver = object : BroadcastReceiver() {
+ public override fun onReceive(context: Context, intent: Intent) {
+ if (Objects.equals(intent.getAction(),
+ VideoDetailFragment.Companion.ACTION_PLAYER_STARTED)) {
+ openMiniPlayerIfMissing()
+ // At this point the player is added 100%, we can unregister. Other actions
+ // are useless since the fragment will not be removed after that.
+ unregisterReceiver(broadcastReceiver)
+ broadcastReceiver = null
+ }
+ }
+ }
+ val intentFilter: IntentFilter = IntentFilter()
+ intentFilter.addAction(VideoDetailFragment.Companion.ACTION_PLAYER_STARTED)
+ registerReceiver(broadcastReceiver, intentFilter)
+ }
+ }
+
+ private fun openDetailFragmentFromCommentReplies(
+ fm: FragmentManager,
+ popBackStack: Boolean
+ ) {
+ // obtain the name of the fragment under the replies fragment that's going to be popped
+ val fragmentUnderEntryName: String?
+ if (fm.getBackStackEntryCount() < 2) {
+ fragmentUnderEntryName = null
+ } else {
+ fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
+ .getName()
+ }
+
+ // the root comment is the comment for which the user opened the replies page
+ val repliesFragment: CommentRepliesFragment? = fm.findFragmentByTag(CommentRepliesFragment.Companion.TAG) as CommentRepliesFragment?
+ val rootComment: CommentsInfoItem? = if (repliesFragment == null) null else repliesFragment.getCommentsInfoItem()
+
+ // sometimes this function pops the backstack, other times it's handled by the system
+ if (popBackStack) {
+ fm.popBackStackImmediate()
+ }
+
+ // only expand the bottom sheet back if there are no more nested comment replies fragments
+ // stacked under the one that is currently being popped
+ if ((CommentRepliesFragment.Companion.TAG == fragmentUnderEntryName)) {
+ return
+ }
+ val behavior: BottomSheetBehavior = BottomSheetBehavior
+ .from(mainBinding!!.fragmentPlayerHolder)
+ // do not return to the comment if the details fragment was closed
+ if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
+ return
+ }
+
+ // scroll to the root comment once the bottom sheet expansion animation is finished
+ behavior.addBottomSheetCallback(object : BottomSheetCallback() {
+ public override fun onStateChanged(bottomSheet: View,
+ newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+ val detailFragment: Fragment? = fm.findFragmentById(
+ R.id.fragment_player_holder)
+ if (detailFragment is VideoDetailFragment && rootComment != null) {
+ // should always be the case
+ detailFragment.scrollToComment(rootComment)
+ }
+ behavior.removeBottomSheetCallback(this)
+ }
+ }
+
+ public override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ // not needed, listener is removed once the sheet is expanded
+ }
+ })
+ behavior.setState(BottomSheetBehavior.STATE_EXPANDED)
+ }
+
+ private fun bottomSheetHiddenOrCollapsed(): Boolean {
+ val bottomSheetBehavior: BottomSheetBehavior = BottomSheetBehavior.from(mainBinding!!.fragmentPlayerHolder)
+ val sheetState: Int = bottomSheetBehavior.getState()
+ return (sheetState == BottomSheetBehavior.STATE_HIDDEN
+ || sheetState == BottomSheetBehavior.STATE_COLLAPSED)
+ }
+
+ companion object {
+ private val TAG: String = "MainActivity"
+ val DEBUG: Boolean = !BuildConfig.BUILD_TYPE.equals("release")
+ private val ITEM_ID_SUBSCRIPTIONS: Int = -1
+ private val ITEM_ID_FEED: Int = -2
+ private val ITEM_ID_BOOKMARKS: Int = -3
+ private val ITEM_ID_DOWNLOADS: Int = -4
+ private val ITEM_ID_HISTORY: Int = -5
+ private val ITEM_ID_SETTINGS: Int = 0
+ private val ITEM_ID_ABOUT: Int = 1
+ private val ORDER: Int = 0
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
deleted file mode 100644
index 21c5354f44d..00000000000
--- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.schabi.newpipe;
-
-import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
-
-import android.content.Context;
-import android.database.Cursor;
-
-import androidx.annotation.NonNull;
-import androidx.room.Room;
-
-import org.schabi.newpipe.database.AppDatabase;
-
-public final class NewPipeDatabase {
- private static volatile AppDatabase databaseInstance;
-
- private NewPipeDatabase() {
- //no instance
- }
-
- private static AppDatabase getDatabase(final Context context) {
- return Room
- .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
- MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
- .build();
- }
-
- @NonNull
- public static AppDatabase getInstance(@NonNull final Context context) {
- AppDatabase result = databaseInstance;
- if (result == null) {
- synchronized (NewPipeDatabase.class) {
- result = databaseInstance;
- if (result == null) {
- databaseInstance = getDatabase(context);
- result = databaseInstance;
- }
- }
- }
-
- return result;
- }
-
- public static void checkpoint() {
- if (databaseInstance == null) {
- throw new IllegalStateException("database is not initialized");
- }
- final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
- if (c.moveToFirst() && c.getInt(0) == 1) {
- throw new RuntimeException("Checkpoint was blocked from completing");
- }
- }
-
- public static void close() {
- if (databaseInstance != null) {
- synchronized (NewPipeDatabase.class) {
- if (databaseInstance != null) {
- databaseInstance.close();
- databaseInstance = null;
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
new file mode 100644
index 00000000000..197d722608c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
@@ -0,0 +1,54 @@
+package org.schabi.newpipe
+
+import android.content.Context
+import android.database.Cursor
+import androidx.room.Room.databaseBuilder
+import org.schabi.newpipe.database.AppDatabase
+import org.schabi.newpipe.database.Migrations
+import kotlin.concurrent.Volatile
+
+object NewPipeDatabase {
+ @Volatile
+ private var databaseInstance: AppDatabase? = null
+ private fun getDatabase(context: Context): AppDatabase {
+ return databaseBuilder(context.getApplicationContext(), AppDatabase::class.java, AppDatabase.Companion.DATABASE_NAME)
+ .addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5,
+ Migrations.MIGRATION_5_6, Migrations.MIGRATION_6_7, Migrations.MIGRATION_7_8, Migrations.MIGRATION_8_9)
+ .build()
+ }
+
+ fun getInstance(context: Context): AppDatabase {
+ var result: AppDatabase? = databaseInstance
+ if (result == null) {
+ synchronized(NewPipeDatabase::class.java, {
+ result = databaseInstance
+ if (result == null) {
+ databaseInstance = getDatabase(context)
+ result = databaseInstance
+ }
+ })
+ }
+ return (result)!!
+ }
+
+ fun checkpoint() {
+ if (databaseInstance == null) {
+ throw IllegalStateException("database is not initialized")
+ }
+ val c: Cursor = databaseInstance!!.query("pragma wal_checkpoint(full)", null)
+ if (c.moveToFirst() && c.getInt(0) == 1) {
+ throw RuntimeException("Checkpoint was blocked from completing")
+ }
+ }
+
+ fun close() {
+ if (databaseInstance != null) {
+ synchronized(NewPipeDatabase::class.java, {
+ if (databaseInstance != null) {
+ databaseInstance!!.close()
+ databaseInstance = null
+ }
+ })
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
deleted file mode 100644
index f0d1af81a66..00000000000
--- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.schabi.newpipe;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-/*
- * Copyright (C) Hans-Christoph Steiner 2016
- * PanicResponderActivity.java is part of NewPipe.
- *
- * NewPipe is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * NewPipe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with NewPipe. If not, see .
- */
-
-public class PanicResponderActivity extends Activity {
- public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
-
- @SuppressLint("NewApi")
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final Intent intent = getIntent();
- if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
- // TODO: Explicitly clear the search results
- // once they are restored when the app restarts
- // or if the app reloads the current video after being killed,
- // that should be cleared also
- ExitActivity.exitAndRemoveFromRecentApps(this);
- }
-
- finishAndRemoveTask();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.kt b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.kt
new file mode 100644
index 00000000000..4fe3daa123f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.kt
@@ -0,0 +1,43 @@
+package org.schabi.newpipe
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+
+/*
+* Copyright (C) Hans-Christoph Steiner 2016
+* PanicResponderActivity.java is part of NewPipe.
+*
+* NewPipe is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* NewPipe is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with NewPipe. If not, see .
+*/
+class PanicResponderActivity() : Activity() {
+ @SuppressLint("NewApi")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val intent: Intent? = getIntent()
+ if (intent != null && (PANIC_TRIGGER_ACTION == intent.getAction())) {
+ // TODO: Explicitly clear the search results
+ // once they are restored when the app restarts
+ // or if the app reloads the current video after being killed,
+ // that should be cleared also
+ ExitActivity.Companion.exitAndRemoveFromRecentApps(this)
+ }
+ finishAndRemoveTask()
+ }
+
+ companion object {
+ val PANIC_TRIGGER_ACTION: String = "info.guardianproject.panic.action.TRIGGER"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
deleted file mode 100644
index e6177f6a358..00000000000
--- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.schabi.newpipe;
-
-import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
-import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
-
-import android.content.Context;
-import android.view.ContextThemeWrapper;
-import android.view.View;
-import android.widget.PopupMenu;
-
-import androidx.fragment.app.FragmentManager;
-
-import org.schabi.newpipe.database.stream.model.StreamEntity;
-import org.schabi.newpipe.download.DownloadDialog;
-import org.schabi.newpipe.local.dialog.PlaylistDialog;
-import org.schabi.newpipe.player.playqueue.PlayQueue;
-import org.schabi.newpipe.player.playqueue.PlayQueueItem;
-import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.SparseItemUtil;
-
-import java.util.List;
-
-public final class QueueItemMenuUtil {
- private QueueItemMenuUtil() {
- }
-
- public static void openPopupMenu(final PlayQueue playQueue,
- final PlayQueueItem item,
- final View view,
- final boolean hideDetails,
- final FragmentManager fragmentManager,
- final Context context) {
- final ContextThemeWrapper themeWrapper =
- new ContextThemeWrapper(context, R.style.DarkPopupMenu);
-
- final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
- popupMenu.inflate(R.menu.menu_play_queue_item);
-
- if (hideDetails) {
- popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
- }
-
- popupMenu.setOnMenuItemClickListener(menuItem -> {
- switch (menuItem.getItemId()) {
- case R.id.menu_item_remove:
- final int index = playQueue.indexOf(item);
- playQueue.remove(index);
- return true;
- case R.id.menu_item_details:
- // playQueue is null since we don't want any queue change
- NavigationHelper.openVideoDetail(context, item.getServiceId(),
- item.getUrl(), item.getTitle(), null,
- false);
- return true;
- case R.id.menu_item_append_playlist:
- PlaylistDialog.createCorrespondingDialog(
- context,
- List.of(new StreamEntity(item)),
- dialog -> dialog.show(
- fragmentManager,
- "QueueItemMenuUtil@append_playlist"
- )
- );
-
- return true;
- case R.id.menu_item_channel_details:
- SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
- item.getUrl(), item.getUploaderUrl(),
- // An intent must be used here.
- // Opening with FragmentManager transactions is not working,
- // as PlayQueueActivity doesn't use fragments.
- uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
- context, item.getServiceId(), uploaderUrl, item.getUploader()
- ));
- return true;
- case R.id.menu_item_share:
- shareText(context, item.getTitle(), item.getUrl(),
- item.getThumbnails());
- return true;
- case R.id.menu_item_download:
- fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
- info -> {
- final DownloadDialog downloadDialog = new DownloadDialog(context,
- info);
- downloadDialog.show(fragmentManager, "downloadDialog");
- });
- return true;
- }
- return false;
- });
-
- popupMenu.show();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.kt b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.kt
new file mode 100644
index 00000000000..51ad2d167f7
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.kt
@@ -0,0 +1,97 @@
+package org.schabi.newpipe
+
+import android.content.Context
+import android.view.ContextThemeWrapper
+import android.view.MenuItem
+import android.view.View
+import android.widget.PopupMenu
+import androidx.fragment.app.FragmentManager
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.download.DownloadDialog
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.local.dialog.PlaylistDialog
+import org.schabi.newpipe.player.playqueue.PlayQueue
+import org.schabi.newpipe.player.playqueue.PlayQueueItem
+import org.schabi.newpipe.util.NavigationHelper
+import org.schabi.newpipe.util.SparseItemUtil
+import org.schabi.newpipe.util.external_communication.ShareUtils
+import java.util.List
+import java.util.function.Consumer
+
+object QueueItemMenuUtil {
+ fun openPopupMenu(playQueue: PlayQueue?,
+ item: PlayQueueItem,
+ view: View?,
+ hideDetails: Boolean,
+ fragmentManager: FragmentManager?,
+ context: Context) {
+ val themeWrapper: ContextThemeWrapper = ContextThemeWrapper(context, R.style.DarkPopupMenu)
+ val popupMenu: PopupMenu = PopupMenu(themeWrapper, view)
+ popupMenu.inflate(R.menu.menu_play_queue_item)
+ if (hideDetails) {
+ popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false)
+ }
+ popupMenu.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener({ menuItem: MenuItem ->
+ when (menuItem.getItemId()) {
+ R.id.menu_item_remove -> {
+ val index: Int = playQueue!!.indexOf(item)
+ playQueue.remove(index)
+ return@setOnMenuItemClickListener true
+ }
+
+ R.id.menu_item_details -> {
+ // playQueue is null since we don't want any queue change
+ NavigationHelper.openVideoDetail(context, item.getServiceId(),
+ item.getUrl(), item.getTitle(), null,
+ false)
+ return@setOnMenuItemClickListener true
+ }
+
+ R.id.menu_item_append_playlist -> {
+ PlaylistDialog.Companion.createCorrespondingDialog(
+ context,
+ List.of(StreamEntity(item)),
+ Consumer({ dialog: PlaylistDialog ->
+ dialog.show(
+ (fragmentManager)!!,
+ "QueueItemMenuUtil@append_playlist"
+ )
+ })
+ )
+ return@setOnMenuItemClickListener true
+ }
+
+ R.id.menu_item_channel_details -> {
+ SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
+ item.getUrl(), item.getUploaderUrl(), // An intent must be used here.
+ // Opening with FragmentManager transactions is not working,
+ // as PlayQueueActivity doesn't use fragments.
+ Consumer({ uploaderUrl: String? ->
+ NavigationHelper.openChannelFragmentUsingIntent(
+ context, item.getServiceId(), uploaderUrl, item.getUploader()
+ )
+ }))
+ return@setOnMenuItemClickListener true
+ }
+
+ R.id.menu_item_share -> {
+ ShareUtils.shareText(context, item.getTitle(), item.getUrl(),
+ item.getThumbnails())
+ return@setOnMenuItemClickListener true
+ }
+
+ R.id.menu_item_download -> {
+ SparseItemUtil.fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
+ Consumer({ info: StreamInfo ->
+ val downloadDialog: DownloadDialog = DownloadDialog(context,
+ info)
+ downloadDialog.show((fragmentManager)!!, "downloadDialog")
+ }))
+ return@setOnMenuItemClickListener true
+ }
+ }
+ false
+ }))
+ popupMenu.show()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
deleted file mode 100644
index c59dc753235..00000000000
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ /dev/null
@@ -1,1092 +0,0 @@
-package org.schabi.newpipe;
-
-import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
-import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
-
-import android.annotation.SuppressLint;
-import android.app.IntentService;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.ContextThemeWrapper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.Toast;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.ServiceCompat;
-import androidx.core.math.MathUtils;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.lifecycle.DefaultLifecycleObserver;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.preference.PreferenceManager;
-
-import org.schabi.newpipe.database.stream.model.StreamEntity;
-import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
-import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
-import org.schabi.newpipe.download.DownloadDialog;
-import org.schabi.newpipe.download.LoadingDialog;
-import org.schabi.newpipe.error.ErrorInfo;
-import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.error.ReCaptchaActivity;
-import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.extractor.Info;
-import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.StreamingService;
-import org.schabi.newpipe.extractor.StreamingService.LinkType;
-import org.schabi.newpipe.extractor.channel.ChannelInfo;
-import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
-import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
-import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
-import org.schabi.newpipe.extractor.exceptions.ExtractionException;
-import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
-import org.schabi.newpipe.extractor.exceptions.PaidContentException;
-import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
-import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
-import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
-import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
-import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
-import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.ktx.ExceptionUtils;
-import org.schabi.newpipe.local.dialog.PlaylistDialog;
-import org.schabi.newpipe.player.PlayerType;
-import org.schabi.newpipe.player.helper.PlayerHelper;
-import org.schabi.newpipe.player.helper.PlayerHolder;
-import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
-import org.schabi.newpipe.player.playqueue.PlayQueue;
-import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
-import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
-import org.schabi.newpipe.util.ChannelTabHelper;
-import org.schabi.newpipe.util.Constants;
-import org.schabi.newpipe.util.DeviceUtils;
-import org.schabi.newpipe.util.ExtractorHelper;
-import org.schabi.newpipe.util.Localization;
-import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.PermissionHelper;
-import org.schabi.newpipe.util.ThemeHelper;
-import org.schabi.newpipe.util.external_communication.ShareUtils;
-import org.schabi.newpipe.util.urlfinder.UrlFinder;
-import org.schabi.newpipe.views.FocusOverlayView;
-
-import java.io.Serializable;
-import java.lang.ref.Reference;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Consumer;
-
-import icepick.Icepick;
-import icepick.State;
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.core.Observable;
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.disposables.Disposable;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-
-/**
- * Get the url from the intent and open it in the chosen preferred player.
- */
-public class RouterActivity extends AppCompatActivity {
- protected final CompositeDisposable disposables = new CompositeDisposable();
- @State
- protected int currentServiceId = -1;
- @State
- protected LinkType currentLinkType;
- @State
- protected int selectedRadioPosition = -1;
- protected int selectedPreviously = -1;
- protected String currentUrl;
- private StreamingService currentService;
- private boolean selectionIsDownload = false;
- private boolean selectionIsAddToPlaylist = false;
- private AlertDialog alertDialogChoice = null;
- private FragmentManager.FragmentLifecycleCallbacks dismissListener = null;
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- ThemeHelper.setDayNightMode(this);
- setTheme(ThemeHelper.isLightThemeSelected(this)
- ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
- Localization.assureCorrectAppLanguage(this);
-
- // Pass-through touch events to background activities
- // so that our transparent window won't lock UI in the mean time
- // network request is underway before showing PlaylistDialog or DownloadDialog
- // (ref: https://stackoverflow.com/a/10606141)
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
-
- // Android never fails to impress us with a list of new restrictions per API.
- // Starting with S (Android 12) one of the prerequisite conditions has to be met
- // before the FLAG_NOT_TOUCHABLE flag is allowed to kick in:
- // @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
- // For our present purpose it seems we can just set LayoutParams.alpha to 0
- // on the strength of "4. Fully transparent windows" without affecting the scrim of dialogs
- final WindowManager.LayoutParams params = getWindow().getAttributes();
- params.alpha = 0f;
- getWindow().setAttributes(params);
-
- super.onCreate(savedInstanceState);
- Icepick.restoreInstanceState(this, savedInstanceState);
-
- // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
- // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
- // but those callbacks won't survive a config change
- // Try an alternate approach to hook into FragmentManager instead, to that effect
- // (ref: https://stackoverflow.com/a/44028453)
- final FragmentManager fm = getSupportFragmentManager();
- if (dismissListener == null) {
- dismissListener = new FragmentManager.FragmentLifecycleCallbacks() {
- @Override
- public void onFragmentDestroyed(@NonNull final FragmentManager fm,
- @NonNull final Fragment f) {
- super.onFragmentDestroyed(fm, f);
- if (f instanceof DialogFragment && fm.getFragments().isEmpty()) {
- // No more DialogFragments, we're done
- finish();
- }
- }
- };
- }
- fm.registerFragmentLifecycleCallbacks(dismissListener, false);
-
- if (TextUtils.isEmpty(currentUrl)) {
- currentUrl = getUrl(getIntent());
-
- if (TextUtils.isEmpty(currentUrl)) {
- handleText();
- finish();
- }
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- // we need to dismiss the dialog before leaving the activity or we get leaks
- if (alertDialogChoice != null) {
- alertDialogChoice.dismiss();
- }
- }
-
- @Override
- protected void onSaveInstanceState(@NonNull final Bundle outState) {
- super.onSaveInstanceState(outState);
- Icepick.saveInstanceState(this, outState);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
-
- // Don't overlap the DialogFragment after rotating the screen
- // If there's no DialogFragment, we're either starting afresh
- // or we didn't make it to PlaylistDialog or DownloadDialog before the orientation change
- if (getSupportFragmentManager().getFragments().isEmpty()) {
- // Start over from scratch
- handleUrl(currentUrl);
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (dismissListener != null) {
- getSupportFragmentManager().unregisterFragmentLifecycleCallbacks(dismissListener);
- }
-
- disposables.clear();
- }
-
- @Override
- public void finish() {
- // allow the activity to recreate in case orientation changes
- if (!isChangingConfigurations()) {
- super.finish();
- }
- }
-
- private void handleUrl(final String url) {
- disposables.add(Observable
- .fromCallable(() -> {
- try {
- if (currentServiceId == -1) {
- currentService = NewPipe.getServiceByUrl(url);
- currentServiceId = currentService.getServiceId();
- currentLinkType = currentService.getLinkTypeByUrl(url);
- currentUrl = url;
- } else {
- currentService = NewPipe.getService(currentServiceId);
- }
-
- // return whether the url was found to be supported or not
- return currentLinkType != LinkType.NONE;
- } catch (final ExtractionException e) {
- // this can be reached only when the url is completely unsupported
- return false;
- }
- })
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(isUrlSupported -> {
- if (isUrlSupported) {
- onSuccess();
- } else {
- showUnsupportedUrlDialog(url);
- }
- }, throwable -> handleError(this, new ErrorInfo(throwable,
- UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
- }
-
- /**
- * @param context the context. It will be {@code finish()}ed at the end of the handling if it is
- * an instance of {@link RouterActivity}.
- * @param errorInfo the error information
- */
- private static void handleError(final Context context, final ErrorInfo errorInfo) {
- if (errorInfo.getThrowable() != null) {
- errorInfo.getThrowable().printStackTrace();
- }
-
- if (errorInfo.getThrowable() instanceof ReCaptchaException) {
- Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
- // Starting ReCaptcha Challenge Activity
- final Intent intent = new Intent(context, ReCaptchaActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- } else if (errorInfo.getThrowable() != null
- && ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
- Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
- Toast.makeText(context, R.string.restricted_video_no_stream,
- Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
- Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof PaidContentException) {
- Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof PrivateContentException) {
- Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
- Toast.makeText(context, R.string.soundcloud_go_plus_content,
- Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
- Toast.makeText(context, R.string.youtube_music_premium_content,
- Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
- Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
- } else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
- Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
- } else {
- ErrorUtil.createNotification(context, errorInfo);
- }
-
- if (context instanceof RouterActivity) {
- ((RouterActivity) context).finish();
- }
- }
-
- protected void showUnsupportedUrlDialog(final String url) {
- final Context context = getThemeWrapperContext();
- new AlertDialog.Builder(context)
- .setTitle(R.string.unsupported_url)
- .setMessage(R.string.unsupported_url_dialog_message)
- .setIcon(R.drawable.ic_share)
- .setPositiveButton(R.string.open_in_browser,
- (dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
- .setNegativeButton(R.string.share,
- (dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject
- .setNeutralButton(R.string.cancel, null)
- .setOnDismissListener(dialog -> finish())
- .show();
- }
-
- protected void onSuccess() {
- final SharedPreferences preferences = PreferenceManager
- .getDefaultSharedPreferences(this);
-
- final ChoiceAvailabilityChecker choiceChecker = new ChoiceAvailabilityChecker(
- getChoicesForService(currentService, currentLinkType),
- preferences.getString(getString(R.string.preferred_open_action_key),
- getString(R.string.preferred_open_action_default)));
-
- // Check for non-player related choices
- if (choiceChecker.isAvailableAndSelected(
- R.string.show_info_key,
- R.string.download_key,
- R.string.add_to_playlist_key)) {
- handleChoice(choiceChecker.getSelectedChoiceKey());
- return;
- }
- // Check if the choice is player related
- if (choiceChecker.isAvailableAndSelected(
- R.string.video_player_key,
- R.string.background_player_key,
- R.string.popup_player_key)) {
-
- final String selectedChoice = choiceChecker.getSelectedChoiceKey();
-
- final boolean isExtVideoEnabled = preferences.getBoolean(
- getString(R.string.use_external_video_player_key), false);
- final boolean isExtAudioEnabled = preferences.getBoolean(
- getString(R.string.use_external_audio_player_key), false);
- final boolean isVideoPlayerSelected =
- selectedChoice.equals(getString(R.string.video_player_key))
- || selectedChoice.equals(getString(R.string.popup_player_key));
- final boolean isAudioPlayerSelected =
- selectedChoice.equals(getString(R.string.background_player_key));
-
- if (currentLinkType != LinkType.STREAM
- && ((isExtAudioEnabled && isAudioPlayerSelected)
- || (isExtVideoEnabled && isVideoPlayerSelected))
- ) {
- Toast.makeText(this, R.string.external_player_unsupported_link_type,
- Toast.LENGTH_LONG).show();
- handleChoice(getString(R.string.show_info_key));
- return;
- }
-
- final List capabilities =
- currentService.getServiceInfo().getMediaCapabilities();
-
- // Check if the service supports the choice
- if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
- || (isAudioPlayerSelected && capabilities.contains(AUDIO))) {
- handleChoice(selectedChoice);
- } else {
- handleChoice(getString(R.string.show_info_key));
- }
- return;
- }
-
- // Default / Ask always
- final List availableChoices = choiceChecker.getAvailableChoices();
- switch (availableChoices.size()) {
- case 1:
- handleChoice(availableChoices.get(0).key);
- break;
- case 0:
- handleChoice(getString(R.string.show_info_key));
- break;
- default:
- showDialog(availableChoices);
- break;
- }
- }
-
- /**
- * This is a helper class for checking if the choices are available and/or selected.
- */
- class ChoiceAvailabilityChecker {
- private final List availableChoices;
- private final String selectedChoiceKey;
-
- ChoiceAvailabilityChecker(
- @NonNull final List availableChoices,
- @NonNull final String selectedChoiceKey) {
- this.availableChoices = availableChoices;
- this.selectedChoiceKey = selectedChoiceKey;
- }
-
- public List getAvailableChoices() {
- return availableChoices;
- }
-
- public String getSelectedChoiceKey() {
- return selectedChoiceKey;
- }
-
- public boolean isAvailableAndSelected(@StringRes final int... wantedKeys) {
- return Arrays.stream(wantedKeys).anyMatch(this::isAvailableAndSelected);
- }
-
- public boolean isAvailableAndSelected(@StringRes final int wantedKey) {
- final String wanted = getString(wantedKey);
- // Check if the wanted option is selected
- if (!selectedChoiceKey.equals(wanted)) {
- return false;
- }
- // Check if it's available
- return availableChoices.stream().anyMatch(item -> wanted.equals(item.key));
- }
- }
-
- private void showDialog(final List choices) {
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-
- final Context themeWrapperContext = getThemeWrapperContext();
- final LayoutInflater layoutInflater = LayoutInflater.from(themeWrapperContext);
-
- final SingleChoiceDialogViewBinding binding =
- SingleChoiceDialogViewBinding.inflate(layoutInflater);
- final RadioGroup radioGroup = binding.list;
-
- final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
- final int indexOfChild = radioGroup.indexOfChild(
- radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
- final AdapterChoiceItem choice = choices.get(indexOfChild);
-
- handleChoice(choice.key);
-
- // open future streams always like this one, because "always" button was used by user
- if (which == DialogInterface.BUTTON_POSITIVE) {
- preferences.edit()
- .putString(getString(R.string.preferred_open_action_key), choice.key)
- .apply();
- }
- };
-
- alertDialogChoice = new AlertDialog.Builder(themeWrapperContext)
- .setTitle(R.string.preferred_open_action_share_menu_title)
- .setView(binding.getRoot())
- .setCancelable(true)
- .setNegativeButton(R.string.just_once, dialogButtonsClickListener)
- .setPositiveButton(R.string.always, dialogButtonsClickListener)
- .setOnDismissListener(dialog -> {
- if (!selectionIsDownload && !selectionIsAddToPlaylist) {
- finish();
- }
- })
- .create();
-
- alertDialogChoice.setOnShowListener(dialog -> setDialogButtonsState(
- alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1));
-
- radioGroup.setOnCheckedChangeListener((group, checkedId) ->
- setDialogButtonsState(alertDialogChoice, true));
- final View.OnClickListener radioButtonsClickListener = v -> {
- final int indexOfChild = radioGroup.indexOfChild(v);
- if (indexOfChild == -1) {
- return;
- }
-
- selectedPreviously = selectedRadioPosition;
- selectedRadioPosition = indexOfChild;
-
- if (selectedPreviously == selectedRadioPosition) {
- handleChoice(choices.get(selectedRadioPosition).key);
- }
- };
-
- int id = 12345;
- for (final AdapterChoiceItem item : choices) {
- final RadioButton radioButton = ListRadioIconItemBinding.inflate(layoutInflater)
- .getRoot();
- radioButton.setText(item.description);
- radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
- AppCompatResources.getDrawable(themeWrapperContext, item.icon),
- null, null, null);
- radioButton.setChecked(false);
- radioButton.setId(id++);
- radioButton.setLayoutParams(new RadioGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- radioButton.setOnClickListener(radioButtonsClickListener);
- radioGroup.addView(radioButton);
- }
-
- if (selectedRadioPosition == -1) {
- final String lastSelectedPlayer = preferences.getString(
- getString(R.string.preferred_open_action_last_selected_key), null);
- if (!TextUtils.isEmpty(lastSelectedPlayer)) {
- for (int i = 0; i < choices.size(); i++) {
- final AdapterChoiceItem c = choices.get(i);
- if (lastSelectedPlayer.equals(c.key)) {
- selectedRadioPosition = i;
- break;
- }
- }
- }
- }
-
- selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1);
- if (selectedRadioPosition != -1) {
- ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
- }
- selectedPreviously = selectedRadioPosition;
-
- alertDialogChoice.show();
-
- if (DeviceUtils.isTv(this)) {
- FocusOverlayView.setupFocusObserver(alertDialogChoice);
- }
- }
-
- private List getChoicesForService(final StreamingService service,
- final LinkType linkType) {
- final AdapterChoiceItem showInfo = new AdapterChoiceItem(
- getString(R.string.show_info_key), getString(R.string.show_info),
- R.drawable.ic_info_outline);
- final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
- getString(R.string.video_player_key), getString(R.string.video_player),
- R.drawable.ic_play_arrow);
- final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
- getString(R.string.background_player_key), getString(R.string.background_player),
- R.drawable.ic_headset);
- final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
- getString(R.string.popup_player_key), getString(R.string.popup_player),
- R.drawable.ic_picture_in_picture);
-
- final List returnedItems = new ArrayList<>();
- returnedItems.add(showInfo); // Always present
-
- final List capabilities =
- service.getServiceInfo().getMediaCapabilities();
-
- if (linkType == LinkType.STREAM) {
- if (capabilities.contains(VIDEO)) {
- returnedItems.add(videoPlayer);
- returnedItems.add(popupPlayer);
- }
- if (capabilities.contains(AUDIO)) {
- returnedItems.add(backgroundPlayer);
- }
- // download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is
- // not supported )
- returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key),
- getString(R.string.download),
- R.drawable.ic_file_download));
-
- // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
- // not be added to a playlist
- returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key),
- getString(R.string.add_to_playlist),
- R.drawable.ic_add));
- } else {
- // LinkType.NONE is never present because it's filtered out before
- // channels and playlist can be played as they contain a list of videos
- final SharedPreferences preferences = PreferenceManager
- .getDefaultSharedPreferences(this);
- final boolean isExtVideoEnabled = preferences.getBoolean(
- getString(R.string.use_external_video_player_key), false);
- final boolean isExtAudioEnabled = preferences.getBoolean(
- getString(R.string.use_external_audio_player_key), false);
-
- if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
- returnedItems.add(videoPlayer);
- returnedItems.add(popupPlayer);
- }
- if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
- returnedItems.add(backgroundPlayer);
- }
- }
-
- return returnedItems;
- }
-
- protected Context getThemeWrapperContext() {
- return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this)
- ? R.style.LightTheme : R.style.DarkTheme);
- }
-
- private void setDialogButtonsState(final AlertDialog dialog, final boolean state) {
- final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
- final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (negativeButton == null || positiveButton == null) {
- return;
- }
-
- negativeButton.setEnabled(state);
- positiveButton.setEnabled(state);
- }
-
- private void handleText() {
- final String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
- final int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
- final Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString);
- }
-
- private void handleChoice(final String selectedChoiceKey) {
- final List validChoicesList = Arrays.asList(getResources()
- .getStringArray(R.array.preferred_open_action_values_list));
- if (validChoicesList.contains(selectedChoiceKey)) {
- PreferenceManager.getDefaultSharedPreferences(this).edit()
- .putString(getString(
- R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
- .apply();
- }
-
- if (selectedChoiceKey.equals(getString(R.string.popup_player_key))
- && !PermissionHelper.isPopupEnabledElseAsk(this)) {
- finish();
- return;
- }
-
- if (selectedChoiceKey.equals(getString(R.string.download_key))) {
- if (PermissionHelper.checkStoragePermissions(this,
- PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
- selectionIsDownload = true;
- openDownloadDialog();
- }
- return;
- }
-
- if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
- selectionIsAddToPlaylist = true;
- openAddToPlaylistDialog();
- return;
- }
-
- // stop and bypass FetcherService if InfoScreen was selected since
- // StreamDetailFragment can fetch data itself
- if (selectedChoiceKey.equals(getString(R.string.show_info_key))
- || canHandleChoiceLikeShowInfo(selectedChoiceKey)) {
- disposables.add(Observable
- .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(intent -> {
- startActivity(intent);
- finish();
- }, throwable -> handleError(this, new ErrorInfo(throwable,
- UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
- );
- return;
- }
-
- final Intent intent = new Intent(this, FetcherService.class);
- final Choice choice = new Choice(currentService.getServiceId(), currentLinkType,
- currentUrl, selectedChoiceKey);
- intent.putExtra(FetcherService.KEY_CHOICE, choice);
- startService(intent);
-
- finish();
- }
-
- private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) {
- if (!selectedChoiceKey.equals(getString(R.string.video_player_key))) {
- return false;
- }
- // "video player" can be handled like "show info" (because VideoDetailFragment can load
- // the stream instead of FetcherService) when...
-
- // ...Autoplay is enabled
- if (!PlayerHelper.isAutoplayAllowedByUser(getThemeWrapperContext())) {
- return false;
- }
-
- final boolean isExtVideoEnabled = PreferenceManager.getDefaultSharedPreferences(this)
- .getBoolean(getString(R.string.use_external_video_player_key), false);
- // ...it's not done via an external player
- if (isExtVideoEnabled) {
- return false;
- }
-
- // ...the player is not running or in normal Video-mode/type
- final PlayerType playerType = PlayerHolder.getInstance().getType();
- return playerType == null || playerType == PlayerType.MAIN;
- }
-
- public static class PersistentFragment extends Fragment {
- private WeakReference weakContext;
- private final CompositeDisposable disposables = new CompositeDisposable();
- private int running = 0;
-
- private synchronized void inFlight(final boolean started) {
- if (started) {
- running++;
- } else {
- running--;
- if (running <= 0) {
- getActivityContext().ifPresent(context -> context.getSupportFragmentManager()
- .beginTransaction().remove(this).commit());
- }
- }
- }
-
- @Override
- public void onAttach(@NonNull final Context activityContext) {
- super.onAttach(activityContext);
- weakContext = new WeakReference<>((AppCompatActivity) activityContext);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- weakContext = null;
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- disposables.clear();
- }
-
- /**
- * @return the activity context, if there is one and the activity is not finishing
- */
- private Optional getActivityContext() {
- return Optional.ofNullable(weakContext)
- .map(Reference::get)
- .filter(context -> !context.isFinishing());
- }
-
- // guard against IllegalStateException in calling DialogFragment.show() whilst in background
- // (which could happen, say, when the user pressed the home button while waiting for
- // the network request to return) when it internally calls FragmentTransaction.commit()
- // after the FragmentManager has saved its states (isStateSaved() == true)
- // (ref: https://stackoverflow.com/a/39813506)
- private void runOnVisible(final Consumer runnable) {
- getActivityContext().ifPresentOrElse(context -> {
- if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
- context.runOnUiThread(() -> {
- runnable.accept(context);
- inFlight(false);
- });
- } else {
- getLifecycle().addObserver(new DefaultLifecycleObserver() {
- @Override
- public void onResume(@NonNull final LifecycleOwner owner) {
- getLifecycle().removeObserver(this);
- getActivityContext().ifPresentOrElse(context ->
- context.runOnUiThread(() -> {
- runnable.accept(context);
- inFlight(false);
- }),
- () -> inFlight(false)
- );
- }
- });
- // this trick doesn't seem to work on Android 10+ (API 29)
- // which places restrictions on starting activities from the background
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
- && !context.isChangingConfigurations()) {
- // try to bring the activity back to front if minimised
- final Intent i = new Intent(context, RouterActivity.class);
- i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- startActivity(i);
- }
- }
-
- }, () ->
- // this branch is executed if there is no activity context
- inFlight(false)
- );
- }
-
- Single pleaseWait(final Single single) {
- // 'abuse' ambWith() here to cancel the toast for us when the wait is over
- return single.ambWith(Single.create(emitter -> getActivityContext().ifPresent(context ->
- context.runOnUiThread(() -> {
- // Getting the stream info usually takes a moment
- // Notifying the user here to ensure that no confusion arises
- final Toast toast = Toast.makeText(context,
- getString(R.string.processing_may_take_a_moment),
- Toast.LENGTH_LONG);
- toast.show();
- emitter.setCancellable(toast::cancel);
- }))));
- }
-
- @SuppressLint("CheckResult")
- private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
- inFlight(true);
- final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
- loadingDialog.show(getParentFragmentManager(), "loadingDialog");
- disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .compose(this::pleaseWait)
- .subscribe(result ->
- runOnVisible(ctx -> {
- loadingDialog.dismiss();
- final FragmentManager fm = ctx.getSupportFragmentManager();
- final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
- // dismiss listener to be handled by FragmentManager
- downloadDialog.show(fm, "downloadDialog");
- }
- ), throwable -> runOnVisible(ctx -> {
- loadingDialog.dismiss();
- ((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
- })));
- }
-
- private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
- inFlight(true);
- disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .compose(this::pleaseWait)
- .subscribe(
- info -> getActivityContext().ifPresent(context ->
- PlaylistDialog.createCorrespondingDialog(context,
- List.of(new StreamEntity(info)),
- playlistDialog -> runOnVisible(ctx -> {
- // dismiss listener to be handled by FragmentManager
- final FragmentManager fm =
- ctx.getSupportFragmentManager();
- playlistDialog.show(fm, "addToPlaylistDialog");
- })
- )),
- throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
- throwable,
- UserAction.REQUESTED_STREAM,
- "Tried to add " + currentUrl + " to a playlist",
- ((RouterActivity) ctx).currentService.getServiceId())
- ))
- )
- );
- }
- }
-
- private void openAddToPlaylistDialog() {
- getPersistFragment().openAddToPlaylistDialog(currentServiceId, currentUrl);
- }
-
- private void openDownloadDialog() {
- getPersistFragment().openDownloadDialog(currentServiceId, currentUrl);
- }
-
- private PersistentFragment getPersistFragment() {
- final FragmentManager fm = getSupportFragmentManager();
- PersistentFragment persistFragment =
- (PersistentFragment) fm.findFragmentByTag("PERSIST_FRAGMENT");
- if (persistFragment == null) {
- persistFragment = new PersistentFragment();
- fm.beginTransaction()
- .add(persistFragment, "PERSIST_FRAGMENT")
- .commitNow();
- }
- return persistFragment;
- }
-
- @Override
- public void onRequestPermissionsResult(final int requestCode,
- @NonNull final String[] permissions,
- @NonNull final int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- for (final int i : grantResults) {
- if (i == PackageManager.PERMISSION_DENIED) {
- finish();
- return;
- }
- }
- if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) {
- openDownloadDialog();
- }
- }
-
- private static class AdapterChoiceItem {
- final String description;
- final String key;
- @DrawableRes
- final int icon;
-
- AdapterChoiceItem(final String key, final String description, final int icon) {
- this.key = key;
- this.description = description;
- this.icon = icon;
- }
- }
-
- private static class Choice implements Serializable {
- final int serviceId;
- final String url;
- final String playerChoice;
- final LinkType linkType;
-
- Choice(final int serviceId, final LinkType linkType,
- final String url, final String playerChoice) {
- this.serviceId = serviceId;
- this.linkType = linkType;
- this.url = url;
- this.playerChoice = playerChoice;
- }
-
- @NonNull
- @Override
- public String toString() {
- return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
- }
- }
-
- public static class FetcherService extends IntentService {
-
- public static final String KEY_CHOICE = "key_choice";
- private static final int ID = 456;
- private Disposable fetcher;
-
- public FetcherService() {
- super(FetcherService.class.getSimpleName());
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- startForeground(ID, createNotification().build());
- }
-
- @Override
- protected void onHandleIntent(@Nullable final Intent intent) {
- if (intent == null) {
- return;
- }
-
- final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
- if (!(serializable instanceof Choice)) {
- return;
- }
- final Choice playerChoice = (Choice) serializable;
- handleChoice(playerChoice);
- }
-
- public void handleChoice(final Choice choice) {
- Single extends Info> single = null;
- UserAction userAction = UserAction.SOMETHING_ELSE;
-
- switch (choice.linkType) {
- case STREAM:
- single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_STREAM;
- break;
- case CHANNEL:
- single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_CHANNEL;
- break;
- case PLAYLIST:
- single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_PLAYLIST;
- break;
- }
-
-
- if (single != null) {
- final UserAction finalUserAction = userAction;
- final Consumer resultHandler = getResultHandler(choice);
- fetcher = single
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(info -> {
- resultHandler.accept(info);
- if (fetcher != null) {
- fetcher.dispose();
- }
- }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
- choice.url + " opened with " + choice.playerChoice,
- choice.serviceId)));
- }
- }
-
- public Consumer getResultHandler(final Choice choice) {
- return info -> {
- final String videoPlayerKey = getString(R.string.video_player_key);
- final String backgroundPlayerKey = getString(R.string.background_player_key);
- final String popupPlayerKey = getString(R.string.popup_player_key);
-
- final SharedPreferences preferences = PreferenceManager
- .getDefaultSharedPreferences(this);
- final boolean isExtVideoEnabled = preferences.getBoolean(
- getString(R.string.use_external_video_player_key), false);
- final boolean isExtAudioEnabled = preferences.getBoolean(
- getString(R.string.use_external_audio_player_key), false);
-
- final PlayQueue playQueue;
- if (info instanceof StreamInfo) {
- if (choice.playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
- NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
- return;
- } else if (choice.playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
- NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
- return;
- }
- playQueue = new SinglePlayQueue((StreamInfo) info);
- } else if (info instanceof ChannelInfo) {
- final Optional playableTab = ((ChannelInfo) info).getTabs()
- .stream()
- .filter(ChannelTabHelper::isStreamsTab)
- .findFirst();
-
- if (playableTab.isPresent()) {
- playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
- } else {
- return; // there is no playable tab
- }
- } else if (info instanceof PlaylistInfo) {
- playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
- } else {
- return;
- }
-
- if (choice.playerChoice.equals(videoPlayerKey)) {
- NavigationHelper.playOnMainPlayer(this, playQueue, false);
- } else if (choice.playerChoice.equals(backgroundPlayerKey)) {
- NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
- } else if (choice.playerChoice.equals(popupPlayerKey)) {
- NavigationHelper.playOnPopupPlayer(this, playQueue, true);
- }
- };
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
- if (fetcher != null) {
- fetcher.dispose();
- }
- }
-
- private NotificationCompat.Builder createNotification() {
- return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
- .setOngoing(true)
- .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setContentTitle(
- getString(R.string.preferred_player_fetcher_notification_title))
- .setContentText(
- getString(R.string.preferred_player_fetcher_notification_message));
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- @Nullable
- private String getUrl(final Intent intent) {
- String foundUrl = null;
- if (intent.getData() != null) {
- // Called from another app
- foundUrl = intent.getData().toString();
- } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
- // Called from the share menu
- final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
- foundUrl = UrlFinder.firstUrlFromInput(extraText);
- }
-
- return foundUrl;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.kt b/app/src/main/java/org/schabi/newpipe/RouterActivity.kt
new file mode 100644
index 00000000000..57d49b1f527
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.kt
@@ -0,0 +1,1002 @@
+package org.schabi.newpipe
+
+import android.annotation.SuppressLint
+import android.app.IntentService
+import android.content.Context
+import android.content.DialogInterface
+import android.content.DialogInterface.OnShowListener
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.RadioButton
+import android.widget.RadioGroup
+import android.widget.Toast
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.math.MathUtils
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.Lifecycle.State.isAtLeast
+import androidx.lifecycle.Lifecycle.addObserver
+import androidx.lifecycle.Lifecycle.currentState
+import androidx.lifecycle.Lifecycle.removeObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.preference.PreferenceManager
+import icepick.Icepick
+import icepick.State
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.Observable
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.core.SingleEmitter
+import io.reactivex.rxjava3.core.SingleOnSubscribe
+import io.reactivex.rxjava3.core.SingleTransformer
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.disposables.Disposable
+import io.reactivex.rxjava3.functions.Cancellable
+import io.reactivex.rxjava3.schedulers.Schedulers
+import org.schabi.newpipe.RouterActivity.FetcherService
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.databinding.ListRadioIconItemBinding
+import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding
+import org.schabi.newpipe.download.DownloadDialog
+import org.schabi.newpipe.download.LoadingDialog
+import org.schabi.newpipe.error.ErrorInfo
+import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
+import org.schabi.newpipe.error.ReCaptchaActivity
+import org.schabi.newpipe.error.UserAction
+import org.schabi.newpipe.extractor.Info
+import org.schabi.newpipe.extractor.NewPipe
+import org.schabi.newpipe.extractor.StreamingService
+import org.schabi.newpipe.extractor.StreamingService.LinkType
+import org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability
+import org.schabi.newpipe.extractor.channel.ChannelInfo
+import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
+import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
+import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
+import org.schabi.newpipe.extractor.exceptions.ExtractionException
+import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
+import org.schabi.newpipe.extractor.exceptions.PaidContentException
+import org.schabi.newpipe.extractor.exceptions.PrivateContentException
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
+import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
+import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
+import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
+import org.schabi.newpipe.extractor.playlist.PlaylistInfo
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.ktx.isNetworkRelated
+import org.schabi.newpipe.local.dialog.PlaylistDialog
+import org.schabi.newpipe.player.PlayerType
+import org.schabi.newpipe.player.helper.PlayerHelper
+import org.schabi.newpipe.player.helper.PlayerHolder
+import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue
+import org.schabi.newpipe.player.playqueue.PlayQueue
+import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
+import org.schabi.newpipe.player.playqueue.SinglePlayQueue
+import org.schabi.newpipe.util.ChannelTabHelper
+import org.schabi.newpipe.util.DeviceUtils
+import org.schabi.newpipe.util.ExtractorHelper
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.NavigationHelper
+import org.schabi.newpipe.util.PermissionHelper
+import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.util.external_communication.ShareUtils
+import org.schabi.newpipe.util.urlfinder.UrlFinder.Companion.firstUrlFromInput
+import org.schabi.newpipe.views.FocusOverlayView
+import java.io.Serializable
+import java.lang.ref.WeakReference
+import java.util.Arrays
+import java.util.Optional
+import java.util.concurrent.Callable
+import java.util.function.Function
+import java.util.function.IntPredicate
+import java.util.function.Predicate
+
+/**
+ * Get the url from the intent and open it in the chosen preferred player.
+ */
+class RouterActivity() : AppCompatActivity() {
+ protected val disposables: CompositeDisposable = CompositeDisposable()
+
+ @State
+ protected var currentServiceId: Int = -1
+
+ @State
+ protected var currentLinkType: LinkType? = null
+
+ @State
+ protected var selectedRadioPosition: Int = -1
+ protected var selectedPreviously: Int = -1
+ protected var currentUrl: String? = null
+ private var currentService: StreamingService? = null
+ private var selectionIsDownload: Boolean = false
+ private var selectionIsAddToPlaylist: Boolean = false
+ private var alertDialogChoice: AlertDialog? = null
+ private var dismissListener: FragmentManager.FragmentLifecycleCallbacks? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeHelper.setDayNightMode(this)
+ setTheme(if (ThemeHelper.isLightThemeSelected(this)) R.style.RouterActivityThemeLight else R.style.RouterActivityThemeDark)
+ Localization.assureCorrectAppLanguage(this)
+
+ // Pass-through touch events to background activities
+ // so that our transparent window won't lock UI in the mean time
+ // network request is underway before showing PlaylistDialog or DownloadDialog
+ // (ref: https://stackoverflow.com/a/10606141)
+ getWindow().addFlags((WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE))
+
+ // Android never fails to impress us with a list of new restrictions per API.
+ // Starting with S (Android 12) one of the prerequisite conditions has to be met
+ // before the FLAG_NOT_TOUCHABLE flag is allowed to kick in:
+ // @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
+ // For our present purpose it seems we can just set LayoutParams.alpha to 0
+ // on the strength of "4. Fully transparent windows" without affecting the scrim of dialogs
+ val params: WindowManager.LayoutParams = getWindow().getAttributes()
+ params.alpha = 0f
+ getWindow().setAttributes(params)
+ super.onCreate(savedInstanceState)
+ Icepick.restoreInstanceState(this, savedInstanceState)
+
+ // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
+ // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
+ // but those callbacks won't survive a config change
+ // Try an alternate approach to hook into FragmentManager instead, to that effect
+ // (ref: https://stackoverflow.com/a/44028453)
+ val fm: FragmentManager = getSupportFragmentManager()
+ if (dismissListener == null) {
+ dismissListener = object : FragmentManager.FragmentLifecycleCallbacks() {
+ public override fun onFragmentDestroyed(fm: FragmentManager,
+ f: Fragment) {
+ super.onFragmentDestroyed(fm, f)
+ if (f is DialogFragment && fm.getFragments().isEmpty()) {
+ // No more DialogFragments, we're done
+ finish()
+ }
+ }
+ }
+ }
+ fm.registerFragmentLifecycleCallbacks(dismissListener!!, false)
+ if (TextUtils.isEmpty(currentUrl)) {
+ currentUrl = getUrl(getIntent())
+ if (TextUtils.isEmpty(currentUrl)) {
+ handleText()
+ finish()
+ }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ // we need to dismiss the dialog before leaving the activity or we get leaks
+ if (alertDialogChoice != null) {
+ alertDialogChoice!!.dismiss()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ Icepick.saveInstanceState(this, outState)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ // Don't overlap the DialogFragment after rotating the screen
+ // If there's no DialogFragment, we're either starting afresh
+ // or we didn't make it to PlaylistDialog or DownloadDialog before the orientation change
+ if (getSupportFragmentManager().getFragments().isEmpty()) {
+ // Start over from scratch
+ handleUrl(currentUrl)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (dismissListener != null) {
+ getSupportFragmentManager().unregisterFragmentLifecycleCallbacks(dismissListener!!)
+ }
+ disposables.clear()
+ }
+
+ public override fun finish() {
+ // allow the activity to recreate in case orientation changes
+ if (!isChangingConfigurations()) {
+ super.finish()
+ }
+ }
+
+ private fun handleUrl(url: String?) {
+ disposables.add(Observable
+ .fromCallable(Callable({
+ try {
+ if (currentServiceId == -1) {
+ currentService = NewPipe.getServiceByUrl(url)
+ currentServiceId = currentService.getServiceId()
+ currentLinkType = currentService.getLinkTypeByUrl(url)
+ currentUrl = url
+ } else {
+ currentService = NewPipe.getService(currentServiceId)
+ }
+
+ // return whether the url was found to be supported or not
+ return@fromCallable currentLinkType != LinkType.NONE
+ } catch (e: ExtractionException) {
+ // this can be reached only when the url is completely unsupported
+ return@fromCallable false
+ }
+ }))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(io.reactivex.rxjava3.functions.Consumer({ isUrlSupported: Boolean ->
+ if (isUrlSupported) {
+ onSuccess()
+ } else {
+ showUnsupportedUrlDialog(url)
+ }
+ }), io.reactivex.rxjava3.functions.Consumer({ throwable: Throwable? ->
+ handleError(this, ErrorInfo((throwable)!!,
+ UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))
+ })))
+ }
+
+ protected fun showUnsupportedUrlDialog(url: String?) {
+ val context: Context = getThemeWrapperContext()
+ AlertDialog.Builder(context)
+ .setTitle(R.string.unsupported_url)
+ .setMessage(R.string.unsupported_url_dialog_message)
+ .setIcon(R.drawable.ic_share)
+ .setPositiveButton(R.string.open_in_browser,
+ DialogInterface.OnClickListener({ dialog: DialogInterface?, which: Int -> ShareUtils.openUrlInBrowser(this, url) }))
+ .setNegativeButton(R.string.share,
+ DialogInterface.OnClickListener({ dialog: DialogInterface?, which: Int -> shareText(this, "", url) })) // no subject
+ .setNeutralButton(R.string.cancel, null)
+ .setOnDismissListener(DialogInterface.OnDismissListener({ dialog: DialogInterface? -> finish() }))
+ .show()
+ }
+
+ protected fun onSuccess() {
+ val preferences: SharedPreferences = PreferenceManager
+ .getDefaultSharedPreferences(this)
+ val choiceChecker: ChoiceAvailabilityChecker = ChoiceAvailabilityChecker(
+ getChoicesForService(currentService, currentLinkType),
+ (preferences.getString(getString(R.string.preferred_open_action_key),
+ getString(R.string.preferred_open_action_default)))!!)
+
+ // Check for non-player related choices
+ if (choiceChecker.isAvailableAndSelected(
+ R.string.show_info_key,
+ R.string.download_key,
+ R.string.add_to_playlist_key)) {
+ handleChoice(choiceChecker.getSelectedChoiceKey())
+ return
+ }
+ // Check if the choice is player related
+ if (choiceChecker.isAvailableAndSelected(
+ R.string.video_player_key,
+ R.string.background_player_key,
+ R.string.popup_player_key)) {
+ val selectedChoice: String = choiceChecker.getSelectedChoiceKey()
+ val isExtVideoEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false)
+ val isExtAudioEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false)
+ val isVideoPlayerSelected: Boolean = ((selectedChoice == getString(R.string.video_player_key)) || (selectedChoice == getString(R.string.popup_player_key)))
+ val isAudioPlayerSelected: Boolean = (selectedChoice == getString(R.string.background_player_key))
+ if ((currentLinkType != LinkType.STREAM
+ && ((isExtAudioEnabled && isAudioPlayerSelected)
+ || (isExtVideoEnabled && isVideoPlayerSelected)))) {
+ Toast.makeText(this, R.string.external_player_unsupported_link_type,
+ Toast.LENGTH_LONG).show()
+ handleChoice(getString(R.string.show_info_key))
+ return
+ }
+ val capabilities: List = currentService!!.getServiceInfo().getMediaCapabilities()
+
+ // Check if the service supports the choice
+ if (((isVideoPlayerSelected && capabilities.contains(MediaCapability.VIDEO))
+ || (isAudioPlayerSelected && capabilities.contains(MediaCapability.AUDIO)))) {
+ handleChoice(selectedChoice)
+ } else {
+ handleChoice(getString(R.string.show_info_key))
+ }
+ return
+ }
+
+ // Default / Ask always
+ val availableChoices: List = choiceChecker.getAvailableChoices()
+ when (availableChoices.size) {
+ 1 -> handleChoice(availableChoices.get(0).key)
+ 0 -> handleChoice(getString(R.string.show_info_key))
+ else -> showDialog(availableChoices)
+ }
+ }
+
+ /**
+ * This is a helper class for checking if the choices are available and/or selected.
+ */
+ internal inner class ChoiceAvailabilityChecker(
+ private val availableChoices: List,
+ private val selectedChoiceKey: String) {
+ fun getAvailableChoices(): List {
+ return availableChoices
+ }
+
+ fun getSelectedChoiceKey(): String {
+ return selectedChoiceKey
+ }
+
+ fun isAvailableAndSelected(@StringRes vararg wantedKeys: Int): Boolean {
+ return Arrays.stream(wantedKeys).anyMatch(IntPredicate({ wantedKey: Int -> this.isAvailableAndSelected(wantedKey) }))
+ }
+
+ fun isAvailableAndSelected(@StringRes wantedKey: Int): Boolean {
+ val wanted: String = getString(wantedKey)
+ // Check if the wanted option is selected
+ if (!(selectedChoiceKey == wanted)) {
+ return false
+ }
+ // Check if it's available
+ return availableChoices.stream().anyMatch(Predicate({ item: AdapterChoiceItem -> (wanted == item.key) }))
+ }
+ }
+
+ private fun showDialog(choices: List) {
+ val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ val themeWrapperContext: Context = getThemeWrapperContext()
+ val layoutInflater: LayoutInflater = LayoutInflater.from(themeWrapperContext)
+ val binding: SingleChoiceDialogViewBinding = SingleChoiceDialogViewBinding.inflate(layoutInflater)
+ val radioGroup: RadioGroup = binding.list
+ val dialogButtonsClickListener: DialogInterface.OnClickListener = DialogInterface.OnClickListener({ dialog: DialogInterface?, which: Int ->
+ val indexOfChild: Int = radioGroup.indexOfChild(
+ radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()))
+ val choice: AdapterChoiceItem = choices.get(indexOfChild)
+ handleChoice(choice.key)
+
+ // open future streams always like this one, because "always" button was used by user
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ preferences.edit()
+ .putString(getString(R.string.preferred_open_action_key), choice.key)
+ .apply()
+ }
+ })
+ alertDialogChoice = AlertDialog.Builder(themeWrapperContext)
+ .setTitle(R.string.preferred_open_action_share_menu_title)
+ .setView(binding.getRoot())
+ .setCancelable(true)
+ .setNegativeButton(R.string.just_once, dialogButtonsClickListener)
+ .setPositiveButton(R.string.always, dialogButtonsClickListener)
+ .setOnDismissListener(DialogInterface.OnDismissListener({ dialog: DialogInterface? ->
+ if (!selectionIsDownload && !selectionIsAddToPlaylist) {
+ finish()
+ }
+ }))
+ .create()
+ alertDialogChoice!!.setOnShowListener(OnShowListener({ dialog: DialogInterface? ->
+ setDialogButtonsState(
+ alertDialogChoice!!, radioGroup.getCheckedRadioButtonId() != -1)
+ }))
+ radioGroup.setOnCheckedChangeListener(RadioGroup.OnCheckedChangeListener({ group: RadioGroup?, checkedId: Int -> setDialogButtonsState(alertDialogChoice!!, true) }))
+ val radioButtonsClickListener: View.OnClickListener = View.OnClickListener({ v: View? ->
+ val indexOfChild: Int = radioGroup.indexOfChild(v)
+ if (indexOfChild == -1) {
+ return@OnClickListener
+ }
+ selectedPreviously = selectedRadioPosition
+ selectedRadioPosition = indexOfChild
+ if (selectedPreviously == selectedRadioPosition) {
+ handleChoice(choices.get(selectedRadioPosition).key)
+ }
+ })
+ var id: Int = 12345
+ for (item: AdapterChoiceItem in choices) {
+ val radioButton: RadioButton = ListRadioIconItemBinding.inflate(layoutInflater)
+ .getRoot()
+ radioButton.setText(item.description)
+ radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ AppCompatResources.getDrawable(themeWrapperContext, item.icon),
+ null, null, null)
+ radioButton.setChecked(false)
+ radioButton.setId(id++)
+ radioButton.setLayoutParams(RadioGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
+ radioButton.setOnClickListener(radioButtonsClickListener)
+ radioGroup.addView(radioButton)
+ }
+ if (selectedRadioPosition == -1) {
+ val lastSelectedPlayer: String? = preferences.getString(
+ getString(R.string.preferred_open_action_last_selected_key), null)
+ if (!TextUtils.isEmpty(lastSelectedPlayer)) {
+ for (i in choices.indices) {
+ val c: AdapterChoiceItem = choices.get(i)
+ if ((lastSelectedPlayer == c.key)) {
+ selectedRadioPosition = i
+ break
+ }
+ }
+ }
+ }
+ selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size - 1)
+ if (selectedRadioPosition != -1) {
+ (radioGroup.getChildAt(selectedRadioPosition) as RadioButton).setChecked(true)
+ }
+ selectedPreviously = selectedRadioPosition
+ alertDialogChoice!!.show()
+ if (DeviceUtils.isTv(this)) {
+ FocusOverlayView.Companion.setupFocusObserver(alertDialogChoice!!)
+ }
+ }
+
+ private fun getChoicesForService(service: StreamingService?,
+ linkType: LinkType?): List {
+ val showInfo: AdapterChoiceItem = AdapterChoiceItem(
+ getString(R.string.show_info_key), getString(R.string.show_info),
+ R.drawable.ic_info_outline)
+ val videoPlayer: AdapterChoiceItem = AdapterChoiceItem(
+ getString(R.string.video_player_key), getString(R.string.video_player),
+ R.drawable.ic_play_arrow)
+ val backgroundPlayer: AdapterChoiceItem = AdapterChoiceItem(
+ getString(R.string.background_player_key), getString(R.string.background_player),
+ R.drawable.ic_headset)
+ val popupPlayer: AdapterChoiceItem = AdapterChoiceItem(
+ getString(R.string.popup_player_key), getString(R.string.popup_player),
+ R.drawable.ic_picture_in_picture)
+ val returnedItems: MutableList = ArrayList()
+ returnedItems.add(showInfo) // Always present
+ val capabilities: List = service!!.getServiceInfo().getMediaCapabilities()
+ if (linkType == LinkType.STREAM) {
+ if (capabilities.contains(MediaCapability.VIDEO)) {
+ returnedItems.add(videoPlayer)
+ returnedItems.add(popupPlayer)
+ }
+ if (capabilities.contains(MediaCapability.AUDIO)) {
+ returnedItems.add(backgroundPlayer)
+ }
+ // download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is
+ // not supported )
+ returnedItems.add(AdapterChoiceItem(getString(R.string.download_key),
+ getString(R.string.download),
+ R.drawable.ic_file_download))
+
+ // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
+ // not be added to a playlist
+ returnedItems.add(AdapterChoiceItem(getString(R.string.add_to_playlist_key),
+ getString(R.string.add_to_playlist),
+ R.drawable.ic_add))
+ } else {
+ // LinkType.NONE is never present because it's filtered out before
+ // channels and playlist can be played as they contain a list of videos
+ val preferences: SharedPreferences = PreferenceManager
+ .getDefaultSharedPreferences(this)
+ val isExtVideoEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false)
+ val isExtAudioEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false)
+ if (capabilities.contains(MediaCapability.VIDEO) && !isExtVideoEnabled) {
+ returnedItems.add(videoPlayer)
+ returnedItems.add(popupPlayer)
+ }
+ if (capabilities.contains(MediaCapability.AUDIO) && !isExtAudioEnabled) {
+ returnedItems.add(backgroundPlayer)
+ }
+ }
+ return returnedItems
+ }
+
+ protected fun getThemeWrapperContext(): Context {
+ return ContextThemeWrapper(this, if (ThemeHelper.isLightThemeSelected(this)) R.style.LightTheme else R.style.DarkTheme)
+ }
+
+ private fun setDialogButtonsState(dialog: AlertDialog, state: Boolean) {
+ val negativeButton: Button? = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+ val positiveButton: Button? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+ if (negativeButton == null || positiveButton == null) {
+ return
+ }
+ negativeButton.setEnabled(state)
+ positiveButton.setEnabled(state)
+ }
+
+ private fun handleText() {
+ val searchString: String? = getIntent().getStringExtra(Intent.EXTRA_TEXT)
+ val serviceId: Int = getIntent().getIntExtra(KEY_SERVICE_ID, 0)
+ val intent: Intent = Intent(getThemeWrapperContext(), MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString)
+ }
+
+ private fun handleChoice(selectedChoiceKey: String) {
+ val validChoicesList: List = Arrays.asList(*getResources()
+ .getStringArray(R.array.preferred_open_action_values_list))
+ if (validChoicesList.contains(selectedChoiceKey)) {
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putString(getString(
+ R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
+ .apply()
+ }
+ if (((selectedChoiceKey == getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabledElseAsk(this))) {
+ finish()
+ return
+ }
+ if ((selectedChoiceKey == getString(R.string.download_key))) {
+ if (PermissionHelper.checkStoragePermissions(this,
+ PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+ selectionIsDownload = true
+ openDownloadDialog()
+ }
+ return
+ }
+ if ((selectedChoiceKey == getString(R.string.add_to_playlist_key))) {
+ selectionIsAddToPlaylist = true
+ openAddToPlaylistDialog()
+ return
+ }
+
+ // stop and bypass FetcherService if InfoScreen was selected since
+ // StreamDetailFragment can fetch data itself
+ if (((selectedChoiceKey == getString(R.string.show_info_key)) || canHandleChoiceLikeShowInfo(selectedChoiceKey))) {
+ disposables.add(Observable
+ .fromCallable(Callable({ NavigationHelper.getIntentByLink(this, currentUrl) }))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(io.reactivex.rxjava3.functions.Consumer({ intent: Intent? ->
+ startActivity(intent)
+ finish()
+ }), io.reactivex.rxjava3.functions.Consumer({ throwable: Throwable? ->
+ handleError(this, ErrorInfo((throwable)!!,
+ UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl))
+ }))
+ )
+ return
+ }
+ val intent: Intent = Intent(this, FetcherService::class.java)
+ val choice: Choice = Choice(currentService!!.getServiceId(), currentLinkType,
+ currentUrl, selectedChoiceKey)
+ intent.putExtra(FetcherService.KEY_CHOICE, choice)
+ startService(intent)
+ finish()
+ }
+
+ private fun canHandleChoiceLikeShowInfo(selectedChoiceKey: String): Boolean {
+ if (!(selectedChoiceKey == getString(R.string.video_player_key))) {
+ return false
+ }
+ // "video player" can be handled like "show info" (because VideoDetailFragment can load
+ // the stream instead of FetcherService) when...
+
+ // ...Autoplay is enabled
+ if (!PlayerHelper.isAutoplayAllowedByUser(getThemeWrapperContext())) {
+ return false
+ }
+ val isExtVideoEnabled: Boolean = PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean(getString(R.string.use_external_video_player_key), false)
+ // ...it's not done via an external player
+ if (isExtVideoEnabled) {
+ return false
+ }
+
+ // ...the player is not running or in normal Video-mode/type
+ val playerType: PlayerType? = PlayerHolder.Companion.getInstance().getType()
+ return playerType == null || playerType == PlayerType.MAIN
+ }
+
+ class PersistentFragment() : Fragment() {
+ private var weakContext: WeakReference? = null
+ private val disposables: CompositeDisposable = CompositeDisposable()
+ private var running: Int = 0
+ @Synchronized
+ private fun inFlight(started: Boolean) {
+ if (started) {
+ running++
+ } else {
+ running--
+ if (running <= 0) {
+ getActivityContext().ifPresent(java.util.function.Consumer({ context: AppCompatActivity? ->
+ context!!.getSupportFragmentManager()
+ .beginTransaction().remove(this).commit()
+ }))
+ }
+ }
+ }
+
+ public override fun onAttach(activityContext: Context) {
+ super.onAttach(activityContext)
+ weakContext = WeakReference(activityContext as AppCompatActivity)
+ }
+
+ public override fun onDetach() {
+ super.onDetach()
+ weakContext = null
+ }
+
+ @Suppress("deprecation")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setRetainInstance(true)
+ }
+
+ public override fun onDestroy() {
+ super.onDestroy()
+ disposables.clear()
+ }
+
+ /**
+ * @return the activity context, if there is one and the activity is not finishing
+ */
+ private fun getActivityContext(): Optional {
+ return Optional.ofNullable(weakContext)
+ .map(Function({ obj: WeakReference? -> obj!!.get() }))
+ .filter(Predicate({ context: AppCompatActivity? -> !context!!.isFinishing() }))
+ }
+
+ // guard against IllegalStateException in calling DialogFragment.show() whilst in background
+ // (which could happen, say, when the user pressed the home button while waiting for
+ // the network request to return) when it internally calls FragmentTransaction.commit()
+ // after the FragmentManager has saved its states (isStateSaved() == true)
+ // (ref: https://stackoverflow.com/a/39813506)
+ private fun runOnVisible(runnable: java.util.function.Consumer) {
+ getActivityContext().ifPresentOrElse(java.util.function.Consumer({ context: AppCompatActivity? ->
+ if (getLifecycle().currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ context!!.runOnUiThread(Runnable({
+ runnable.accept((context))
+ inFlight(false)
+ }))
+ } else {
+ getLifecycle().addObserver(object : DefaultLifecycleObserver {
+ public override fun onResume(owner: LifecycleOwner) {
+ getLifecycle().removeObserver(this)
+ getActivityContext().ifPresentOrElse(java.util.function.Consumer({ context: AppCompatActivity? ->
+ context!!.runOnUiThread(Runnable({
+ runnable.accept((context))
+ inFlight(false)
+ }))
+ }),
+ Runnable({ inFlight(false) })
+ )
+ }
+ })
+ // this trick doesn't seem to work on Android 10+ (API 29)
+ // which places restrictions on starting activities from the background
+ if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
+ && !context!!.isChangingConfigurations())) {
+ // try to bring the activity back to front if minimised
+ val i: Intent = Intent(context, RouterActivity::class.java)
+ i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ startActivity(i)
+ }
+ }
+ }), Runnable({ // this branch is executed if there is no activity context
+ inFlight(false)
+ })
+ )
+ }
+
+ fun pleaseWait(single: Single): Single {
+ // 'abuse' ambWith() here to cancel the toast for us when the wait is over
+ return single.ambWith(Single.create(SingleOnSubscribe({ emitter: SingleEmitter ->
+ getActivityContext().ifPresent(java.util.function.Consumer({ context: AppCompatActivity? ->
+ context!!.runOnUiThread(Runnable({
+
+ // Getting the stream info usually takes a moment
+ // Notifying the user here to ensure that no confusion arises
+ val toast: Toast = Toast.makeText(context,
+ getString(R.string.processing_may_take_a_moment),
+ Toast.LENGTH_LONG)
+ toast.show()
+ emitter.setCancellable(Cancellable({ toast.cancel() }))
+ }))
+ }))
+ })))
+ }
+
+ @SuppressLint("CheckResult")
+ fun openDownloadDialog(currentServiceId: Int, currentUrl: String?) {
+ inFlight(true)
+ val loadingDialog: LoadingDialog = LoadingDialog(R.string.loading_metadata_title)
+ loadingDialog.show(getParentFragmentManager(), "loadingDialog")
+ disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .compose(SingleTransformer({ single: Single -> pleaseWait(single) }))
+ .subscribe(io.reactivex.rxjava3.functions.Consumer({ result: StreamInfo ->
+ runOnVisible(java.util.function.Consumer({ ctx: AppCompatActivity ->
+ loadingDialog.dismiss()
+ val fm: FragmentManager = ctx.getSupportFragmentManager()
+ val downloadDialog: DownloadDialog = DownloadDialog(ctx, result)
+ // dismiss listener to be handled by FragmentManager
+ downloadDialog.show(fm, "downloadDialog")
+ })
+ )
+ }), io.reactivex.rxjava3.functions.Consumer({ throwable: Throwable? ->
+ runOnVisible(java.util.function.Consumer({ ctx: AppCompatActivity ->
+ loadingDialog.dismiss()
+ (ctx as RouterActivity).showUnsupportedUrlDialog(currentUrl)
+ }))
+ })))
+ }
+
+ fun openAddToPlaylistDialog(currentServiceId: Int, currentUrl: String?) {
+ inFlight(true)
+ disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .compose(SingleTransformer({ single: Single -> pleaseWait(single) }))
+ .subscribe(
+ io.reactivex.rxjava3.functions.Consumer({ info: StreamInfo? ->
+ getActivityContext().ifPresent(java.util.function.Consumer({ context: AppCompatActivity? ->
+ PlaylistDialog.Companion.createCorrespondingDialog(context,
+ java.util.List.of(StreamEntity((info)!!)),
+ java.util.function.Consumer({ playlistDialog: PlaylistDialog ->
+ runOnVisible(java.util.function.Consumer({ ctx: AppCompatActivity ->
+ // dismiss listener to be handled by FragmentManager
+ val fm: FragmentManager = ctx.getSupportFragmentManager()
+ playlistDialog.show(fm, "addToPlaylistDialog")
+ }))
+ })
+ )
+ }))
+ }),
+ io.reactivex.rxjava3.functions.Consumer({ throwable: Throwable? ->
+ runOnVisible(java.util.function.Consumer({ ctx: AppCompatActivity ->
+ handleError(ctx, ErrorInfo(
+ (throwable)!!,
+ UserAction.REQUESTED_STREAM,
+ "Tried to add " + currentUrl + " to a playlist",
+ (ctx as RouterActivity).currentService!!.getServiceId())
+ )
+ }))
+ })
+ )
+ )
+ }
+ }
+
+ private fun openAddToPlaylistDialog() {
+ getPersistFragment().openAddToPlaylistDialog(currentServiceId, currentUrl)
+ }
+
+ private fun openDownloadDialog() {
+ getPersistFragment().openDownloadDialog(currentServiceId, currentUrl)
+ }
+
+ private fun getPersistFragment(): PersistentFragment {
+ val fm: FragmentManager = getSupportFragmentManager()
+ var persistFragment: PersistentFragment? = fm.findFragmentByTag("PERSIST_FRAGMENT") as PersistentFragment?
+ if (persistFragment == null) {
+ persistFragment = PersistentFragment()
+ fm.beginTransaction()
+ .add(persistFragment, "PERSIST_FRAGMENT")
+ .commitNow()
+ }
+ return persistFragment
+ }
+
+ public override fun onRequestPermissionsResult(requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ for (i: Int in grantResults) {
+ if (i == PackageManager.PERMISSION_DENIED) {
+ finish()
+ return
+ }
+ }
+ if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) {
+ openDownloadDialog()
+ }
+ }
+
+ private class AdapterChoiceItem internal constructor(val key: String, val description: String, @field:DrawableRes val icon: Int)
+ class Choice internal constructor(val serviceId: Int, val linkType: LinkType?,
+ val url: String?, val playerChoice: String) : Serializable {
+ public override fun toString(): String {
+ return serviceId.toString() + ":" + url + " > " + linkType + " ::: " + playerChoice
+ }
+ }
+
+ class FetcherService() : IntentService(FetcherService::class.java.getSimpleName()) {
+ private var fetcher: Disposable? = null
+ public override fun onCreate() {
+ super.onCreate()
+ startForeground(ID, createNotification().build())
+ }
+
+ override fun onHandleIntent(intent: Intent?) {
+ if (intent == null) {
+ return
+ }
+ val serializable: Serializable? = intent.getSerializableExtra(KEY_CHOICE)
+ if (!(serializable is Choice)) {
+ return
+ }
+ handleChoice(serializable)
+ }
+
+ fun handleChoice(choice: Choice) {
+ var single: Single? = null
+ var userAction: UserAction = UserAction.SOMETHING_ELSE
+ when (choice.linkType) {
+ LinkType.STREAM -> {
+ single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false)
+ userAction = UserAction.REQUESTED_STREAM
+ }
+
+ LinkType.CHANNEL -> {
+ single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false)
+ userAction = UserAction.REQUESTED_CHANNEL
+ }
+
+ LinkType.PLAYLIST -> {
+ single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false)
+ userAction = UserAction.REQUESTED_PLAYLIST
+ }
+ }
+ if (single != null) {
+ val finalUserAction: UserAction = userAction
+ val resultHandler: java.util.function.Consumer = getResultHandler(choice)
+ fetcher = single
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ info: Info? ->
+ resultHandler.accept(info)
+ if (fetcher != null) {
+ fetcher!!.dispose()
+ }
+ }, io.reactivex.rxjava3.functions.Consumer({ throwable: Throwable? ->
+ handleError(this, ErrorInfo((throwable)!!, finalUserAction,
+ choice.url + " opened with " + choice.playerChoice,
+ choice.serviceId))
+ }))
+ }
+ }
+
+ fun getResultHandler(choice: Choice): java.util.function.Consumer {
+ return java.util.function.Consumer({ info: Info? ->
+ val videoPlayerKey: String = getString(R.string.video_player_key)
+ val backgroundPlayerKey: String = getString(R.string.background_player_key)
+ val popupPlayerKey: String = getString(R.string.popup_player_key)
+ val preferences: SharedPreferences = PreferenceManager
+ .getDefaultSharedPreferences(this)
+ val isExtVideoEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false)
+ val isExtAudioEnabled: Boolean = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false)
+ val playQueue: PlayQueue
+ if (info is StreamInfo) {
+ if ((choice.playerChoice == backgroundPlayerKey) && isExtAudioEnabled) {
+ NavigationHelper.playOnExternalAudioPlayer(this, info)
+ return@Consumer
+ } else if ((choice.playerChoice == videoPlayerKey) && isExtVideoEnabled) {
+ NavigationHelper.playOnExternalVideoPlayer(this, info)
+ return@Consumer
+ }
+ playQueue = SinglePlayQueue(info as StreamInfo?)
+ } else if (info is ChannelInfo) {
+ val playableTab: Optional = info.getTabs()
+ .stream()
+ .filter(Predicate({ obj: ListLinkHandler? -> ChannelTabHelper.isStreamsTab() }))
+ .findFirst()
+ if (playableTab.isPresent()) {
+ playQueue = ChannelTabPlayQueue(info.getServiceId(), playableTab.get())
+ } else {
+ return@Consumer // there is no playable tab
+ }
+ } else if (info is PlaylistInfo) {
+ playQueue = PlaylistPlayQueue(info)
+ } else {
+ return@Consumer
+ }
+ if ((choice.playerChoice == videoPlayerKey)) {
+ NavigationHelper.playOnMainPlayer(this, playQueue, false)
+ } else if ((choice.playerChoice == backgroundPlayerKey)) {
+ NavigationHelper.playOnBackgroundPlayer(this, playQueue, true)
+ } else if ((choice.playerChoice == popupPlayerKey)) {
+ NavigationHelper.playOnPopupPlayer(this, playQueue, true)
+ }
+ })
+ }
+
+ public override fun onDestroy() {
+ super.onDestroy()
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
+ if (fetcher != null) {
+ fetcher!!.dispose()
+ }
+ }
+
+ private fun createNotification(): NotificationCompat.Builder {
+ return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setContentTitle(
+ getString(R.string.preferred_player_fetcher_notification_title))
+ .setContentText(
+ getString(R.string.preferred_player_fetcher_notification_message))
+ }
+
+ companion object {
+ val KEY_CHOICE: String = "key_choice"
+ private val ID: Int = 456
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun getUrl(intent: Intent): String? {
+ var foundUrl: String? = null
+ if (intent.getData() != null) {
+ // Called from another app
+ foundUrl = intent.getData().toString()
+ } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
+ // Called from the share menu
+ val extraText: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
+ foundUrl = firstUrlFromInput(extraText)
+ }
+ return foundUrl
+ }
+
+ companion object {
+ /**
+ * @param context the context. It will be `finish()`ed at the end of the handling if it is
+ * an instance of [RouterActivity].
+ * @param errorInfo the error information
+ */
+ private fun handleError(context: Context, errorInfo: ErrorInfo) {
+ if (errorInfo.throwable != null) {
+ errorInfo.throwable!!.printStackTrace()
+ }
+ if (errorInfo.throwable is ReCaptchaException) {
+ Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show()
+ // Starting ReCaptcha Challenge Activity
+ val intent: Intent = Intent(context, ReCaptchaActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ } else if ((errorInfo.throwable != null
+ && errorInfo.throwable!!.isNetworkRelated)) {
+ Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is AgeRestrictedContentException) {
+ Toast.makeText(context, R.string.restricted_video_no_stream,
+ Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is GeographicRestrictionException) {
+ Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is PaidContentException) {
+ Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is PrivateContentException) {
+ Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is SoundCloudGoPlusContentException) {
+ Toast.makeText(context, R.string.soundcloud_go_plus_content,
+ Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is YoutubeMusicPremiumContentException) {
+ Toast.makeText(context, R.string.youtube_music_premium_content,
+ Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is ContentNotAvailableException) {
+ Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show()
+ } else if (errorInfo.throwable is ContentNotSupportedException) {
+ Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show()
+ } else {
+ createNotification(context, errorInfo)
+ }
+ if (context is RouterActivity) {
+ context.finish()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java
deleted file mode 100644
index 04d93a238d5..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.schabi.newpipe.database;
-
-import static org.schabi.newpipe.database.Migrations.DB_VER_9;
-
-import androidx.room.Database;
-import androidx.room.RoomDatabase;
-import androidx.room.TypeConverters;
-
-import org.schabi.newpipe.database.feed.dao.FeedDAO;
-import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
-import org.schabi.newpipe.database.feed.model.FeedEntity;
-import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
-import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity;
-import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity;
-import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
-import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
-import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
-import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
-import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
-import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
-import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
-import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
-import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
-import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
-import org.schabi.newpipe.database.stream.dao.StreamDAO;
-import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
-import org.schabi.newpipe.database.stream.model.StreamEntity;
-import org.schabi.newpipe.database.stream.model.StreamStateEntity;
-import org.schabi.newpipe.database.subscription.SubscriptionDAO;
-import org.schabi.newpipe.database.subscription.SubscriptionEntity;
-
-@TypeConverters({Converters.class})
-@Database(
- entities = {
- SubscriptionEntity.class, SearchHistoryEntry.class,
- StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
- PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
- FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
- FeedLastUpdatedEntity.class
- },
- version = DB_VER_9
-)
-public abstract class AppDatabase extends RoomDatabase {
- public static final String DATABASE_NAME = "newpipe.db";
-
- public abstract SearchHistoryDAO searchHistoryDAO();
-
- public abstract StreamDAO streamDAO();
-
- public abstract StreamHistoryDAO streamHistoryDAO();
-
- public abstract StreamStateDAO streamStateDAO();
-
- public abstract PlaylistDAO playlistDAO();
-
- public abstract PlaylistStreamDAO playlistStreamDAO();
-
- public abstract PlaylistRemoteDAO playlistRemoteDAO();
-
- public abstract FeedDAO feedDAO();
-
- public abstract FeedGroupDAO feedGroupDAO();
-
- public abstract SubscriptionDAO subscriptionDAO();
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
new file mode 100644
index 00000000000..93d643d86b4
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
@@ -0,0 +1,46 @@
+package org.schabi.newpipe.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import org.schabi.newpipe.database.feed.dao.FeedDAO
+import org.schabi.newpipe.database.feed.dao.FeedGroupDAO
+import org.schabi.newpipe.database.feed.model.FeedEntity
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
+import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
+import org.schabi.newpipe.database.history.dao.SearchHistoryDAO
+import org.schabi.newpipe.database.history.dao.StreamHistoryDAO
+import org.schabi.newpipe.database.history.model.SearchHistoryEntry
+import org.schabi.newpipe.database.history.model.StreamHistoryEntity
+import org.schabi.newpipe.database.playlist.dao.PlaylistDAO
+import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO
+import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
+import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
+import org.schabi.newpipe.database.stream.dao.StreamDAO
+import org.schabi.newpipe.database.stream.dao.StreamStateDAO
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+import org.schabi.newpipe.database.subscription.SubscriptionDAO
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+
+@TypeConverters([Converters::class])
+@Database(entities = [SubscriptionEntity::class, SearchHistoryEntry::class, StreamEntity::class, StreamHistoryEntity::class, StreamStateEntity::class, PlaylistEntity::class, PlaylistStreamEntity::class, PlaylistRemoteEntity::class, FeedEntity::class, FeedGroupEntity::class, FeedGroupSubscriptionEntity::class, FeedLastUpdatedEntity::class], version = Migrations.DB_VER_9)
+abstract class AppDatabase() : RoomDatabase() {
+ abstract fun searchHistoryDAO(): SearchHistoryDAO?
+ abstract fun streamDAO(): StreamDAO
+ abstract fun streamHistoryDAO(): StreamHistoryDAO?
+ abstract fun streamStateDAO(): StreamStateDAO?
+ abstract fun playlistDAO(): PlaylistDAO?
+ abstract fun playlistStreamDAO(): PlaylistStreamDAO?
+ abstract fun playlistRemoteDAO(): PlaylistRemoteDAO?
+ abstract fun feedDAO(): FeedDAO?
+ abstract fun feedGroupDAO(): FeedGroupDAO?
+ abstract fun subscriptionDAO(): SubscriptionDAO?
+
+ companion object {
+ val DATABASE_NAME: String = "newpipe.db"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
deleted file mode 100644
index 255f5ba8deb..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.schabi.newpipe.database;
-
-import androidx.room.Dao;
-import androidx.room.Delete;
-import androidx.room.Insert;
-import androidx.room.Update;
-
-import java.util.Collection;
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-@Dao
-public interface BasicDAO {
- /* Inserts */
- @Insert
- long insert(Entity entity);
-
- @Insert
- List insertAll(Collection entities);
-
- /* Searches */
- Flowable> getAll();
-
- Flowable> listByService(int serviceId);
-
- /* Deletes */
- @Delete
- void delete(Entity entity);
-
- int deleteAll();
-
- /* Updates */
- @Update
- int update(Entity entity);
-
- @Update
- void update(Collection entities);
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
new file mode 100644
index 00000000000..bc368c3a7f9
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
@@ -0,0 +1,33 @@
+package org.schabi.newpipe.database
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Update
+import io.reactivex.rxjava3.core.Flowable
+
+@Dao
+open interface BasicDAO {
+ /* Inserts */
+ @Insert
+ fun insert(entity: Entity): Long
+
+ @Insert
+ fun insertAll(entities: Collection?): List?
+
+ /* Searches */
+ fun getAll(): Flowable?>?
+ fun listByService(serviceId: Int): Flowable?>?
+
+ /* Deletes */
+ @Delete
+ fun delete(entity: Entity)
+ fun deleteAll(): Int
+
+ /* Updates */
+ @Update
+ fun update(entity: Entity): Int
+
+ @Update
+ fun update(entities: Collection?)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java
deleted file mode 100644
index 54b856b0653..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.schabi.newpipe.database;
-
-public interface LocalItem {
- LocalItemType getLocalItemType();
-
- enum LocalItemType {
- PLAYLIST_LOCAL_ITEM,
- PLAYLIST_REMOTE_ITEM,
-
- PLAYLIST_STREAM_ITEM,
- STATISTIC_STREAM_ITEM,
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
new file mode 100644
index 00000000000..4723eacf2dd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
@@ -0,0 +1,11 @@
+package org.schabi.newpipe.database
+
+open interface LocalItem {
+ fun getLocalItemType(): LocalItemType
+ enum class LocalItemType {
+ PLAYLIST_LOCAL_ITEM,
+ PLAYLIST_REMOTE_ITEM,
+ PLAYLIST_STREAM_ITEM,
+ STATISTIC_STREAM_ITEM
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt
similarity index 57%
rename from app/src/main/java/org/schabi/newpipe/database/Migrations.java
rename to app/src/main/java/org/schabi/newpipe/database/Migrations.kt
index c9f630869c9..a0ea3c78a24 100644
--- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java
+++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt
@@ -1,15 +1,11 @@
-package org.schabi.newpipe.database;
+package org.schabi.newpipe.database
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.room.migration.Migration;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-
-import org.schabi.newpipe.MainActivity;
-
-public final class Migrations {
+import android.util.Log
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import org.schabi.newpipe.MainActivity
+object Migrations {
/////////////////////////////////////////////////////////////////////////////
// Test new migrations manually by importing a database from daily usage //
// and checking if the migration works (Use the Database Inspector //
@@ -17,25 +13,21 @@ public final class Migrations {
// If you add a migration point it out in the pull request, so that //
// others remember to test it themselves. //
/////////////////////////////////////////////////////////////////////////////
-
- public static final int DB_VER_1 = 1;
- public static final int DB_VER_2 = 2;
- public static final int DB_VER_3 = 3;
- public static final int DB_VER_4 = 4;
- public static final int DB_VER_5 = 5;
- public static final int DB_VER_6 = 6;
- public static final int DB_VER_7 = 7;
- public static final int DB_VER_8 = 8;
- public static final int DB_VER_9 = 9;
-
- private static final String TAG = Migrations.class.getName();
- public static final boolean DEBUG = MainActivity.DEBUG;
-
- public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ val DB_VER_1: Int = 1
+ val DB_VER_2: Int = 2
+ val DB_VER_3: Int = 3
+ val DB_VER_4: Int = 4
+ val DB_VER_5: Int = 5
+ val DB_VER_6: Int = 6
+ val DB_VER_7: Int = 7
+ val DB_VER_8: Int = 8
+ val DB_VER_9: Int = 9
+ private val TAG: String = Migrations::class.java.getName()
+ val DEBUG: Boolean = MainActivity.Companion.DEBUG
+ val MIGRATION_1_2: Migration = object : Migration(DB_VER_1, DB_VER_2) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
if (DEBUG) {
- Log.d(TAG, "Start migrating database");
+ Log.d(TAG, "Start migrating database")
}
/*
* Unfortunately these queries must be hardcoded due to the possibility of
@@ -45,170 +37,152 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) {
// Not much we can do about this, since room doesn't create tables before migration.
// It's either this or blasting the entire database anew.
- database.execSQL("CREATE INDEX `index_search_history_search` "
- + "ON `search_history` (`search`)");
- database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
+ database.execSQL(("CREATE INDEX `index_search_history_search` "
+ + "ON `search_history` (`search`)"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `streams` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
- + "`thumbnail_url` TEXT)");
- database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
- + "ON `streams` (`service_id`, `url`)");
- database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
+ + "`thumbnail_url` TEXT)"))
+ database.execSQL(("CREATE UNIQUE INDEX `index_streams_service_id_url` "
+ + "ON `streams` (`service_id`, `url`)"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_history` "
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
- + "ON UPDATE CASCADE ON DELETE CASCADE )");
- database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
- + "ON `stream_history` (`stream_id`)");
- database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
+ + "ON UPDATE CASCADE ON DELETE CASCADE )"))
+ database.execSQL(("CREATE INDEX `index_stream_history_stream_id` "
+ + "ON `stream_history` (`stream_id`)"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_state` "
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
- + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
- database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
+ + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `playlists` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
- + "`name` TEXT, `thumbnail_url` TEXT)");
- database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
- database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
+ + "`name` TEXT, `thumbnail_url` TEXT)"))
+ database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)")
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
- + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
- database.execSQL("CREATE UNIQUE INDEX "
+ + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
+ database.execSQL(("CREATE UNIQUE INDEX "
+ "`index_playlist_stream_join_playlist_id_join_index` "
- + "ON `playlist_stream_join` (`playlist_id`, `join_index`)");
- database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` "
- + "ON `playlist_stream_join` (`stream_id`)");
- database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
+ + "ON `playlist_stream_join` (`playlist_id`, `join_index`)"))
+ database.execSQL(("CREATE INDEX `index_playlist_stream_join_stream_id` "
+ + "ON `playlist_stream_join` (`stream_id`)"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `remote_playlists` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
- + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
- database.execSQL("CREATE INDEX `index_remote_playlists_name` "
- + "ON `remote_playlists` (`name`)");
- database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
- + "ON `remote_playlists` (`service_id`, `url`)");
+ + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"))
+ database.execSQL(("CREATE INDEX `index_remote_playlists_name` "
+ + "ON `remote_playlists` (`name`)"))
+ database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ + "ON `remote_playlists` (`service_id`, `url`)"))
// Populate streams table with existing entries in watch history
// Latest data first, thus ignoring older entries with the same indices
- database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, "
+ database.execSQL(("INSERT OR IGNORE INTO streams (service_id, url, title, "
+ "stream_type, duration, uploader, thumbnail_url) "
-
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
+ "uploader, thumbnail_url "
-
+ "FROM watch_history "
- + "ORDER BY creation_date DESC");
+ + "ORDER BY creation_date DESC"))
// Once the streams have PKs, join them with the normalized history table
// and populate it with the remaining data from watch history
- database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
+ database.execSQL(("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
+ "SELECT uid, creation_date, 1 "
+ "FROM watch_history INNER JOIN streams "
+ "ON watch_history.service_id == streams.service_id "
+ "AND watch_history.url == streams.url "
- + "ORDER BY creation_date DESC");
-
- database.execSQL("DROP TABLE IF EXISTS watch_history");
-
+ + "ORDER BY creation_date DESC"))
+ database.execSQL("DROP TABLE IF EXISTS watch_history")
if (DEBUG) {
- Log.d(TAG, "Stop migrating database");
+ Log.d(TAG, "Stop migrating database")
}
}
- };
-
- public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ }
+ val MIGRATION_2_3: Migration = object : Migration(DB_VER_2, DB_VER_3) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
// Add NOT NULLs and new fields
- database.execSQL("CREATE TABLE IF NOT EXISTS streams_new "
+ database.execSQL(("CREATE TABLE IF NOT EXISTS streams_new "
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
+ "textual_upload_date TEXT, upload_date INTEGER, "
- + "is_upload_date_approximation INTEGER)");
-
- database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
+ + "is_upload_date_approximation INTEGER)"))
+ database.execSQL(("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
+ "upload_date, is_upload_date_approximation) "
-
+ "SELECT uid, service_id, url, ifnull(title, ''), "
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
-
- + "FROM streams WHERE url IS NOT NULL");
-
- database.execSQL("DROP TABLE streams");
- database.execSQL("ALTER TABLE streams_new RENAME TO streams");
- database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
- + "ON streams (service_id, url)");
+ + "FROM streams WHERE url IS NOT NULL"))
+ database.execSQL("DROP TABLE streams")
+ database.execSQL("ALTER TABLE streams_new RENAME TO streams")
+ database.execSQL(("CREATE UNIQUE INDEX index_streams_service_id_url "
+ + "ON streams (service_id, url)"))
// Tables for feed feature
- database.execSQL("CREATE TABLE IF NOT EXISTS feed "
+ database.execSQL(("CREATE TABLE IF NOT EXISTS feed "
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
+ "PRIMARY KEY(stream_id, subscription_id), "
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
- + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
- database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
- database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
+ + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
+ database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)")
+ database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group "
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
- + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
- database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
- database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
+ + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)"))
+ database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)")
+ database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
+ "PRIMARY KEY(group_id, subscription_id), "
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
- + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
- database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
- + "ON feed_group_subscription_join (subscription_id)");
- database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
+ + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
+ database.execSQL(("CREATE INDEX index_feed_group_subscription_join_subscription_id "
+ + "ON feed_group_subscription_join (subscription_id)"))
+ database.execSQL(("CREATE TABLE IF NOT EXISTS feed_last_updated "
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
+ "PRIMARY KEY(subscription_id), "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
- + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
+ + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
}
- };
-
- public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ }
+ val MIGRATION_3_4: Migration = object : Migration(DB_VER_3, DB_VER_4) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
- );
+ )
}
- };
-
- public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
- database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
- + "INTEGER NOT NULL DEFAULT 0");
+ }
+ val MIGRATION_4_5: Migration = object : Migration(DB_VER_4, DB_VER_5) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
+ + "INTEGER NOT NULL DEFAULT 0"))
}
- };
-
- public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
- database.execSQL("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
- + "INTEGER NOT NULL DEFAULT 0");
+ }
+ val MIGRATION_5_6: Migration = object : Migration(DB_VER_5, DB_VER_6) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
+ + "INTEGER NOT NULL DEFAULT 0"))
}
- };
-
- public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ }
+ val MIGRATION_6_7: Migration = object : Migration(DB_VER_6, DB_VER_7) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
// Create a new column thumbnail_stream_id
- database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
- + "INTEGER NOT NULL DEFAULT -1");
+ database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
+ + "INTEGER NOT NULL DEFAULT -1"))
// Migrate the thumbnail_url to the thumbnail_stream_id
- database.execSQL("UPDATE playlists SET thumbnail_stream_id = ("
+ database.execSQL(("UPDATE playlists SET thumbnail_stream_id = ("
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
+ " FROM ("
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
@@ -216,92 +190,81 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id"
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
- + " WHERE playlist_uid = playlists.uid)");
+ + " WHERE playlist_uid = playlists.uid)"))
// Remove the thumbnail_url field in the playlist table
- database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`"
+ database.execSQL(("CREATE TABLE IF NOT EXISTS `playlists_new`"
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "name TEXT, "
+ "is_thumbnail_permanent INTEGER NOT NULL, "
- + "thumbnail_stream_id INTEGER NOT NULL)");
-
- database.execSQL("INSERT INTO playlists_new"
+ + "thumbnail_stream_id INTEGER NOT NULL)"))
+ database.execSQL(("INSERT INTO playlists_new"
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
- + " FROM playlists");
-
-
- database.execSQL("DROP TABLE playlists");
- database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
- database.execSQL("CREATE INDEX IF NOT EXISTS "
- + "`index_playlists_name` ON `playlists` (`name`)");
+ + " FROM playlists"))
+ database.execSQL("DROP TABLE playlists")
+ database.execSQL("ALTER TABLE playlists_new RENAME TO playlists")
+ database.execSQL(("CREATE INDEX IF NOT EXISTS "
+ + "`index_playlists_name` ON `playlists` (`name`)"))
}
- };
-
- public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
- database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
- + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
- database.execSQL("UPDATE search_history SET search = trim(search)");
+ }
+ val MIGRATION_7_8: Migration = object : Migration(DB_VER_7, DB_VER_8) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
+ + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"))
+ database.execSQL("UPDATE search_history SET search = trim(search)")
}
- };
-
- public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
- @Override
- public void migrate(@NonNull final SupportSQLiteDatabase database) {
+ }
+ val MIGRATION_8_9: Migration = object : Migration(DB_VER_8, DB_VER_9) {
+ public override fun migrate(database: SupportSQLiteDatabase) {
try {
- database.beginTransaction();
+ database.beginTransaction()
// Update playlists.
// Create a temp table to initialize display_index.
- database.execSQL("CREATE TABLE `playlists_tmp` "
+ database.execSQL(("CREATE TABLE `playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
- + "`display_index` INTEGER NOT NULL)");
- database.execSQL("INSERT INTO `playlists_tmp` "
+ + "`display_index` INTEGER NOT NULL)"))
+ database.execSQL(("INSERT INTO `playlists_tmp` "
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "`display_index`) "
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "-1 "
- + "FROM `playlists`");
+ + "FROM `playlists`"))
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
- database.execSQL("DROP TABLE `playlists`");
- database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
+ database.execSQL("DROP TABLE `playlists`")
+ database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`")
// Update remote_playlists.
// Create a temp table to initialize display_index.
- database.execSQL("CREATE TABLE `remote_playlists_tmp` "
+ database.execSQL(("CREATE TABLE `remote_playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
+ "`display_index` INTEGER NOT NULL,"
- + "`stream_count` INTEGER)");
- database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
+ + "`stream_count` INTEGER)"))
+ database.execSQL(("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
+ "`stream_count`)"
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
- + "-1, `stream_count` FROM `remote_playlists`");
+ + "-1, `stream_count` FROM `remote_playlists`"))
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
- database.execSQL("DROP TABLE `remote_playlists`");
- database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
+ database.execSQL("DROP TABLE `remote_playlists`")
+ database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`")
// Create index on the new table.
- database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
- + "ON `remote_playlists` (`service_id`, `url`)");
-
- database.setTransactionSuccessful();
+ database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ + "ON `remote_playlists` (`service_id`, `url`)"))
+ database.setTransactionSuccessful()
} finally {
- database.endTransaction();
+ database.endTransaction()
}
}
- };
-
- private Migrations() {
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java
deleted file mode 100644
index 1ade08122c8..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.schabi.newpipe.database.history.dao;
-
-import org.schabi.newpipe.database.BasicDAO;
-
-public interface HistoryDAO extends BasicDAO {
- T getLatestEntry();
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt
new file mode 100644
index 00000000000..a33550f947e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.database.history.dao
+
+import org.schabi.newpipe.database.BasicDAO
+
+open interface HistoryDAO : BasicDAO {
+ fun getLatestEntry(): T
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java
deleted file mode 100644
index 8a281bdb48c..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.schabi.newpipe.database.history.dao;
-
-import androidx.annotation.Nullable;
-import androidx.room.Dao;
-import androidx.room.Query;
-
-import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
-import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
-import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
-import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
-import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
-
-@Dao
-public interface SearchHistoryDAO extends HistoryDAO {
- String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
- String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
-
- @Query("SELECT * FROM " + TABLE_NAME
- + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
- @Nullable
- SearchHistoryEntry getLatestEntry();
-
- @Query("DELETE FROM " + TABLE_NAME)
- @Override
- int deleteAll();
-
- @Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query")
- int deleteAllWhereQuery(String query);
-
- @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
- @Override
- Flowable> getAll();
-
- @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
- + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
- Flowable> getUniqueEntries(int limit);
-
- @Query("SELECT * FROM " + TABLE_NAME
- + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
- @Override
- Flowable> listByService(int serviceId);
-
- @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
- + " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
- Flowable> getSimilarEntries(String query, int limit);
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt
new file mode 100644
index 00000000000..b3550d191af
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt
@@ -0,0 +1,37 @@
+package org.schabi.newpipe.database.history.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.history.model.SearchHistoryEntry
+
+@Dao
+open interface SearchHistoryDAO : HistoryDAO {
+ @Query(("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME
+ + " WHERE " + SearchHistoryEntry.ID + " = (SELECT MAX(" + SearchHistoryEntry.ID + ") FROM " + SearchHistoryEntry.TABLE_NAME + ")"))
+ public override fun getLatestEntry(): SearchHistoryEntry?
+ @Query("DELETE FROM " + SearchHistoryEntry.TABLE_NAME)
+ public override fun deleteAll(): Int
+
+ @Query("DELETE FROM " + SearchHistoryEntry.TABLE_NAME + " WHERE " + SearchHistoryEntry.SEARCH + " = :query")
+ fun deleteAllWhereQuery(query: String?): Int
+ @Query("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME + ORDER_BY_CREATION_DATE)
+ public override fun getAll(): Flowable>?
+
+ @Query(("SELECT " + SearchHistoryEntry.SEARCH + " FROM " + SearchHistoryEntry.TABLE_NAME + " GROUP BY " + SearchHistoryEntry.SEARCH
+ + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit"))
+ fun getUniqueEntries(limit: Int): Flowable?>?
+
+ @Query(("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME
+ + " WHERE " + SearchHistoryEntry.SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE))
+ public override fun listByService(serviceId: Int): Flowable>?
+
+ @Query(("SELECT " + SearchHistoryEntry.SEARCH + " FROM " + SearchHistoryEntry.TABLE_NAME + " WHERE " + SearchHistoryEntry.SEARCH + " LIKE :query || '%'"
+ + " GROUP BY " + SearchHistoryEntry.SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit"))
+ fun getSimilarEntries(query: String?, limit: Int): Flowable?>?
+
+ companion object {
+ val ORDER_BY_CREATION_DATE: String = " ORDER BY " + SearchHistoryEntry.CREATION_DATE + " DESC"
+ val ORDER_BY_MAX_CREATION_DATE: String = " ORDER BY MAX(" + SearchHistoryEntry.CREATION_DATE + ") DESC"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java
deleted file mode 100644
index 150d4a8e5b5..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.schabi.newpipe.database.history.dao;
-
-import androidx.annotation.Nullable;
-import androidx.room.Dao;
-import androidx.room.Query;
-import androidx.room.RewriteQueriesToDropUnusedColumns;
-
-import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
-import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
-import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
-import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
-import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
-
-@Dao
-public abstract class StreamHistoryDAO implements HistoryDAO {
- @Query("SELECT * FROM " + STREAM_HISTORY_TABLE
- + " WHERE " + STREAM_ACCESS_DATE + " = "
- + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
- @Override
- @Nullable
- public abstract StreamHistoryEntity getLatestEntry();
-
- @Override
- @Query("SELECT * FROM " + STREAM_HISTORY_TABLE)
- public abstract Flowable> getAll();
-
- @Override
- @Query("DELETE FROM " + STREAM_HISTORY_TABLE)
- public abstract int deleteAll();
-
- @Override
- public Flowable> listByService(final int serviceId) {
- throw new UnsupportedOperationException();
- }
-
- @Query("SELECT * FROM " + STREAM_TABLE
- + " INNER JOIN " + STREAM_HISTORY_TABLE
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
- + " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
- public abstract Flowable> getHistory();
-
-
- @Query("SELECT * FROM " + STREAM_TABLE
- + " INNER JOIN " + STREAM_HISTORY_TABLE
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
- + " ORDER BY " + STREAM_ID + " ASC")
- public abstract Flowable> getHistorySortedById();
-
- @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
- + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
- @Nullable
- public abstract StreamHistoryEntity getLatestEntry(long streamId);
-
- @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
- public abstract int deleteStreamHistory(long streamId);
-
- @RewriteQueriesToDropUnusedColumns
- @Query("SELECT * FROM " + STREAM_TABLE
-
- // Select the latest entry and watch count for each stream id on history table
- + " INNER JOIN "
- + "(SELECT " + JOIN_STREAM_ID + ", "
- + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", "
- + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
- + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
-
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
-
- + " LEFT JOIN "
- + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
- + STREAM_PROGRESS_MILLIS
- + " FROM " + STREAM_STATE_TABLE + " )"
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
- public abstract Flowable> getStatistics();
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt
new file mode 100644
index 00000000000..0068aacc899
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt
@@ -0,0 +1,59 @@
+package org.schabi.newpipe.database.history.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.RewriteQueriesToDropUnusedColumns
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.history.model.StreamHistoryEntity
+import org.schabi.newpipe.database.history.model.StreamHistoryEntry
+import org.schabi.newpipe.database.stream.StreamStatisticsEntry
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+
+@Dao
+abstract class StreamHistoryDAO() : HistoryDAO {
+ @Query(("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
+ + " WHERE " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " = "
+ + "(SELECT MAX(" + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + ") FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + ")"))
+ abstract override fun getLatestEntry(): StreamHistoryEntity?
+ @Query("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE)
+ abstract override fun getAll(): Flowable?>?
+ @Query("DELETE FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE)
+ abstract override fun deleteAll(): Int
+ public override fun listByService(serviceId: Int): Flowable>? {
+ throw UnsupportedOperationException()
+ }
+
+ @Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE
+ + " INNER JOIN " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
+ + " ORDER BY " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " DESC"))
+ abstract fun getHistory(): Flowable?>?
+
+ @Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE
+ + " INNER JOIN " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
+ + " ORDER BY " + StreamEntity.STREAM_ID + " ASC"))
+ abstract fun getHistorySortedById(): Flowable?>
+
+ @Query(("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " WHERE " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
+ + " = :streamId ORDER BY " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " DESC LIMIT 1"))
+ abstract fun getLatestEntry(streamId: Long): StreamHistoryEntity?
+ @Query("DELETE FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " WHERE " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + " = :streamId")
+ abstract fun deleteStreamHistory(streamId: Long): Int
+
+ @RewriteQueriesToDropUnusedColumns
+ @Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE // Select the latest entry and watch count for each stream id on history table
+ + " INNER JOIN "
+ + "(SELECT " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + ", "
+ + " MAX(" + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + ") AS " + StreamStatisticsEntry.STREAM_LATEST_DATE + ", "
+ + " SUM(" + StreamHistoryEntity.Companion.STREAM_REPEAT_COUNT + ") AS " + StreamStatisticsEntry.STREAM_WATCH_COUNT
+ + " FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " GROUP BY " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + ")"
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
+ + " LEFT JOIN "
+ + "(SELECT " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
+ + StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
+ + " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS))
+ abstract fun getStatistics(): Flowable?>
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java
deleted file mode 100644
index a9d69afe855..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.schabi.newpipe.database.history.model;
-
-import androidx.annotation.NonNull;
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.ForeignKey;
-import androidx.room.Index;
-
-import org.schabi.newpipe.database.stream.model.StreamEntity;
-
-import java.time.OffsetDateTime;
-
-import static androidx.room.ForeignKey.CASCADE;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
-import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
-
-@Entity(tableName = STREAM_HISTORY_TABLE,
- primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
- // No need to index for timestamp as they will almost always be unique
- indices = {@Index(value = {JOIN_STREAM_ID})},
- foreignKeys = {
- @ForeignKey(entity = StreamEntity.class,
- parentColumns = StreamEntity.STREAM_ID,
- childColumns = JOIN_STREAM_ID,
- onDelete = CASCADE, onUpdate = CASCADE)
- })
-public class StreamHistoryEntity {
- public static final String STREAM_HISTORY_TABLE = "stream_history";
- public static final String JOIN_STREAM_ID = "stream_id";
- public static final String STREAM_ACCESS_DATE = "access_date";
- public static final String STREAM_REPEAT_COUNT = "repeat_count";
-
- @ColumnInfo(name = JOIN_STREAM_ID)
- private long streamUid;
-
- @NonNull
- @ColumnInfo(name = STREAM_ACCESS_DATE)
- private OffsetDateTime accessDate;
-
- @ColumnInfo(name = STREAM_REPEAT_COUNT)
- private long repeatCount;
-
- /**
- * @param streamUid the stream id this history item will refer to
- * @param accessDate the last time the stream was accessed
- * @param repeatCount the total number of views this stream received
- */
- public StreamHistoryEntity(final long streamUid,
- @NonNull final OffsetDateTime accessDate,
- final long repeatCount) {
- this.streamUid = streamUid;
- this.accessDate = accessDate;
- this.repeatCount = repeatCount;
- }
-
- public long getStreamUid() {
- return streamUid;
- }
-
- public void setStreamUid(final long streamUid) {
- this.streamUid = streamUid;
- }
-
- @NonNull
- public OffsetDateTime getAccessDate() {
- return accessDate;
- }
-
- public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
- this.accessDate = accessDate;
- }
-
- public long getRepeatCount() {
- return repeatCount;
- }
-
- public void setRepeatCount(final long repeatCount) {
- this.repeatCount = repeatCount;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt
new file mode 100644
index 00000000000..5c041e2195a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt
@@ -0,0 +1,50 @@
+package org.schabi.newpipe.database.history.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import org.schabi.newpipe.database.history.model.StreamHistoryEntity
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import java.time.OffsetDateTime
+
+@Entity(tableName = StreamHistoryEntity.STREAM_HISTORY_TABLE, primaryKeys = [StreamHistoryEntity.JOIN_STREAM_ID, StreamHistoryEntity.STREAM_ACCESS_DATE], indices = [Index(value = [StreamHistoryEntity.JOIN_STREAM_ID])], foreignKeys = [ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = StreamHistoryEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE)])
+class StreamHistoryEntity
+/**
+ * @param streamUid the stream id this history item will refer to
+ * @param accessDate the last time the stream was accessed
+ * @param repeatCount the total number of views this stream received
+ */(@field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long,
+ @field:ColumnInfo(name = STREAM_ACCESS_DATE) private var accessDate: OffsetDateTime,
+ @field:ColumnInfo(name = STREAM_REPEAT_COUNT) private var repeatCount: Long) {
+ fun getStreamUid(): Long {
+ return streamUid
+ }
+
+ fun setStreamUid(streamUid: Long) {
+ this.streamUid = streamUid
+ }
+
+ fun getAccessDate(): OffsetDateTime {
+ return accessDate
+ }
+
+ fun setAccessDate(accessDate: OffsetDateTime) {
+ this.accessDate = accessDate
+ }
+
+ fun getRepeatCount(): Long {
+ return repeatCount
+ }
+
+ fun setRepeatCount(repeatCount: Long) {
+ this.repeatCount = repeatCount
+ }
+
+ companion object {
+ val STREAM_HISTORY_TABLE: String = "stream_history"
+ val JOIN_STREAM_ID: String = "stream_id"
+ val STREAM_ACCESS_DATE: String = "access_date"
+ val STREAM_REPEAT_COUNT: String = "repeat_count"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java
deleted file mode 100644
index 3be85e6e1cb..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.schabi.newpipe.database.playlist;
-
-import androidx.room.ColumnInfo;
-
-/**
- * This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
- * how many times a specific stream is already contained inside a local playlist. Used to be able
- * to grey out playlists which already contain the current stream in the playlist append dialog.
- * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
- */
-public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
- public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
- @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
- public final long timesStreamIsContained;
-
- @SuppressWarnings("checkstyle:ParameterNumber")
- public PlaylistDuplicatesEntry(final long uid,
- final String name,
- final String thumbnailUrl,
- final boolean isThumbnailPermanent,
- final long thumbnailStreamId,
- final long displayIndex,
- final long streamCount,
- final long timesStreamIsContained) {
- super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
- streamCount);
- this.timesStreamIsContained = timesStreamIsContained;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt
new file mode 100644
index 00000000000..a434d199950
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt
@@ -0,0 +1,23 @@
+package org.schabi.newpipe.database.playlist
+
+import androidx.room.ColumnInfo
+
+/**
+ * This class adds a field to [PlaylistMetadataEntry] that contains an integer representing
+ * how many times a specific stream is already contained inside a local playlist. Used to be able
+ * to grey out playlists which already contain the current stream in the playlist append dialog.
+ * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager.getPlaylistDuplicates
+ */
+class PlaylistDuplicatesEntry(uid: Long,
+ name: String,
+ thumbnailUrl: String,
+ isThumbnailPermanent: Boolean,
+ thumbnailStreamId: Long,
+ displayIndex: Long,
+ streamCount: Long,
+ @field:ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) val timesStreamIsContained: Long) : PlaylistMetadataEntry(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
+ streamCount) {
+ companion object {
+ val PLAYLIST_TIMES_STREAM_IS_CONTAINED: String = "timesStreamIsContained"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
deleted file mode 100644
index 072c49e2c07..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.schabi.newpipe.database.playlist;
-
-import org.schabi.newpipe.database.LocalItem;
-
-public interface PlaylistLocalItem extends LocalItem {
- String getOrderingName();
-
- long getDisplayIndex();
-
- long getUid();
-
- void setDisplayIndex(long displayIndex);
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt
new file mode 100644
index 00000000000..d335c448950
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt
@@ -0,0 +1,10 @@
+package org.schabi.newpipe.database.playlist
+
+import org.schabi.newpipe.database.LocalItem
+
+open interface PlaylistLocalItem : LocalItem {
+ fun getOrderingName(): String
+ fun getDisplayIndex(): Long
+ fun getUid(): Long
+ fun setDisplayIndex(displayIndex: Long)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java
deleted file mode 100644
index 03a1e1e308a..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.schabi.newpipe.database.playlist;
-
-import androidx.room.ColumnInfo;
-
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
-
-public class PlaylistMetadataEntry implements PlaylistLocalItem {
- public static final String PLAYLIST_STREAM_COUNT = "streamCount";
-
- @ColumnInfo(name = PLAYLIST_ID)
- private final long uid;
- @ColumnInfo(name = PLAYLIST_NAME)
- public final String name;
- @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
- private final boolean isThumbnailPermanent;
- @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
- private final long thumbnailStreamId;
- @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
- public final String thumbnailUrl;
- @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
- private long displayIndex;
- @ColumnInfo(name = PLAYLIST_STREAM_COUNT)
- public final long streamCount;
-
- public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
- final boolean isThumbnailPermanent, final long thumbnailStreamId,
- final long displayIndex, final long streamCount) {
- this.uid = uid;
- this.name = name;
- this.thumbnailUrl = thumbnailUrl;
- this.isThumbnailPermanent = isThumbnailPermanent;
- this.thumbnailStreamId = thumbnailStreamId;
- this.displayIndex = displayIndex;
- this.streamCount = streamCount;
- }
-
- @Override
- public LocalItemType getLocalItemType() {
- return LocalItemType.PLAYLIST_LOCAL_ITEM;
- }
-
- @Override
- public String getOrderingName() {
- return name;
- }
-
- public boolean isThumbnailPermanent() {
- return isThumbnailPermanent;
- }
-
- public long getThumbnailStreamId() {
- return thumbnailStreamId;
- }
-
- @Override
- public long getDisplayIndex() {
- return displayIndex;
- }
-
- @Override
- public long getUid() {
- return uid;
- }
-
- @Override
- public void setDisplayIndex(final long displayIndex) {
- this.displayIndex = displayIndex;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt
new file mode 100644
index 00000000000..a7b24124d0f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt
@@ -0,0 +1,41 @@
+package org.schabi.newpipe.database.playlist
+
+import androidx.room.ColumnInfo
+import org.schabi.newpipe.database.LocalItem.LocalItemType
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+
+open class PlaylistMetadataEntry(@field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_ID) private val uid: Long, @JvmField @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_NAME) val name: String, @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL) val thumbnailUrl: String,
+ @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT) private val isThumbnailPermanent: Boolean, @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID) private val thumbnailStreamId: Long,
+ @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX) private var displayIndex: Long, @field:ColumnInfo(name = PLAYLIST_STREAM_COUNT) val streamCount: Long) : PlaylistLocalItem {
+ public override fun getLocalItemType(): LocalItemType {
+ return LocalItemType.PLAYLIST_LOCAL_ITEM
+ }
+
+ public override fun getOrderingName(): String {
+ return name
+ }
+
+ fun isThumbnailPermanent(): Boolean {
+ return isThumbnailPermanent
+ }
+
+ fun getThumbnailStreamId(): Long {
+ return thumbnailStreamId
+ }
+
+ public override fun getDisplayIndex(): Long {
+ return displayIndex
+ }
+
+ public override fun getUid(): Long {
+ return uid
+ }
+
+ public override fun setDisplayIndex(displayIndex: Long) {
+ this.displayIndex = displayIndex
+ }
+
+ companion object {
+ val PLAYLIST_STREAM_COUNT: String = "streamCount"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java
deleted file mode 100644
index d8071e0af3a..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.schabi.newpipe.database.playlist.dao;
-
-import androidx.room.Dao;
-import androidx.room.Query;
-import androidx.room.Transaction;
-
-import org.schabi.newpipe.database.BasicDAO;
-import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
-
-@Dao
-public interface PlaylistDAO extends BasicDAO {
- @Override
- @Query("SELECT * FROM " + PLAYLIST_TABLE)
- Flowable> getAll();
-
- @Override
- @Query("DELETE FROM " + PLAYLIST_TABLE)
- int deleteAll();
-
- @Override
- default Flowable> listByService(final int serviceId) {
- throw new UnsupportedOperationException();
- }
-
- @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
- Flowable> getPlaylist(long playlistId);
-
- @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
- int deletePlaylist(long playlistId);
-
- @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
- Flowable getCount();
-
- @Transaction
- default long upsertPlaylist(final PlaylistEntity playlist) {
- final long playlistId = playlist.getUid();
-
- if (playlistId == -1) {
- // This situation is probably impossible.
- return insert(playlist);
- } else {
- update(playlist);
- return playlistId;
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt
new file mode 100644
index 00000000000..0bf9d86f251
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt
@@ -0,0 +1,40 @@
+package org.schabi.newpipe.database.playlist.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.BasicDAO
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+
+@Dao
+open interface PlaylistDAO : BasicDAO {
+ @Query("SELECT * FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
+ public override fun getAll(): Flowable?>?
+ @Query("DELETE FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
+ public override fun deleteAll(): Int
+ public override fun listByService(serviceId: Int): Flowable?>? {
+ throw UnsupportedOperationException()
+ }
+
+ @Query("SELECT * FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE + " WHERE " + PlaylistEntity.Companion.PLAYLIST_ID + " = :playlistId")
+ fun getPlaylist(playlistId: Long): Flowable>
+
+ @Query("DELETE FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE + " WHERE " + PlaylistEntity.Companion.PLAYLIST_ID + " = :playlistId")
+ fun deletePlaylist(playlistId: Long): Int
+
+ @Query("SELECT COUNT(*) FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
+ fun getCount(): Flowable
+
+ @Transaction
+ fun upsertPlaylist(playlist: PlaylistEntity): Long {
+ val playlistId: Long = playlist.getUid()
+ if (playlistId == -1L) {
+ // This situation is probably impossible.
+ return insert(playlist)
+ } else {
+ update(playlist)
+ return playlistId
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java
deleted file mode 100644
index 8ab8a2afd33..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package org.schabi.newpipe.database.playlist.dao;
-
-import androidx.room.Dao;
-import androidx.room.Query;
-import androidx.room.Transaction;
-
-import org.schabi.newpipe.database.BasicDAO;
-import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
-
-@Dao
-public interface PlaylistRemoteDAO extends BasicDAO {
- @Override
- @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
- Flowable> getAll();
-
- @Override
- @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
- int deleteAll();
-
- @Override
- @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
- + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
- Flowable> listByService(int serviceId);
-
- @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
- + REMOTE_PLAYLIST_ID + " = :playlistId")
- Flowable> getPlaylist(long playlistId);
-
- @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
- + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
- Flowable> getPlaylist(long serviceId, String url);
-
- @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
- + " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
- Flowable> getPlaylists();
-
- @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
- + " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
- + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
- Long getPlaylistIdInternal(long serviceId, String url);
-
- @Transaction
- default long upsert(final PlaylistRemoteEntity playlist) {
- final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
-
- if (playlistId == null) {
- return insert(playlist);
- } else {
- playlist.setUid(playlistId);
- update(playlist);
- return playlistId;
- }
- }
-
- @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
- + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
- int deletePlaylist(long playlistId);
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt
new file mode 100644
index 00000000000..b53060e17b9
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt
@@ -0,0 +1,53 @@
+package org.schabi.newpipe.database.playlist.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.BasicDAO
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
+
+@Dao
+open interface PlaylistRemoteDAO : BasicDAO {
+ @Query("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE)
+ public override fun getAll(): Flowable?>?
+ @Query("DELETE FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE)
+ public override fun deleteAll(): Int
+
+ @Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
+ + " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
+ public override fun listByService(serviceId: Int): Flowable?>?
+
+ @Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE + " WHERE "
+ + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " = :playlistId"))
+ fun getPlaylist(playlistId: Long): Flowable?>?
+
+ @Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE + " WHERE "
+ + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL + " = :url AND " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
+ fun getPlaylist(serviceId: Long, url: String?): Flowable?>
+
+ @Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
+ + " ORDER BY " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_DISPLAY_INDEX))
+ fun getPlaylists(): Flowable?>
+
+ @Query(("SELECT " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
+ + " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL + " = :url "
+ + "AND " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
+ fun getPlaylistIdInternal(serviceId: Long, url: String?): Long
+
+ @Transaction
+ fun upsert(playlist: PlaylistRemoteEntity): Long {
+ val playlistId: Long = getPlaylistIdInternal(playlist.getServiceId().toLong(), playlist.getUrl())
+ if (playlistId == null) {
+ return insert(playlist)
+ } else {
+ playlist.setUid(playlistId)
+ update(playlist)
+ return playlistId
+ }
+ }
+
+ @Query(("DELETE FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
+ + " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " = :playlistId"))
+ fun deletePlaylist(playlistId: Long): Int
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java
deleted file mode 100644
index 85b891770ea..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package org.schabi.newpipe.database.playlist.dao;
-
-import androidx.room.Dao;
-import androidx.room.Query;
-import androidx.room.RewriteQueriesToDropUnusedColumns;
-import androidx.room.Transaction;
-
-import org.schabi.newpipe.database.BasicDAO;
-import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
-import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
-import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
-import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
-import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
-import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
-import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
-
-@Dao
-public interface PlaylistStreamDAO extends BasicDAO {
- @Override
- @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
- Flowable> getAll();
-
- @Override
- @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
- int deleteAll();
-
- @Override
- default Flowable> listByService(final int serviceId) {
- throw new UnsupportedOperationException();
- }
-
- @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
- + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
- void deleteBatch(long playlistId);
-
- @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
- + " FROM " + PLAYLIST_STREAM_JOIN_TABLE
- + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
- Flowable getMaximumIndexOf(long playlistId);
-
- @Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID
- + " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END"
- + " FROM " + STREAM_TABLE
- + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
- + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId "
- + " LIMIT 1"
- )
- Flowable getAutomaticThumbnailStreamId(long playlistId);
-
- @RewriteQueriesToDropUnusedColumns
- @Transaction
- @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
- // get ids of streams of the given playlist
- + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
- + " FROM " + PLAYLIST_STREAM_JOIN_TABLE
- + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
-
- // then merge with the stream metadata
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
-
- + " LEFT JOIN "
- + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
- + STREAM_PROGRESS_MILLIS
- + " FROM " + STREAM_STATE_TABLE + " )"
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
-
- + " ORDER BY " + JOIN_INDEX + " ASC")
- Flowable> getOrderedStreamsOf(long playlistId);
-
- @Transaction
- @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
- + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
- + PLAYLIST_DISPLAY_INDEX + ", "
-
- + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
- + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
- + " ELSE (SELECT " + STREAM_THUMBNAIL_URL
- + " FROM " + STREAM_TABLE
- + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
- + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
-
- + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
- + " FROM " + PLAYLIST_TABLE
- + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
- + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
- + " GROUP BY " + PLAYLIST_ID
- + " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
- Flowable> getPlaylistMetadata();
-
- @RewriteQueriesToDropUnusedColumns
- @Transaction
- @Query("SELECT *, MIN(" + JOIN_INDEX + ")"
- + " FROM " + STREAM_TABLE + " INNER JOIN"
- + " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
- + " FROM " + PLAYLIST_STREAM_JOIN_TABLE
- + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
- + " LEFT JOIN "
- + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
- + STREAM_PROGRESS_MILLIS
- + " FROM " + STREAM_STATE_TABLE + " )"
- + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
- + " GROUP BY " + STREAM_ID
- + " ORDER BY MIN(" + JOIN_INDEX + ") ASC")
- Flowable> getStreamsWithoutDuplicates(long playlistId);
-
- @Transaction
- @Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
- + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
- + PLAYLIST_DISPLAY_INDEX + ", "
-
- + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
- + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
- + " ELSE (SELECT " + STREAM_THUMBNAIL_URL
- + " FROM " + STREAM_TABLE
- + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
- + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
-
- + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
- + "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
- + PLAYLIST_TIMES_STREAM_IS_CONTAINED
-
- + " FROM " + PLAYLIST_TABLE
- + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
- + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
-
- + " LEFT JOIN " + STREAM_TABLE
- + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
- + " AND :streamUrl = :streamUrl"
-
- + " GROUP BY " + JOIN_PLAYLIST_ID
- + " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
- Flowable> getPlaylistDuplicatesMetadata(String streamUrl);
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt
new file mode 100644
index 00000000000..c135fecdb96
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt
@@ -0,0 +1,117 @@
+package org.schabi.newpipe.database.playlist.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.RewriteQueriesToDropUnusedColumns
+import androidx.room.Transaction
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.BasicDAO
+import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry
+import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
+import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+
+@Dao
+open interface PlaylistStreamDAO : BasicDAO {
+ @Query("SELECT * FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE)
+ public override fun getAll(): Flowable?>?
+ @Query("DELETE FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE)
+ public override fun deleteAll(): Int
+ public override fun listByService(serviceId: Int): Flowable?>? {
+ throw UnsupportedOperationException()
+ }
+
+ @Query(("DELETE FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId"))
+ fun deleteBatch(playlistId: Long)
+
+ @Query(("SELECT COALESCE(MAX(" + PlaylistStreamEntity.Companion.JOIN_INDEX + "), -1)"
+ + " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId"))
+ fun getMaximumIndexOf(playlistId: Long): Flowable
+
+ @Query(("SELECT CASE WHEN COUNT(*) != 0 then " + StreamEntity.STREAM_ID
+ + " ELSE " + PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " END"
+ + " FROM " + StreamEntity.STREAM_TABLE
+ + " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
+ + " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId "
+ + " LIMIT 1"))
+ fun getAutomaticThumbnailStreamId(playlistId: Long): Flowable
+
+ @RewriteQueriesToDropUnusedColumns
+ @Transaction
+ @Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE + " INNER JOIN " // get ids of streams of the given playlist
+ + "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + "," + PlaylistStreamEntity.Companion.JOIN_INDEX
+ + " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId)" // then merge with the stream metadata
+ + " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
+ + " LEFT JOIN "
+ + "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
+ + StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
+ + " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS
+ + " ORDER BY " + PlaylistStreamEntity.Companion.JOIN_INDEX + " ASC"))
+ fun getOrderedStreamsOf(playlistId: Long): Flowable?>
+
+ @Transaction
+ @Query(("SELECT " + PlaylistEntity.Companion.PLAYLIST_ID + ", " + PlaylistEntity.Companion.PLAYLIST_NAME + ", "
+ + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT + ", " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX + ", "
+ + " CASE WHEN " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ + PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + PlaylistEntity.Companion.DEFAULT_THUMBNAIL + "'"
+ + " ELSE (SELECT " + StreamEntity.STREAM_THUMBNAIL_URL
+ + " FROM " + StreamEntity.STREAM_TABLE
+ + " WHERE " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID
+ + " ) END AS " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL + ", "
+ + "COALESCE(COUNT(" + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + "), 0) AS " + PlaylistMetadataEntry.Companion.PLAYLIST_STREAM_COUNT
+ + " FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE
+ + " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " ON " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + " = " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
+ + " GROUP BY " + PlaylistEntity.Companion.PLAYLIST_ID
+ + " ORDER BY " + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX))
+ fun getPlaylistMetadata(): Flowable?>
+
+ @RewriteQueriesToDropUnusedColumns
+ @Transaction
+ @Query(("SELECT *, MIN(" + PlaylistStreamEntity.Companion.JOIN_INDEX + ")"
+ + " FROM " + StreamEntity.STREAM_TABLE + " INNER JOIN"
+ + " (SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + "," + PlaylistStreamEntity.Companion.JOIN_INDEX
+ + " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId)"
+ + " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
+ + " LEFT JOIN "
+ + "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
+ + StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
+ + " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
+ + " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS
+ + " GROUP BY " + StreamEntity.STREAM_ID
+ + " ORDER BY MIN(" + PlaylistStreamEntity.Companion.JOIN_INDEX + ") ASC"))
+ fun getStreamsWithoutDuplicates(playlistId: Long): Flowable?>
+
+ @Transaction
+ @Query(("SELECT " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + ", " + PlaylistEntity.Companion.PLAYLIST_NAME + ", "
+ + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT + ", " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX + ", "
+ + " CASE WHEN " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ + PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + PlaylistEntity.Companion.DEFAULT_THUMBNAIL + "'"
+ + " ELSE (SELECT " + StreamEntity.STREAM_THUMBNAIL_URL
+ + " FROM " + StreamEntity.STREAM_TABLE
+ + " WHERE " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID
+ + " ) END AS " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL + ", "
+ + "COALESCE(COUNT(" + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + "), 0) AS " + PlaylistMetadataEntry.Companion.PLAYLIST_STREAM_COUNT + ", "
+ + "COALESCE(SUM(" + StreamEntity.STREAM_URL + " = :streamUrl), 0) AS "
+ + PlaylistDuplicatesEntry.Companion.PLAYLIST_TIMES_STREAM_IS_CONTAINED
+ + " FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE
+ + " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
+ + " ON " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + " = " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
+ + " LEFT JOIN " + StreamEntity.STREAM_TABLE
+ + " ON " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
+ + " AND :streamUrl = :streamUrl"
+ + " GROUP BY " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
+ + " ORDER BY " + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX))
+ fun getPlaylistDuplicatesMetadata(streamUrl: String?): Flowable?>
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java
deleted file mode 100644
index e0c1a06b79b..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package org.schabi.newpipe.database.playlist.model;
-
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.Ignore;
-import androidx.room.PrimaryKey;
-
-import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
-
-@Entity(tableName = PLAYLIST_TABLE)
-public class PlaylistEntity {
-
- public static final String DEFAULT_THUMBNAIL = "drawable://"
- + R.drawable.placeholder_thumbnail_playlist;
- public static final long DEFAULT_THUMBNAIL_ID = -1;
-
- public static final String PLAYLIST_TABLE = "playlists";
- public static final String PLAYLIST_ID = "uid";
- public static final String PLAYLIST_NAME = "name";
- public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
- public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
- public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
- public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
-
- @PrimaryKey(autoGenerate = true)
- @ColumnInfo(name = PLAYLIST_ID)
- private long uid = 0;
-
- @ColumnInfo(name = PLAYLIST_NAME)
- private String name;
-
- @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
- private boolean isThumbnailPermanent;
-
- @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
- private long thumbnailStreamId;
-
- @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
- private long displayIndex;
-
- public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
- final long thumbnailStreamId, final long displayIndex) {
- this.name = name;
- this.isThumbnailPermanent = isThumbnailPermanent;
- this.thumbnailStreamId = thumbnailStreamId;
- this.displayIndex = displayIndex;
- }
-
- @Ignore
- public PlaylistEntity(final PlaylistMetadataEntry item) {
- this.uid = item.getUid();
- this.name = item.name;
- this.isThumbnailPermanent = item.isThumbnailPermanent();
- this.thumbnailStreamId = item.getThumbnailStreamId();
- this.displayIndex = item.getDisplayIndex();
- }
-
- public long getUid() {
- return uid;
- }
-
- public void setUid(final long uid) {
- this.uid = uid;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(final String name) {
- this.name = name;
- }
-
- public long getThumbnailStreamId() {
- return thumbnailStreamId;
- }
-
- public void setThumbnailStreamId(final long thumbnailStreamId) {
- this.thumbnailStreamId = thumbnailStreamId;
- }
-
- public boolean getIsThumbnailPermanent() {
- return isThumbnailPermanent;
- }
-
- public void setIsThumbnailPermanent(final boolean isThumbnailSet) {
- this.isThumbnailPermanent = isThumbnailSet;
- }
-
- public long getDisplayIndex() {
- return displayIndex;
- }
-
- public void setDisplayIndex(final long displayIndex) {
- this.displayIndex = displayIndex;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt
new file mode 100644
index 00000000000..87a4f1b68fe
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt
@@ -0,0 +1,98 @@
+package org.schabi.newpipe.database.playlist.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.PrimaryKey
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+
+@Entity(tableName = PlaylistEntity.PLAYLIST_TABLE)
+class PlaylistEntity {
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = PLAYLIST_ID)
+ private var uid: Long = 0
+
+ @ColumnInfo(name = PLAYLIST_NAME)
+ private var name: String?
+
+ @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
+ private var isThumbnailPermanent: Boolean
+
+ @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
+ private var thumbnailStreamId: Long
+
+ @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
+ private var displayIndex: Long
+
+ constructor(name: String?, isThumbnailPermanent: Boolean,
+ thumbnailStreamId: Long, displayIndex: Long) {
+ this.name = name
+ this.isThumbnailPermanent = isThumbnailPermanent
+ this.thumbnailStreamId = thumbnailStreamId
+ this.displayIndex = displayIndex
+ }
+
+ @Ignore
+ constructor(item: PlaylistMetadataEntry) {
+ uid = item.getUid()
+ name = item.name
+ isThumbnailPermanent = item.isThumbnailPermanent()
+ thumbnailStreamId = item.getThumbnailStreamId()
+ displayIndex = item.getDisplayIndex()
+ }
+
+ fun getUid(): Long {
+ return uid
+ }
+
+ fun setUid(uid: Long) {
+ this.uid = uid
+ }
+
+ fun getName(): String? {
+ return name
+ }
+
+ fun setName(name: String?) {
+ this.name = name
+ }
+
+ fun getThumbnailStreamId(): Long {
+ return thumbnailStreamId
+ }
+
+ fun setThumbnailStreamId(thumbnailStreamId: Long) {
+ this.thumbnailStreamId = thumbnailStreamId
+ }
+
+ fun getIsThumbnailPermanent(): Boolean {
+ return isThumbnailPermanent
+ }
+
+ fun setIsThumbnailPermanent(isThumbnailSet: Boolean) {
+ isThumbnailPermanent = isThumbnailSet
+ }
+
+ fun getDisplayIndex(): Long {
+ return displayIndex
+ }
+
+ fun setDisplayIndex(displayIndex: Long) {
+ this.displayIndex = displayIndex
+ }
+
+ companion object {
+ val DEFAULT_THUMBNAIL: String = ("drawable://"
+ + R.drawable.placeholder_thumbnail_playlist)
+ val DEFAULT_THUMBNAIL_ID: Long = -1
+ val PLAYLIST_TABLE: String = "playlists"
+ val PLAYLIST_ID: String = "uid"
+ val PLAYLIST_NAME: String = "name"
+ val PLAYLIST_THUMBNAIL_URL: String = "thumbnail_url"
+ val PLAYLIST_DISPLAY_INDEX: String = "display_index"
+ val PLAYLIST_THUMBNAIL_PERMANENT: String = "is_thumbnail_permanent"
+ val PLAYLIST_THUMBNAIL_STREAM_ID: String = "thumbnail_stream_id"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java
deleted file mode 100644
index 60027a057f2..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package org.schabi.newpipe.database.playlist.model;
-
-import android.text.TextUtils;
-
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.Ignore;
-import androidx.room.Index;
-import androidx.room.PrimaryKey;
-
-import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
-import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
-import org.schabi.newpipe.util.Constants;
-import org.schabi.newpipe.util.image.ImageStrategy;
-
-import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
-import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
-
-@Entity(tableName = REMOTE_PLAYLIST_TABLE,
- indices = {
- @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
- })
-public class PlaylistRemoteEntity implements PlaylistLocalItem {
- public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists";
- public static final String REMOTE_PLAYLIST_ID = "uid";
- public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
- public static final String REMOTE_PLAYLIST_NAME = "name";
- public static final String REMOTE_PLAYLIST_URL = "url";
- public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
- public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
- public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
- public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
-
- @PrimaryKey(autoGenerate = true)
- @ColumnInfo(name = REMOTE_PLAYLIST_ID)
- private long uid = 0;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
- private int serviceId = Constants.NO_SERVICE_ID;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_NAME)
- private String name;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_URL)
- private String url;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
- private String thumbnailUrl;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
- private String uploader;
-
- @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
- private long displayIndex = -1; // Make sure the new item is on the top
-
- @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
- private Long streamCount;
-
- public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
- final String thumbnailUrl, final String uploader,
- final Long streamCount) {
- this.serviceId = serviceId;
- this.name = name;
- this.url = url;
- this.thumbnailUrl = thumbnailUrl;
- this.uploader = uploader;
- this.streamCount = streamCount;
- }
-
- @Ignore
- public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
- final String thumbnailUrl, final String uploader,
- final long displayIndex, final Long streamCount) {
- this.serviceId = serviceId;
- this.name = name;
- this.url = url;
- this.thumbnailUrl = thumbnailUrl;
- this.uploader = uploader;
- this.displayIndex = displayIndex;
- this.streamCount = streamCount;
- }
-
- @Ignore
- public PlaylistRemoteEntity(final PlaylistInfo info) {
- this(info.getServiceId(), info.getName(), info.getUrl(),
- // use uploader avatar when no thumbnail is available
- ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
- ? info.getUploaderAvatars() : info.getThumbnails()),
- info.getUploaderName(), info.getStreamCount());
- }
-
- @Ignore
- public boolean isIdenticalTo(final PlaylistInfo info) {
- /*
- * Returns boolean comparing the online playlist and the local copy.
- * (False if info changed such as playlist name or track count)
- */
- return getServiceId() == info.getServiceId()
- && getStreamCount() == info.getStreamCount()
- && TextUtils.equals(getName(), info.getName())
- && TextUtils.equals(getUrl(), info.getUrl())
- // we want to update the local playlist data even when either the remote thumbnail
- // URL changes, or the preferred image quality setting is changed by the user
- && TextUtils.equals(getThumbnailUrl(),
- ImageStrategy.imageListToDbUrl(info.getThumbnails()))
- && TextUtils.equals(getUploader(), info.getUploaderName());
- }
-
- @Override
- public long getUid() {
- return uid;
- }
-
- public void setUid(final long uid) {
- this.uid = uid;
- }
-
- public int getServiceId() {
- return serviceId;
- }
-
- public void setServiceId(final int serviceId) {
- this.serviceId = serviceId;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(final String name) {
- this.name = name;
- }
-
- public String getThumbnailUrl() {
- return thumbnailUrl;
- }
-
- public void setThumbnailUrl(final String thumbnailUrl) {
- this.thumbnailUrl = thumbnailUrl;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(final String url) {
- this.url = url;
- }
-
- public String getUploader() {
- return uploader;
- }
-
- public void setUploader(final String uploader) {
- this.uploader = uploader;
- }
-
- @Override
- public long getDisplayIndex() {
- return displayIndex;
- }
-
- @Override
- public void setDisplayIndex(final long displayIndex) {
- this.displayIndex = displayIndex;
- }
-
- public Long getStreamCount() {
- return streamCount;
- }
-
- public void setStreamCount(final Long streamCount) {
- this.streamCount = streamCount;
- }
-
- @Override
- public LocalItemType getLocalItemType() {
- return PLAYLIST_REMOTE_ITEM;
- }
-
- @Override
- public String getOrderingName() {
- return name;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt
new file mode 100644
index 00000000000..9f49102570a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt
@@ -0,0 +1,170 @@
+package org.schabi.newpipe.database.playlist.model
+
+import android.text.TextUtils
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import org.schabi.newpipe.database.LocalItem.LocalItemType
+import org.schabi.newpipe.database.playlist.PlaylistLocalItem
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
+import org.schabi.newpipe.extractor.playlist.PlaylistInfo
+import org.schabi.newpipe.util.image.ImageStrategy
+
+@Entity(tableName = PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE, indices = [Index(value = [PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID, PlaylistRemoteEntity.REMOTE_PLAYLIST_URL], unique = true)])
+class PlaylistRemoteEntity : PlaylistLocalItem {
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = REMOTE_PLAYLIST_ID)
+ private var uid: Long = 0
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
+ private var serviceId: Int = NO_SERVICE_ID
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_NAME)
+ private var name: String
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_URL)
+ private var url: String
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
+ private var thumbnailUrl: String?
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
+ private var uploader: String
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
+ private var displayIndex: Long = -1 // Make sure the new item is on the top
+
+ @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
+ private var streamCount: Long
+
+ constructor(serviceId: Int, name: String, url: String,
+ thumbnailUrl: String?, uploader: String,
+ streamCount: Long) {
+ this.serviceId = serviceId
+ this.name = name
+ this.url = url
+ this.thumbnailUrl = thumbnailUrl
+ this.uploader = uploader
+ this.streamCount = streamCount
+ }
+
+ @Ignore
+ constructor(serviceId: Int, name: String, url: String,
+ thumbnailUrl: String?, uploader: String,
+ displayIndex: Long, streamCount: Long) {
+ this.serviceId = serviceId
+ this.name = name
+ this.url = url
+ this.thumbnailUrl = thumbnailUrl
+ this.uploader = uploader
+ this.displayIndex = displayIndex
+ this.streamCount = streamCount
+ }
+
+ @Ignore
+ constructor(info: PlaylistInfo) : this(info.getServiceId(), info.getName(), info.getUrl(), // use uploader avatar when no thumbnail is available
+ ImageStrategy.imageListToDbUrl(if (info.getThumbnails().isEmpty()) info.getUploaderAvatars() else info.getThumbnails()),
+ info.getUploaderName(), info.getStreamCount())
+
+ @Ignore
+ fun isIdenticalTo(info: PlaylistInfo): Boolean {
+ /*
+ * Returns boolean comparing the online playlist and the local copy.
+ * (False if info changed such as playlist name or track count)
+ */
+ return ((getServiceId() == info.getServiceId()
+ ) && (getStreamCount() == info.getStreamCount()
+ ) && TextUtils.equals(getName(), info.getName())
+ && TextUtils.equals(getUrl(), info.getUrl()) // we want to update the local playlist data even when either the remote thumbnail
+ // URL changes, or the preferred image quality setting is changed by the user
+ && TextUtils.equals(getThumbnailUrl(),
+ ImageStrategy.imageListToDbUrl(info.getThumbnails()))
+ && TextUtils.equals(getUploader(), info.getUploaderName()))
+ }
+
+ public override fun getUid(): Long {
+ return uid
+ }
+
+ fun setUid(uid: Long) {
+ this.uid = uid
+ }
+
+ fun getServiceId(): Int {
+ return serviceId
+ }
+
+ fun setServiceId(serviceId: Int) {
+ this.serviceId = serviceId
+ }
+
+ fun getName(): String {
+ return name
+ }
+
+ fun setName(name: String) {
+ this.name = name
+ }
+
+ fun getThumbnailUrl(): String? {
+ return thumbnailUrl
+ }
+
+ fun setThumbnailUrl(thumbnailUrl: String?) {
+ this.thumbnailUrl = thumbnailUrl
+ }
+
+ fun getUrl(): String {
+ return url
+ }
+
+ fun setUrl(url: String) {
+ this.url = url
+ }
+
+ fun getUploader(): String {
+ return uploader
+ }
+
+ fun setUploader(uploader: String) {
+ this.uploader = uploader
+ }
+
+ public override fun getDisplayIndex(): Long {
+ return displayIndex
+ }
+
+ public override fun setDisplayIndex(displayIndex: Long) {
+ this.displayIndex = displayIndex
+ }
+
+ fun getStreamCount(): Long {
+ return streamCount
+ }
+
+ fun setStreamCount(streamCount: Long) {
+ this.streamCount = streamCount
+ }
+
+ public override fun getLocalItemType(): LocalItemType {
+ return LocalItemType.PLAYLIST_REMOTE_ITEM
+ }
+
+ public override fun getOrderingName(): String {
+ return name
+ }
+
+ companion object {
+ val REMOTE_PLAYLIST_TABLE: String = "remote_playlists"
+ val REMOTE_PLAYLIST_ID: String = "uid"
+ val REMOTE_PLAYLIST_SERVICE_ID: String = "service_id"
+ val REMOTE_PLAYLIST_NAME: String = "name"
+ val REMOTE_PLAYLIST_URL: String = "url"
+ val REMOTE_PLAYLIST_THUMBNAIL_URL: String = "thumbnail_url"
+ val REMOTE_PLAYLIST_UPLOADER_NAME: String = "uploader"
+ val REMOTE_PLAYLIST_DISPLAY_INDEX: String = "display_index"
+ val REMOTE_PLAYLIST_STREAM_COUNT: String = "stream_count"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java
deleted file mode 100644
index f3208b6d517..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.schabi.newpipe.database.playlist.model;
-
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.ForeignKey;
-import androidx.room.Index;
-
-import org.schabi.newpipe.database.stream.model.StreamEntity;
-
-import static androidx.room.ForeignKey.CASCADE;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
-
-@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE,
- primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX},
- indices = {
- @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true),
- @Index(value = {JOIN_STREAM_ID})
- },
- foreignKeys = {
- @ForeignKey(entity = PlaylistEntity.class,
- parentColumns = PlaylistEntity.PLAYLIST_ID,
- childColumns = JOIN_PLAYLIST_ID,
- onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
- @ForeignKey(entity = StreamEntity.class,
- parentColumns = StreamEntity.STREAM_ID,
- childColumns = JOIN_STREAM_ID,
- onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
- })
-public class PlaylistStreamEntity {
- public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
- public static final String JOIN_PLAYLIST_ID = "playlist_id";
- public static final String JOIN_STREAM_ID = "stream_id";
- public static final String JOIN_INDEX = "join_index";
-
- @ColumnInfo(name = JOIN_PLAYLIST_ID)
- private long playlistUid;
-
- @ColumnInfo(name = JOIN_STREAM_ID)
- private long streamUid;
-
- @ColumnInfo(name = JOIN_INDEX)
- private int index;
-
- public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) {
- this.playlistUid = playlistUid;
- this.streamUid = streamUid;
- this.index = index;
- }
-
- public long getPlaylistUid() {
- return playlistUid;
- }
-
- public void setPlaylistUid(final long playlistUid) {
- this.playlistUid = playlistUid;
- }
-
- public long getStreamUid() {
- return streamUid;
- }
-
- public void setStreamUid(final long streamUid) {
- this.streamUid = streamUid;
- }
-
- public int getIndex() {
- return index;
- }
-
- public void setIndex(final int index) {
- this.index = index;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt
new file mode 100644
index 00000000000..9ff4f022887
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt
@@ -0,0 +1,43 @@
+package org.schabi.newpipe.database.playlist.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import org.schabi.newpipe.database.playlist.model.PlaylistEntity
+import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
+import org.schabi.newpipe.database.stream.model.StreamEntity
+
+@Entity(tableName = PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE, primaryKeys = [PlaylistStreamEntity.JOIN_PLAYLIST_ID, PlaylistStreamEntity.JOIN_INDEX], indices = [Index(value = [PlaylistStreamEntity.JOIN_PLAYLIST_ID, PlaylistStreamEntity.JOIN_INDEX], unique = true), Index(value = [PlaylistStreamEntity.JOIN_STREAM_ID])], foreignKeys = [ForeignKey(entity = PlaylistEntity::class, parentColumns = PlaylistEntity.Companion.PLAYLIST_ID, childColumns = PlaylistStreamEntity.JOIN_PLAYLIST_ID, onDelete = CASCADE, onUpdate = CASCADE, deferred = true), ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = PlaylistStreamEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE, deferred = true)])
+class PlaylistStreamEntity(@field:ColumnInfo(name = JOIN_PLAYLIST_ID) private var playlistUid: Long, @field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long, @field:ColumnInfo(name = JOIN_INDEX) private var index: Int) {
+ fun getPlaylistUid(): Long {
+ return playlistUid
+ }
+
+ fun setPlaylistUid(playlistUid: Long) {
+ this.playlistUid = playlistUid
+ }
+
+ fun getStreamUid(): Long {
+ return streamUid
+ }
+
+ fun setStreamUid(streamUid: Long) {
+ this.streamUid = streamUid
+ }
+
+ fun getIndex(): Int {
+ return index
+ }
+
+ fun setIndex(index: Int) {
+ this.index = index
+ }
+
+ companion object {
+ val PLAYLIST_STREAM_JOIN_TABLE: String = "playlist_stream_join"
+ val JOIN_PLAYLIST_ID: String = "playlist_id"
+ val JOIN_STREAM_ID: String = "stream_id"
+ val JOIN_INDEX: String = "join_index"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java
deleted file mode 100644
index 06371248d62..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.schabi.newpipe.database.stream.dao;
-
-import androidx.room.Dao;
-import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
-import androidx.room.Query;
-import androidx.room.Transaction;
-
-import org.schabi.newpipe.database.BasicDAO;
-import org.schabi.newpipe.database.stream.model.StreamStateEntity;
-
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
-
-@Dao
-public interface StreamStateDAO extends BasicDAO {
- @Override
- @Query("SELECT * FROM " + STREAM_STATE_TABLE)
- Flowable> getAll();
-
- @Override
- @Query("DELETE FROM " + STREAM_STATE_TABLE)
- int deleteAll();
-
- @Override
- default Flowable> listByService(final int serviceId) {
- throw new UnsupportedOperationException();
- }
-
- @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
- Flowable> getState(long streamId);
-
- @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
- int deleteState(long streamId);
-
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- void silentInsertInternal(StreamStateEntity streamState);
-
- @Transaction
- default long upsert(final StreamStateEntity stream) {
- silentInsertInternal(stream);
- return update(stream);
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt
new file mode 100644
index 00000000000..5e6606897cd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt
@@ -0,0 +1,36 @@
+package org.schabi.newpipe.database.stream.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import io.reactivex.rxjava3.core.Flowable
+import org.schabi.newpipe.database.BasicDAO
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+
+@Dao
+open interface StreamStateDAO : BasicDAO {
+ @Query("SELECT * FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE)
+ public override fun getAll(): Flowable?>?
+ @Query("DELETE FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE)
+ public override fun deleteAll(): Int
+ public override fun listByService(serviceId: Int): Flowable?>? {
+ throw UnsupportedOperationException()
+ }
+
+ @Query("SELECT * FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.Companion.JOIN_STREAM_ID + " = :streamId")
+ fun getState(streamId: Long): Flowable?>
+
+ @Query("DELETE FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.Companion.JOIN_STREAM_ID + " = :streamId")
+ fun deleteState(streamId: Long): Int
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ fun silentInsertInternal(streamState: StreamStateEntity?)
+
+ @Transaction
+ fun upsert(stream: StreamStateEntity?): Long {
+ silentInsertInternal(stream)
+ return update(stream).toLong()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java
deleted file mode 100644
index 627acea45a4..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package org.schabi.newpipe.database.stream.model;
-
-import androidx.annotation.Nullable;
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.ForeignKey;
-
-import java.util.Objects;
-
-import static androidx.room.ForeignKey.CASCADE;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
-
-@Entity(tableName = STREAM_STATE_TABLE,
- primaryKeys = {JOIN_STREAM_ID},
- foreignKeys = {
- @ForeignKey(entity = StreamEntity.class,
- parentColumns = StreamEntity.STREAM_ID,
- childColumns = JOIN_STREAM_ID,
- onDelete = CASCADE, onUpdate = CASCADE)
- })
-public class StreamStateEntity {
- public static final String STREAM_STATE_TABLE = "stream_state";
- public static final String JOIN_STREAM_ID = "stream_id";
- // This additional field is required for the SQL query because 'stream_id' is used
- // for some other joins already
- public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
- public static final String STREAM_PROGRESS_MILLIS = "progress_time";
-
- /**
- * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
- */
- public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
-
- /**
- * Stream will be considered finished if the playback time left exceeds this threshold
- * (60000ms = 60s).
- * @see #isFinished(long)
- * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
- * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
- */
- public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
-
- @ColumnInfo(name = JOIN_STREAM_ID)
- private long streamUid;
-
- @ColumnInfo(name = STREAM_PROGRESS_MILLIS)
- private long progressMillis;
-
- public StreamStateEntity(final long streamUid, final long progressMillis) {
- this.streamUid = streamUid;
- this.progressMillis = progressMillis;
- }
-
- public long getStreamUid() {
- return streamUid;
- }
-
- public void setStreamUid(final long streamUid) {
- this.streamUid = streamUid;
- }
-
- public long getProgressMillis() {
- return progressMillis;
- }
-
- public void setProgressMillis(final long progressMillis) {
- this.progressMillis = progressMillis;
- }
-
- /**
- * The state will be considered valid, and thus be saved, if the progress is more than {@link
- * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
- * @param durationInSeconds the duration of the stream connected with this state, in seconds
- * @return whether this stream state entity should be saved or not
- */
- public boolean isValid(final long durationInSeconds) {
- return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
- || progressMillis > durationInSeconds * 1000 / 4;
- }
-
- /**
- * The video will be considered as finished, if the time left is less than {@link
- * #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
- * The state will be saved anyway, so that it can be shown under stream info items, but the
- * player will not resume if a state is considered as finished. Finished streams are also the
- * ones that can be filtered out in the feed fragment.
- * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
- * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
- * @param durationInSeconds the duration of the stream connected with this state, in seconds
- * @return whether the stream is finished or not
- */
- public boolean isFinished(final long durationInSeconds) {
- return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
- && progressMillis >= durationInSeconds * 1000 * 3 / 4;
- }
-
- @Override
- public boolean equals(@Nullable final Object obj) {
- if (obj instanceof StreamStateEntity) {
- return ((StreamStateEntity) obj).streamUid == streamUid
- && ((StreamStateEntity) obj).progressMillis == progressMillis;
- } else {
- return false;
- }
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(streamUid, progressMillis);
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt
new file mode 100644
index 00000000000..44d0684d472
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt
@@ -0,0 +1,89 @@
+package org.schabi.newpipe.database.stream.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+import java.util.Objects
+
+@Entity(tableName = StreamStateEntity.STREAM_STATE_TABLE, primaryKeys = [StreamStateEntity.JOIN_STREAM_ID], foreignKeys = [ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = StreamStateEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE)])
+class StreamStateEntity(@field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long, @field:ColumnInfo(name = STREAM_PROGRESS_MILLIS) private var progressMillis: Long) {
+ fun getStreamUid(): Long {
+ return streamUid
+ }
+
+ fun setStreamUid(streamUid: Long) {
+ this.streamUid = streamUid
+ }
+
+ fun getProgressMillis(): Long {
+ return progressMillis
+ }
+
+ fun setProgressMillis(progressMillis: Long) {
+ this.progressMillis = progressMillis
+ }
+
+ /**
+ * The state will be considered valid, and thus be saved, if the progress is more than [ ][.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS] or at least 1/4 of the video length.
+ * @param durationInSeconds the duration of the stream connected with this state, in seconds
+ * @return whether this stream state entity should be saved or not
+ */
+ fun isValid(durationInSeconds: Long): Boolean {
+ return (progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
+ || progressMillis > durationInSeconds * 1000 / 4)
+ }
+
+ /**
+ * The video will be considered as finished, if the time left is less than [ ][.PLAYBACK_FINISHED_END_MILLISECONDS] and the progress is at least 3/4 of the video length.
+ * The state will be saved anyway, so that it can be shown under stream info items, but the
+ * player will not resume if a state is considered as finished. Finished streams are also the
+ * ones that can be filtered out in the feed fragment.
+ * @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreams
+ * @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreamsForGroup
+ * @param durationInSeconds the duration of the stream connected with this state, in seconds
+ * @return whether the stream is finished or not
+ */
+ fun isFinished(durationInSeconds: Long): Boolean {
+ return (progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
+ && progressMillis >= durationInSeconds * 1000 * 3 / 4)
+ }
+
+ public override fun equals(obj: Any?): Boolean {
+ if (obj is StreamStateEntity) {
+ return (obj.streamUid == streamUid
+ && obj.progressMillis == progressMillis)
+ } else {
+ return false
+ }
+ }
+
+ public override fun hashCode(): Int {
+ return Objects.hash(streamUid, progressMillis)
+ }
+
+ companion object {
+ val STREAM_STATE_TABLE: String = "stream_state"
+ val JOIN_STREAM_ID: String = "stream_id"
+
+ // This additional field is required for the SQL query because 'stream_id' is used
+ // for some other joins already
+ val JOIN_STREAM_ID_ALIAS: String = "stream_id_alias"
+ val STREAM_PROGRESS_MILLIS: String = "progress_time"
+
+ /**
+ * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
+ */
+ val PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS: Long = 5000
+
+ /**
+ * Stream will be considered finished if the playback time left exceeds this threshold
+ * (60000ms = 60s).
+ * @see .isFinished
+ * @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreams
+ * @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreamsForGroup
+ */
+ val PLAYBACK_FINISHED_END_MILLISECONDS: Long = 60000
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java
deleted file mode 100644
index 07e0eb7d358..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.schabi.newpipe.database.subscription;
-
-import androidx.annotation.IntDef;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED})
-@Retention(RetentionPolicy.SOURCE)
-public @interface NotificationMode {
-
- int DISABLED = 0;
- int ENABLED = 1;
- //other values reserved for the future
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt
new file mode 100644
index 00000000000..7fc9050fbbd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt
@@ -0,0 +1,13 @@
+package org.schabi.newpipe.database.subscription
+
+import androidx.annotation.IntDef
+import org.schabi.newpipe.database.subscription.NotificationMode
+
+@IntDef([NotificationMode.DISABLED, NotificationMode.ENABLED])
+@Retention(AnnotationRetention.SOURCE)
+annotation class NotificationMode() {
+ companion object {
+ val DISABLED: Int = 0
+ val ENABLED: Int = 1 //other values reserved for the future
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java
deleted file mode 100644
index a61a22a8444..00000000000
--- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java
+++ /dev/null
@@ -1,198 +0,0 @@
-package org.schabi.newpipe.database.subscription;
-
-import androidx.annotation.NonNull;
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.Ignore;
-import androidx.room.Index;
-import androidx.room.PrimaryKey;
-
-import org.schabi.newpipe.extractor.channel.ChannelInfo;
-import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
-import org.schabi.newpipe.util.Constants;
-import org.schabi.newpipe.util.image.ImageStrategy;
-
-import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
-import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
-import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
-
-@Entity(tableName = SUBSCRIPTION_TABLE,
- indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
-public class SubscriptionEntity {
- public static final String SUBSCRIPTION_UID = "uid";
- public static final String SUBSCRIPTION_TABLE = "subscriptions";
- public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
- public static final String SUBSCRIPTION_URL = "url";
- public static final String SUBSCRIPTION_NAME = "name";
- public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
- public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
- public static final String SUBSCRIPTION_DESCRIPTION = "description";
- public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode";
-
- @PrimaryKey(autoGenerate = true)
- private long uid = 0;
-
- @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
- private int serviceId = Constants.NO_SERVICE_ID;
-
- @ColumnInfo(name = SUBSCRIPTION_URL)
- private String url;
-
- @ColumnInfo(name = SUBSCRIPTION_NAME)
- private String name;
-
- @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
- private String avatarUrl;
-
- @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
- private Long subscriberCount;
-
- @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
- private String description;
-
- @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
- private int notificationMode;
-
- @Ignore
- public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
- final SubscriptionEntity result = new SubscriptionEntity();
- result.setServiceId(info.getServiceId());
- result.setUrl(info.getUrl());
- result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
- info.getDescription(), info.getSubscriberCount());
- return result;
- }
-
- public long getUid() {
- return uid;
- }
-
- public void setUid(final long uid) {
- this.uid = uid;
- }
-
- public int getServiceId() {
- return serviceId;
- }
-
- public void setServiceId(final int serviceId) {
- this.serviceId = serviceId;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(final String url) {
- this.url = url;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(final String name) {
- this.name = name;
- }
-
- public String getAvatarUrl() {
- return avatarUrl;
- }
-
- public void setAvatarUrl(final String avatarUrl) {
- this.avatarUrl = avatarUrl;
- }
-
- public Long getSubscriberCount() {
- return subscriberCount;
- }
-
- public void setSubscriberCount(final Long subscriberCount) {
- this.subscriberCount = subscriberCount;
- }
-
- public String getDescription() {
- return description;
- }
-
- public void setDescription(final String description) {
- this.description = description;
- }
-
- @NotificationMode
- public int getNotificationMode() {
- return notificationMode;
- }
-
- public void setNotificationMode(@NotificationMode final int notificationMode) {
- this.notificationMode = notificationMode;
- }
-
- @Ignore
- public void setData(final String n, final String au, final String d, final Long sc) {
- this.setName(n);
- this.setAvatarUrl(au);
- this.setDescription(d);
- this.setSubscriberCount(sc);
- }
-
- @Ignore
- public ChannelInfoItem toChannelInfoItem() {
- final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
- item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
- item.setSubscriberCount(getSubscriberCount());
- item.setDescription(getDescription());
- return item;
- }
-
-
- // TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
- @Override
- @SuppressWarnings("EqualsReplaceableByObjectsCall")
- public boolean equals(final Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- final SubscriptionEntity that = (SubscriptionEntity) o;
-
- if (uid != that.uid) {
- return false;
- }
- if (serviceId != that.serviceId) {
- return false;
- }
- if (!url.equals(that.url)) {
- return false;
- }
- if (name != null ? !name.equals(that.name) : that.name != null) {
- return false;
- }
- if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
- return false;
- }
- if (subscriberCount != null
- ? !subscriberCount.equals(that.subscriberCount)
- : that.subscriberCount != null) {
- return false;
- }
- return description != null
- ? description.equals(that.description)
- : that.description == null;
- }
-
- @Override
- public int hashCode() {
- int result = (int) (uid ^ (uid >>> 32));
- result = 31 * result + serviceId;
- result = 31 * result + url.hashCode();
- result = 31 * result + (name != null ? name.hashCode() : 0);
- result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
- result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
- result = 31 * result + (description != null ? description.hashCode() : 0);
- return result;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt
new file mode 100644
index 00000000000..5bfa7c16443
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt
@@ -0,0 +1,182 @@
+package org.schabi.newpipe.database.subscription
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+import org.schabi.newpipe.extractor.channel.ChannelInfo
+import org.schabi.newpipe.extractor.channel.ChannelInfoItem
+import org.schabi.newpipe.util.image.ImageStrategy
+
+@Entity(tableName = SubscriptionEntity.SUBSCRIPTION_TABLE, indices = [Index(value = [SubscriptionEntity.SUBSCRIPTION_SERVICE_ID, SubscriptionEntity.SUBSCRIPTION_URL], unique = true)])
+class SubscriptionEntity() {
+ @PrimaryKey(autoGenerate = true)
+ private var uid: Long = 0
+
+ @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
+ private var serviceId: Int = NO_SERVICE_ID
+
+ @ColumnInfo(name = SUBSCRIPTION_URL)
+ private var url: String? = null
+
+ @ColumnInfo(name = SUBSCRIPTION_NAME)
+ private var name: String? = null
+
+ @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
+ private var avatarUrl: String? = null
+
+ @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
+ private var subscriberCount: Long? = null
+
+ @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
+ private var description: String? = null
+
+ @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
+ private var notificationMode: Int = 0
+ fun getUid(): Long {
+ return uid
+ }
+
+ fun setUid(uid: Long) {
+ this.uid = uid
+ }
+
+ fun getServiceId(): Int {
+ return serviceId
+ }
+
+ fun setServiceId(serviceId: Int) {
+ this.serviceId = serviceId
+ }
+
+ fun getUrl(): String? {
+ return url
+ }
+
+ fun setUrl(url: String?) {
+ this.url = url
+ }
+
+ fun getName(): String? {
+ return name
+ }
+
+ fun setName(name: String?) {
+ this.name = name
+ }
+
+ fun getAvatarUrl(): String? {
+ return avatarUrl
+ }
+
+ fun setAvatarUrl(avatarUrl: String?) {
+ this.avatarUrl = avatarUrl
+ }
+
+ fun getSubscriberCount(): Long? {
+ return subscriberCount
+ }
+
+ fun setSubscriberCount(subscriberCount: Long?) {
+ this.subscriberCount = subscriberCount
+ }
+
+ fun getDescription(): String? {
+ return description
+ }
+
+ fun setDescription(description: String?) {
+ this.description = description
+ }
+
+ @NotificationMode
+ fun getNotificationMode(): Int {
+ return notificationMode
+ }
+
+ fun setNotificationMode(@NotificationMode notificationMode: Int) {
+ this.notificationMode = notificationMode
+ }
+
+ @Ignore
+ fun setData(n: String?, au: String?, d: String?, sc: Long?) {
+ setName(n)
+ setAvatarUrl(au)
+ setDescription(d)
+ setSubscriberCount(sc)
+ }
+
+ @Ignore
+ fun toChannelInfoItem(): ChannelInfoItem {
+ val item: ChannelInfoItem = ChannelInfoItem(getServiceId(), getUrl(), getName())
+ item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()))
+ item.setSubscriberCount((getSubscriberCount())!!)
+ item.setDescription(getDescription())
+ return item
+ }
+
+ // TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
+ public override fun equals(o: Any?): Boolean {
+ if (this === o) {
+ return true
+ }
+ if (o == null || javaClass != o.javaClass) {
+ return false
+ }
+ val that: SubscriptionEntity = o as SubscriptionEntity
+ if (uid != that.uid) {
+ return false
+ }
+ if (serviceId != that.serviceId) {
+ return false
+ }
+ if (!(url == that.url)) {
+ return false
+ }
+ if (if (name != null) !(name == that.name) else that.name != null) {
+ return false
+ }
+ if (if (avatarUrl != null) !(avatarUrl == that.avatarUrl) else that.avatarUrl != null) {
+ return false
+ }
+ if (if (subscriberCount != null) !(subscriberCount == that.subscriberCount) else that.subscriberCount != null) {
+ return false
+ }
+ return if (description != null) (description == that.description) else that.description == null
+ }
+
+ public override fun hashCode(): Int {
+ var result: Int = (uid xor (uid ushr 32)).toInt()
+ result = 31 * result + serviceId
+ result = 31 * result + url.hashCode()
+ result = 31 * result + (if (name != null) name.hashCode() else 0)
+ result = 31 * result + (if (avatarUrl != null) avatarUrl.hashCode() else 0)
+ result = 31 * result + (if (subscriberCount != null) subscriberCount.hashCode() else 0)
+ result = 31 * result + (if (description != null) description.hashCode() else 0)
+ return result
+ }
+
+ companion object {
+ val SUBSCRIPTION_UID: String = "uid"
+ val SUBSCRIPTION_TABLE: String = "subscriptions"
+ val SUBSCRIPTION_SERVICE_ID: String = "service_id"
+ val SUBSCRIPTION_URL: String = "url"
+ val SUBSCRIPTION_NAME: String = "name"
+ val SUBSCRIPTION_AVATAR_URL: String = "avatar_url"
+ val SUBSCRIPTION_SUBSCRIBER_COUNT: String = "subscriber_count"
+ val SUBSCRIPTION_DESCRIPTION: String = "description"
+ val SUBSCRIPTION_NOTIFICATION_MODE: String = "notification_mode"
+ @JvmStatic
+ @Ignore
+ fun from(info: ChannelInfo): SubscriptionEntity {
+ val result: SubscriptionEntity = SubscriptionEntity()
+ result.setServiceId(info.getServiceId())
+ result.setUrl(info.getUrl())
+ result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
+ info.getDescription(), info.getSubscriberCount())
+ return result
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
deleted file mode 100644
index 37eefed96c6..00000000000
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package org.schabi.newpipe.download;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.ViewTreeObserver;
-
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.FragmentTransaction;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
-import org.schabi.newpipe.util.DeviceUtils;
-import org.schabi.newpipe.util.ThemeHelper;
-import org.schabi.newpipe.views.FocusOverlayView;
-
-import us.shandian.giga.service.DownloadManagerService;
-import us.shandian.giga.ui.fragment.MissionsFragment;
-
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
-public class DownloadActivity extends AppCompatActivity {
-
- private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- // Service
- final Intent i = new Intent();
- i.setClass(this, DownloadManagerService.class);
- startService(i);
-
- assureCorrectAppLanguage(this);
- ThemeHelper.setTheme(this);
-
- super.onCreate(savedInstanceState);
-
- final ActivityDownloaderBinding downloaderBinding =
- ActivityDownloaderBinding.inflate(getLayoutInflater());
- setContentView(downloaderBinding.getRoot());
-
- setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
-
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- actionBar.setTitle(R.string.downloads_title);
- actionBar.setDisplayShowTitleEnabled(true);
- }
-
- getWindow().getDecorView().getViewTreeObserver()
- .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- updateFragments();
- getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
- }
- });
-
- if (DeviceUtils.isTv(this)) {
- FocusOverlayView.setupFocusObserver(this);
- }
- }
-
- private void updateFragments() {
- final MissionsFragment fragment = new MissionsFragment();
-
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
- .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
- .commit();
- }
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- super.onCreateOptionsMenu(menu);
- final MenuInflater inflater = getMenuInflater();
-
- inflater.inflate(R.menu.download_menu, menu);
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- onBackPressed();
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.kt
new file mode 100644
index 00000000000..8b8c33be8de
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.kt
@@ -0,0 +1,80 @@
+package org.schabi.newpipe.download
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentTransaction
+import org.schabi.newpipe.R
+import org.schabi.newpipe.databinding.ActivityDownloaderBinding
+import org.schabi.newpipe.util.DeviceUtils
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.views.FocusOverlayView
+import us.shandian.giga.service.DownloadManagerService
+import us.shandian.giga.ui.fragment.MissionsFragment
+
+class DownloadActivity() : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // Service
+ val i: Intent = Intent()
+ i.setClass(this, DownloadManagerService::class.java)
+ startService(i)
+ Localization.assureCorrectAppLanguage(this)
+ ThemeHelper.setTheme(this)
+ super.onCreate(savedInstanceState)
+ val downloaderBinding: ActivityDownloaderBinding = ActivityDownloaderBinding.inflate(getLayoutInflater())
+ setContentView(downloaderBinding.getRoot())
+ setSupportActionBar(downloaderBinding.toolbarLayout.toolbar)
+ val actionBar: ActionBar? = getSupportActionBar()
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true)
+ actionBar.setTitle(R.string.downloads_title)
+ actionBar.setDisplayShowTitleEnabled(true)
+ }
+ getWindow().getDecorView().getViewTreeObserver()
+ .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+ public override fun onGlobalLayout() {
+ updateFragments()
+ getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this)
+ }
+ })
+ if (DeviceUtils.isTv(this)) {
+ FocusOverlayView.Companion.setupFocusObserver(this)
+ }
+ }
+
+ private fun updateFragments() {
+ val fragment: MissionsFragment = MissionsFragment()
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ .commit()
+ }
+
+ public override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ val inflater: MenuInflater = getMenuInflater()
+ inflater.inflate(R.menu.download_menu, menu)
+ return true
+ }
+
+ public override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.getItemId()) {
+ android.R.id.home -> {
+ onBackPressed()
+ return true
+ }
+
+ else -> return super.onOptionsItemSelected(item)
+ }
+ }
+
+ companion object {
+ private val MISSIONS_FRAGMENT_TAG: String = "fragment_tag"
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
deleted file mode 100644
index bbdb462922f..00000000000
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ /dev/null
@@ -1,1149 +0,0 @@
-package org.schabi.newpipe.download;
-
-import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
-import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
-import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.IBinder;
-import android.provider.Settings;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.RadioGroup;
-import android.widget.SeekBar;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResult;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.view.menu.ActionMenuItemView;
-import androidx.appcompat.widget.Toolbar;
-import androidx.collection.SparseArrayCompat;
-import androidx.documentfile.provider.DocumentFile;
-import androidx.fragment.app.DialogFragment;
-import androidx.preference.PreferenceManager;
-
-import com.nononsenseapps.filepicker.Utils;
-
-import org.schabi.newpipe.MainActivity;
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.databinding.DownloadDialogBinding;
-import org.schabi.newpipe.error.ErrorInfo;
-import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.extractor.MediaFormat;
-import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.localization.Localization;
-import org.schabi.newpipe.extractor.stream.AudioStream;
-import org.schabi.newpipe.extractor.stream.Stream;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.SubtitlesStream;
-import org.schabi.newpipe.extractor.stream.VideoStream;
-import org.schabi.newpipe.settings.NewPipeSettings;
-import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
-import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
-import org.schabi.newpipe.streams.io.StoredFileHelper;
-import org.schabi.newpipe.util.FilePickerActivityHelper;
-import org.schabi.newpipe.util.FilenameUtils;
-import org.schabi.newpipe.util.ListHelper;
-import org.schabi.newpipe.util.PermissionHelper;
-import org.schabi.newpipe.util.SecondaryStreamHelper;
-import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
-import org.schabi.newpipe.util.StreamItemAdapter;
-import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
-import org.schabi.newpipe.util.AudioTrackAdapter;
-import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
-import org.schabi.newpipe.util.ThemeHelper;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-
-import icepick.Icepick;
-import icepick.State;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import us.shandian.giga.get.MissionRecoveryInfo;
-import us.shandian.giga.postprocessing.Postprocessing;
-import us.shandian.giga.service.DownloadManager;
-import us.shandian.giga.service.DownloadManagerService;
-import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
-import us.shandian.giga.service.MissionState;
-
-public class DownloadDialog extends DialogFragment
- implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
- private static final String TAG = "DialogFragment";
- private static final boolean DEBUG = MainActivity.DEBUG;
-
- @State
- StreamInfo currentInfo;
- @State
- StreamInfoWrapper wrappedVideoStreams;
- @State
- StreamInfoWrapper wrappedSubtitleStreams;
- @State
- AudioTracksWrapper wrappedAudioTracks;
- @State
- int selectedAudioTrackIndex;
- @State
- int selectedVideoIndex; // set in the constructor
- @State
- int selectedAudioIndex = 0; // default to the first item
- @State
- int selectedSubtitleIndex = 0; // default to the first item
-
- private StoredDirectoryHelper mainStorageAudio = null;
- private StoredDirectoryHelper mainStorageVideo = null;
- private DownloadManager downloadManager = null;
- private ActionMenuItemView okButton = null;
- private Context context = null;
- private boolean askForSavePath;
-
- private AudioTrackAdapter audioTrackAdapter;
- private StreamItemAdapter audioStreamsAdapter;
- private StreamItemAdapter videoStreamsAdapter;
- private StreamItemAdapter subtitleStreamsAdapter;
-
- private final CompositeDisposable disposables = new CompositeDisposable();
-
- private DownloadDialogBinding dialogBinding;
-
- private SharedPreferences prefs;
-
- // Variables for file name and MIME type when picking new folder because it's not set yet
- private String filenameTmp;
- private String mimeTmp;
-
- private final ActivityResultLauncher requestDownloadSaveAsLauncher =
- registerForActivityResult(
- new StartActivityForResult(), this::requestDownloadSaveAsResult);
- private final ActivityResultLauncher requestDownloadPickAudioFolderLauncher =
- registerForActivityResult(
- new StartActivityForResult(), this::requestDownloadPickAudioFolderResult);
- private final ActivityResultLauncher requestDownloadPickVideoFolderLauncher =
- registerForActivityResult(
- new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
-
- /*//////////////////////////////////////////////////////////////////////////
- // Instance creation
- //////////////////////////////////////////////////////////////////////////*/
-
- public DownloadDialog() {
- // Just an empty default no-arg ctor to keep Fragment.instantiate() happy
- // otherwise InstantiationException will be thrown when fragment is recreated
- // TODO: Maybe use a custom FragmentFactory instead?
- }
-
- /**
- * Create a new download dialog with the video, audio and subtitle streams from the provided
- * stream info. Video streams and video-only streams will be put into a single list menu,
- * sorted according to their resolution and the default video resolution will be selected.
- *
- * @param context the context to use just to obtain preferences and strings (will not be stored)
- * @param info the info from which to obtain downloadable streams and other info (e.g. title)
- */
- public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
- this.currentInfo = info;
-
- final List audioStreams =
- getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP);
- final List> groupedAudioStreams =
- ListHelper.getGroupedAudioStreams(context, audioStreams);
- this.wrappedAudioTracks = new AudioTracksWrapper(groupedAudioStreams, context);
- this.selectedAudioTrackIndex =
- ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams);
-
- // TODO: Adapt this code when the downloader support other types of stream deliveries
- final List videoStreams = ListHelper.getSortedStreamVideosList(
- context,
- getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
- getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
- false,
- // If there are multiple languages available, prefer streams without audio
- // to allow language selection
- wrappedAudioTracks.size() > 1
- );
-
- this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context);
- this.wrappedSubtitleStreams = new StreamInfoWrapper<>(
- getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
-
- this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Android lifecycle
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (DEBUG) {
- Log.d(TAG, "onCreate() called with: "
- + "savedInstanceState = [" + savedInstanceState + "]");
- }
-
- if (!PermissionHelper.checkStoragePermissions(getActivity(),
- PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
- dismiss();
- return;
- }
-
- // context will remain null if dismiss() was called above, allowing to check whether the
- // dialog is being dismissed in onViewCreated()
- context = getContext();
-
- setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
- Icepick.restoreInstanceState(this, savedInstanceState);
-
- this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
- this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
- updateSecondaryStreams();
-
- final Intent intent = new Intent(context, DownloadManagerService.class);
- context.startService(intent);
-
- context.bindService(intent, new ServiceConnection() {
- @Override
- public void onServiceConnected(final ComponentName cname, final IBinder service) {
- final DownloadManagerBinder mgr = (DownloadManagerBinder) service;
-
- mainStorageAudio = mgr.getMainStorageAudio();
- mainStorageVideo = mgr.getMainStorageVideo();
- downloadManager = mgr.getDownloadManager();
- askForSavePath = mgr.askForSavePath();
-
- okButton.setEnabled(true);
-
- context.unbindService(this);
- }
-
- @Override
- public void onServiceDisconnected(final ComponentName name) {
- // nothing to do
- }
- }, Context.BIND_AUTO_CREATE);
- }
-
- /**
- * Update the displayed video streams based on the selected audio track.
- */
- private void updateSecondaryStreams() {
- final StreamInfoWrapper audioStreams = getWrappedAudioStreams();
- final var secondaryStreams = new SparseArrayCompat>(4);
- final List videoStreams = wrappedVideoStreams.getStreamsList();
- wrappedVideoStreams.resetInfo();
-
- for (int i = 0; i < videoStreams.size(); i++) {
- if (!videoStreams.get(i).isVideoOnly()) {
- continue;
- }
- final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
- context, audioStreams.getStreamsList(), videoStreams.get(i));
-
- if (audioStream != null) {
- secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
- } else if (DEBUG) {
- final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
- if (mediaFormat != null) {
- Log.w(TAG, "No audio stream candidates for video format "
- + mediaFormat.name());
- } else {
- Log.w(TAG, "No audio stream candidates for unknown video format");
- }
- }
- }
-
- this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
- this.audioStreamsAdapter = new StreamItemAdapter<>(audioStreams);
- }
-
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState) {
- if (DEBUG) {
- Log.d(TAG, "onCreateView() called with: "
- + "inflater = [" + inflater + "], container = [" + container + "], "
- + "savedInstanceState = [" + savedInstanceState + "]");
- }
- return inflater.inflate(R.layout.download_dialog, container);
- }
-
- @Override
- public void onViewCreated(@NonNull final View view,
- @Nullable final Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- dialogBinding = DownloadDialogBinding.bind(view);
- if (context == null) {
- return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
- }
-
- dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
- currentInfo.getName()));
- selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
- getWrappedAudioStreams().getStreamsList());
-
- selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
-
- dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
- dialogBinding.audioStreamSpinner.setOnItemSelectedListener(this);
- dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
- dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
-
- initToolbar(dialogBinding.toolbarLayout.toolbar);
- setupDownloadOptions();
-
- prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
-
- final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
- dialogBinding.threadsCount.setText(String.valueOf(threads));
- dialogBinding.threads.setProgress(threads - 1);
- dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(@NonNull final SeekBar seekbar,
- final int progress,
- final boolean fromUser) {
- final int newProgress = progress + 1;
- prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
- .apply();
- dialogBinding.threadsCount.setText(String.valueOf(newProgress));
- }
- });
-
- fetchStreamsSize();
- }
-
- private void initToolbar(final Toolbar toolbar) {
- if (DEBUG) {
- Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
- }
-
- toolbar.setTitle(R.string.download_dialog_title);
- toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
- toolbar.inflateMenu(R.menu.dialog_url);
- toolbar.setNavigationOnClickListener(v -> dismiss());
- toolbar.setNavigationContentDescription(R.string.cancel);
-
- okButton = toolbar.findViewById(R.id.okay);
- okButton.setEnabled(false); // disable until the download service connection is done
-
- toolbar.setOnMenuItemClickListener(item -> {
- if (item.getItemId() == R.id.okay) {
- prepareSelectedDownload();
- return true;
- }
- return false;
- });
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- disposables.clear();
- }
-
- @Override
- public void onDestroyView() {
- dialogBinding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onSaveInstanceState(@NonNull final Bundle outState) {
- super.onSaveInstanceState(outState);
- Icepick.saveInstanceState(this, outState);
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Video, audio and subtitle spinners
- //////////////////////////////////////////////////////////////////////////*/
-
- private void fetchStreamsSize() {
- disposables.clear();
- disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams)
- .subscribe(result -> {
- if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
- == R.id.video_button) {
- setupVideoSpinner();
- }
- }, throwable -> ErrorUtil.showSnackbar(context,
- new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
- "Downloading video stream size",
- currentInfo.getServiceId()))));
- disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
- .subscribe(result -> {
- if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
- == R.id.audio_button) {
- setupAudioSpinner();
- }
- }, throwable -> ErrorUtil.showSnackbar(context,
- new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
- "Downloading audio stream size",
- currentInfo.getServiceId()))));
- disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
- .subscribe(result -> {
- if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
- == R.id.subtitle_button) {
- setupSubtitleSpinner();
- }
- }, throwable -> ErrorUtil.showSnackbar(context,
- new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
- "Downloading subtitle stream size",
- currentInfo.getServiceId()))));
- }
-
- private void setupAudioTrackSpinner() {
- if (getContext() == null) {
- return;
- }
-
- dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
- dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex);
- }
-
- private void setupAudioSpinner() {
- if (getContext() == null) {
- return;
- }
-
- dialogBinding.qualitySpinner.setVisibility(View.GONE);
- setRadioButtonsState(true);
- dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter);
- dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex);
- dialogBinding.audioStreamSpinner.setVisibility(View.VISIBLE);
- dialogBinding.audioTrackSpinner.setVisibility(
- wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
- dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
- }
-
- private void setupVideoSpinner() {
- if (getContext() == null) {
- return;
- }
-
- dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
- dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
- dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
- setRadioButtonsState(true);
- dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
- onVideoStreamSelected();
- }
-
- private void onVideoStreamSelected() {
- final boolean isVideoOnly = videoStreamsAdapter.getItem(selectedVideoIndex).isVideoOnly();
-
- dialogBinding.audioTrackSpinner.setVisibility(
- isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
- dialogBinding.audioTrackPresentInVideoText.setVisibility(
- !isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
- }
-
- private void setupSubtitleSpinner() {
- if (getContext() == null) {
- return;
- }
-
- dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
- dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
- dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
- setRadioButtonsState(true);
- dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
- dialogBinding.audioTrackSpinner.setVisibility(View.GONE);
- dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Activity results
- //////////////////////////////////////////////////////////////////////////*/
-
- private void requestDownloadPickAudioFolderResult(final ActivityResult result) {
- requestDownloadPickFolderResult(
- result, getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
- }
-
- private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
- requestDownloadPickFolderResult(
- result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
- }
-
- private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
- if (result.getResultCode() != Activity.RESULT_OK) {
- return;
- }
-
- if (result.getData() == null || result.getData().getData() == null) {
- showFailedDialog(R.string.general_error);
- return;
- }
-
- if (FilePickerActivityHelper.isOwnFileUri(context, result.getData().getData())) {
- final File file = Utils.getFileForUri(result.getData().getData());
- checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
- StoredFileHelper.DEFAULT_MIME);
- return;
- }
-
- final DocumentFile docFile = DocumentFile.fromSingleUri(context,
- result.getData().getData());
- if (docFile == null) {
- showFailedDialog(R.string.general_error);
- return;
- }
-
- // check if the selected file was previously used
- checkSelectedDownload(null, result.getData().getData(), docFile.getName(),
- docFile.getType());
- }
-
- private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
- final String key,
- final String tag) {
- if (result.getResultCode() != Activity.RESULT_OK) {
- return;
- }
-
- if (result.getData() == null || result.getData().getData() == null) {
- showFailedDialog(R.string.general_error);
- return;
- }
-
- Uri uri = result.getData().getData();
- if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
- uri = Uri.fromFile(Utils.getFileForUri(uri));
- } else {
- context.grantUriPermission(context.getPackageName(), uri,
- StoredDirectoryHelper.PERMISSION_FLAGS);
- }
-
- PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
- uri.toString()).apply();
-
- try {
- final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
- checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
- filenameTmp, mimeTmp);
- } catch (final IOException e) {
- showFailedDialog(R.string.general_error);
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Listeners
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
- if (DEBUG) {
- Log.d(TAG, "onCheckedChanged() called with: "
- + "group = [" + group + "], checkedId = [" + checkedId + "]");
- }
- boolean flag = true;
-
- switch (checkedId) {
- case R.id.audio_button:
- setupAudioSpinner();
- break;
- case R.id.video_button:
- setupVideoSpinner();
- break;
- case R.id.subtitle_button:
- setupSubtitleSpinner();
- flag = false;
- break;
- }
-
- dialogBinding.threads.setEnabled(flag);
- }
-
- @Override
- public void onItemSelected(final AdapterView> parent,
- final View view,
- final int position,
- final long id) {
- if (DEBUG) {
- Log.d(TAG, "onItemSelected() called with: "
- + "parent = [" + parent + "], view = [" + view + "], "
- + "position = [" + position + "], id = [" + id + "]");
- }
-
- switch (parent.getId()) {
- case R.id.quality_spinner:
- switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
- case R.id.video_button:
- selectedVideoIndex = position;
- onVideoStreamSelected();
- break;
- case R.id.subtitle_button:
- selectedSubtitleIndex = position;
- break;
- }
- onItemSelectedSetFileName();
- break;
- case R.id.audio_track_spinner:
- final boolean trackChanged = selectedAudioTrackIndex != position;
- selectedAudioTrackIndex = position;
- if (trackChanged) {
- updateSecondaryStreams();
- fetchStreamsSize();
- }
- break;
- case R.id.audio_stream_spinner:
- selectedAudioIndex = position;
- }
- }
-
- private void onItemSelectedSetFileName() {
- final String fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
- final String prevFileName = Optional.ofNullable(dialogBinding.fileName.getText())
- .map(Object::toString)
- .orElse("");
-
- if (prevFileName.isEmpty()
- || prevFileName.equals(fileName)
- || prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
- // only update the file name field if it was not edited by the user
-
- switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
- case R.id.audio_button:
- case R.id.video_button:
- if (!prevFileName.equals(fileName)) {
- // since the user might have switched between audio and video, the correct
- // text might already be in place, so avoid resetting the cursor position
- dialogBinding.fileName.setText(fileName);
- }
- break;
-
- case R.id.subtitle_button:
- final String setSubtitleLanguageCode = subtitleStreamsAdapter
- .getItem(selectedSubtitleIndex).getLanguageTag();
- // this will reset the cursor position, which is bad UX, but it can't be avoided
- dialogBinding.fileName.setText(getString(
- R.string.caption_file_name, fileName, setSubtitleLanguageCode));
- break;
- }
- }
- }
-
- @Override
- public void onNothingSelected(final AdapterView> parent) {
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Download
- //////////////////////////////////////////////////////////////////////////*/
-
- protected void setupDownloadOptions() {
- setRadioButtonsState(false);
- setupAudioTrackSpinner();
-
- final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
- final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
- final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
-
- dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
- : View.GONE);
- dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
- : View.GONE);
- dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
- ? View.VISIBLE : View.GONE);
-
- prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
- final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
- getString(R.string.last_download_type_video_key));
-
- if (isVideoStreamsAvailable
- && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
- dialogBinding.videoButton.setChecked(true);
- setupVideoSpinner();
- } else if (isAudioStreamsAvailable
- && (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
- dialogBinding.audioButton.setChecked(true);
- setupAudioSpinner();
- } else if (isSubtitleStreamsAvailable
- && (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
- dialogBinding.subtitleButton.setChecked(true);
- setupSubtitleSpinner();
- } else if (isVideoStreamsAvailable) {
- dialogBinding.videoButton.setChecked(true);
- setupVideoSpinner();
- } else if (isAudioStreamsAvailable) {
- dialogBinding.audioButton.setChecked(true);
- setupAudioSpinner();
- } else if (isSubtitleStreamsAvailable) {
- dialogBinding.subtitleButton.setChecked(true);
- setupSubtitleSpinner();
- } else {
- Toast.makeText(getContext(), R.string.no_streams_available_download,
- Toast.LENGTH_SHORT).show();
- dismiss();
- }
- }
-
- private void setRadioButtonsState(final boolean enabled) {
- dialogBinding.audioButton.setEnabled(enabled);
- dialogBinding.videoButton.setEnabled(enabled);
- dialogBinding.subtitleButton.setEnabled(enabled);
- }
-
- private StreamInfoWrapper getWrappedAudioStreams() {
- if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
- return StreamInfoWrapper.empty();
- }
- return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
- }
-
- private int getSubtitleIndexBy(@NonNull final List streams) {
- final Localization preferredLocalization = NewPipe.getPreferredLocalization();
-
- int candidate = 0;
- for (int i = 0; i < streams.size(); i++) {
- final Locale streamLocale = streams.get(i).getLocale();
-
- final boolean languageEquals = streamLocale.getLanguage() != null
- && preferredLocalization.getLanguageCode() != null
- && streamLocale.getLanguage()
- .equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
- final boolean countryEquals = streamLocale.getCountry() != null
- && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
-
- if (languageEquals) {
- if (countryEquals) {
- return i;
- }
-
- candidate = i;
- }
- }
-
- return candidate;
- }
-
- @NonNull
- private String getNameEditText() {
- final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
- .trim();
-
- return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
- }
-
- private void showFailedDialog(@StringRes final int msg) {
- assureCorrectAppLanguage(requireContext());
- new AlertDialog.Builder(context)
- .setTitle(R.string.general_error)
- .setMessage(msg)
- .setNegativeButton(getString(R.string.ok), null)
- .show();
- }
-
- private void launchDirectoryPicker(final ActivityResultLauncher launcher) {
- NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
- context);
- }
-
- private void prepareSelectedDownload() {
- final StoredDirectoryHelper mainStorage;
- final MediaFormat format;
- final String selectedMediaType;
- final long size;
-
- // first, build the filename and get the output folder (if possible)
- // later, run a very very very large file checking logic
-
- filenameTmp = getNameEditText().concat(".");
-
- switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
- case R.id.audio_button:
- selectedMediaType = getString(R.string.last_download_type_audio_key);
- mainStorage = mainStorageAudio;
- format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
- size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
- if (format == MediaFormat.WEBMA_OPUS) {
- mimeTmp = "audio/ogg";
- filenameTmp += "opus";
- } else if (format != null) {
- mimeTmp = format.mimeType;
- filenameTmp += format.getSuffix();
- }
- break;
- case R.id.video_button:
- selectedMediaType = getString(R.string.last_download_type_video_key);
- mainStorage = mainStorageVideo;
- format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
- size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
- if (format != null) {
- mimeTmp = format.mimeType;
- filenameTmp += format.getSuffix();
- }
- break;
- case R.id.subtitle_button:
- selectedMediaType = getString(R.string.last_download_type_subtitle_key);
- mainStorage = mainStorageVideo; // subtitle & video files go together
- format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
- size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
- if (format != null) {
- mimeTmp = format.mimeType;
- }
-
- if (format == MediaFormat.TTML) {
- filenameTmp += MediaFormat.SRT.getSuffix();
- } else if (format != null) {
- filenameTmp += format.getSuffix();
- }
- break;
- default:
- throw new RuntimeException("No stream selected");
- }
-
- if (!askForSavePath && (mainStorage == null
- || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
- || mainStorage.isInvalidSafStorage())) {
- // Pick new download folder if one of:
- // - Download folder is not set
- // - Download folder uses SAF while SAF is disabled
- // - Download folder doesn't use SAF while SAF is enabled
- // - Download folder uses SAF but the user manually revoked access to it
- Toast.makeText(context, getString(R.string.no_dir_yet),
- Toast.LENGTH_LONG).show();
-
- if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
- launchDirectoryPicker(requestDownloadPickAudioFolderLauncher);
- } else {
- launchDirectoryPicker(requestDownloadPickVideoFolderLauncher);
- }
-
- return;
- }
-
- if (askForSavePath) {
- final Uri initialPath;
- if (NewPipeSettings.useStorageAccessFramework(context)) {
- initialPath = null;
- } else {
- final File initialSavePath;
- if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
- initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
- } else {
- initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
- }
- initialPath = Uri.parse(initialSavePath.getAbsolutePath());
- }
-
- NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
- StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
- context);
-
- return;
- }
-
- // Check for free memory space (for api 24 and up)
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
- final long freeSpace = mainStorage.getFreeMemory();
- if (freeSpace <= size) {
- Toast.makeText(context, getString(R.
- string.error_insufficient_storage), Toast.LENGTH_LONG).show();
- // move the user to storage setting tab
- final Intent storageSettingsIntent = new Intent(Settings.
- ACTION_INTERNAL_STORAGE_SETTINGS);
- if (storageSettingsIntent.resolveActivity(context.getPackageManager()) != null) {
- startActivity(storageSettingsIntent);
- }
- return;
- }
- }
-
- // check for existing file with the same name
- checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
- mimeTmp);
-
- // remember the last media type downloaded by the user
- prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
- .apply();
- }
-
- private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
- final Uri targetFile,
- final String filename,
- final String mime) {
- StoredFileHelper storage;
-
- try {
- if (mainStorage == null) {
- // using SAF on older android version
- storage = new StoredFileHelper(context, null, targetFile, "");
- } else if (targetFile == null) {
- // the file does not exist, but it is probably used in a pending download
- storage = new StoredFileHelper(mainStorage.getUri(), filename, mime,
- mainStorage.getTag());
- } else {
- // the target filename is already use, attempt to use it
- storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
- mainStorage.getTag());
- }
- } catch (final Exception e) {
- ErrorUtil.createNotification(requireContext(),
- new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
- return;
- }
-
- // get state of potential mission referring to the same file
- final MissionState state = downloadManager.checkForExistingMission(storage);
- @StringRes final int msgBtn;
- @StringRes final int msgBody;
-
- // this switch checks if there is already a mission referring to the same file
- switch (state) {
- case Finished: // there is already a finished mission
- msgBtn = R.string.overwrite;
- msgBody = R.string.overwrite_finished_warning;
- break;
- case Pending:
- msgBtn = R.string.overwrite;
- msgBody = R.string.download_already_pending;
- break;
- case PendingRunning:
- msgBtn = R.string.generate_unique_name;
- msgBody = R.string.download_already_running;
- break;
- case None: // there is no mission referring to the same file
- if (mainStorage == null) {
- // This part is called if:
- // * using SAF on older android version
- // * save path not defined
- // * if the file exists overwrite it, is not necessary ask
- if (!storage.existsAsFile() && !storage.create()) {
- showFailedDialog(R.string.error_file_creation);
- return;
- }
- continueSelectedDownload(storage);
- return;
- } else if (targetFile == null) {
- // This part is called if:
- // * the filename is not used in a pending/finished download
- // * the file does not exists, create
-
- if (!mainStorage.mkdirs()) {
- showFailedDialog(R.string.error_path_creation);
- return;
- }
-
- storage = mainStorage.createFile(filename, mime);
- if (storage == null || !storage.canWrite()) {
- showFailedDialog(R.string.error_file_creation);
- return;
- }
-
- continueSelectedDownload(storage);
- return;
- }
- msgBtn = R.string.overwrite;
- msgBody = R.string.overwrite_unrelated_warning;
- break;
- default:
- return; // unreachable
- }
-
- final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
- .setTitle(R.string.download_dialog_title)
- .setMessage(msgBody)
- .setNegativeButton(R.string.cancel, null);
- final StoredFileHelper finalStorage = storage;
-
-
- if (mainStorage == null) {
- // This part is called if:
- // * using SAF on older android version
- // * save path not defined
- switch (state) {
- case Pending:
- case Finished:
- askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
- dialog.dismiss();
- downloadManager.forgetMission(finalStorage);
- continueSelectedDownload(finalStorage);
- });
- break;
- }
-
- askDialog.show();
- return;
- }
-
- askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
- dialog.dismiss();
-
- StoredFileHelper storageNew;
- switch (state) {
- case Finished:
- case Pending:
- downloadManager.forgetMission(finalStorage);
- case None:
- if (targetFile == null) {
- storageNew = mainStorage.createFile(filename, mime);
- } else {
- try {
- // try take (or steal) the file
- storageNew = new StoredFileHelper(context, mainStorage.getUri(),
- targetFile, mainStorage.getTag());
- } catch (final IOException e) {
- Log.e(TAG, "Failed to take (or steal) the file in "
- + targetFile.toString());
- storageNew = null;
- }
- }
-
- if (storageNew != null && storageNew.canWrite()) {
- continueSelectedDownload(storageNew);
- } else {
- showFailedDialog(R.string.error_file_creation);
- }
- break;
- case PendingRunning:
- storageNew = mainStorage.createUniqueFile(filename, mime);
- if (storageNew == null) {
- showFailedDialog(R.string.error_file_creation);
- } else {
- continueSelectedDownload(storageNew);
- }
- break;
- }
- });
-
- askDialog.show();
- }
-
- private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
- if (!storage.canWrite()) {
- showFailedDialog(R.string.permission_denied);
- return;
- }
-
- // check if the selected file has to be overwritten, by simply checking its length
- try {
- if (storage.length() > 0) {
- storage.truncate();
- }
- } catch (final IOException e) {
- Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
- showFailedDialog(R.string.overwrite_failed);
- return;
- }
-
- final Stream selectedStream;
- Stream secondaryStream = null;
- final char kind;
- int threads = dialogBinding.threads.getProgress() + 1;
- final String[] urls;
- final List recoveryInfo;
- String psName = null;
- String[] psArgs = null;
- long nearLength = 0;
-
- // more download logic: select muxer, subtitle converter, etc.
- switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
- case R.id.audio_button:
- kind = 'a';
- selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
-
- if (selectedStream.getFormat() == MediaFormat.M4A) {
- psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
- } else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
- psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
- }
- break;
- case R.id.video_button:
- kind = 'v';
- selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
-
- final SecondaryStreamHelper secondary = videoStreamsAdapter
- .getAllSecondary()
- .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
-
- if (secondary != null) {
- secondaryStream = secondary.getStream();
-
- if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
- psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
- } else {
- psName = Postprocessing.ALGORITHM_WEBM_MUXER;
- }
-
- final long videoSize = wrappedVideoStreams.getSizeInBytes(
- (VideoStream) selectedStream);
-
- // set nearLength, only, if both sizes are fetched or known. This probably
- // does not work on slow networks but is later updated in the downloader
- if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
- nearLength = secondary.getSizeInBytes() + videoSize;
- }
- }
- break;
- case R.id.subtitle_button:
- threads = 1; // use unique thread for subtitles due small file size
- kind = 's';
- selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
-
- if (selectedStream.getFormat() == MediaFormat.TTML) {
- psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
- psArgs = new String[] {
- selectedStream.getFormat().getSuffix(),
- "false" // ignore empty frames
- };
- }
- break;
- default:
- return;
- }
-
- if (secondaryStream == null) {
- urls = new String[] {
- selectedStream.getContent()
- };
- recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
- } else {
- if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
- throw new IllegalArgumentException("Unsupported stream delivery format"
- + secondaryStream.getDeliveryMethod());
- }
-
- urls = new String[] {
- selectedStream.getContent(), secondaryStream.getContent()
- };
- recoveryInfo = List.of(
- new MissionRecoveryInfo(selectedStream),
- new MissionRecoveryInfo(secondaryStream)
- );
- }
-
- DownloadManagerService.startMission(context, urls, storage, kind, threads,
- currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
-
- Toast.makeText(context, getString(R.string.download_has_started),
- Toast.LENGTH_SHORT).show();
-
- dismiss();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.kt b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.kt
new file mode 100644
index 00000000000..77beb59ebad
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.kt
@@ -0,0 +1,1039 @@
+package org.schabi.newpipe.download
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.os.IBinder
+import android.provider.Settings
+import android.text.Editable
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.RadioGroup
+import android.widget.SeekBar
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.IdRes
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.view.menu.ActionMenuItemView
+import androidx.appcompat.widget.Toolbar
+import androidx.collection.SparseArrayCompat
+import androidx.documentfile.provider.DocumentFile
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+import com.nononsenseapps.filepicker.Utils
+import icepick.Icepick
+import icepick.State
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.functions.Consumer
+import org.schabi.newpipe.MainActivity
+import org.schabi.newpipe.R
+import org.schabi.newpipe.databinding.DownloadDialogBinding
+import org.schabi.newpipe.error.ErrorInfo
+import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
+import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
+import org.schabi.newpipe.error.UserAction
+import org.schabi.newpipe.extractor.MediaFormat
+import org.schabi.newpipe.extractor.NewPipe
+import org.schabi.newpipe.extractor.localization.Localization
+import org.schabi.newpipe.extractor.stream.AudioStream
+import org.schabi.newpipe.extractor.stream.DeliveryMethod
+import org.schabi.newpipe.extractor.stream.Stream
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.extractor.stream.SubtitlesStream
+import org.schabi.newpipe.extractor.stream.VideoStream
+import org.schabi.newpipe.settings.NewPipeSettings
+import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
+import org.schabi.newpipe.streams.io.StoredDirectoryHelper
+import org.schabi.newpipe.streams.io.StoredFileHelper
+import org.schabi.newpipe.util.AudioTrackAdapter
+import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper
+import org.schabi.newpipe.util.FilePickerActivityHelper
+import org.schabi.newpipe.util.FilenameUtils
+import org.schabi.newpipe.util.ListHelper
+import org.schabi.newpipe.util.PermissionHelper
+import org.schabi.newpipe.util.SecondaryStreamHelper
+import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener
+import org.schabi.newpipe.util.StreamItemAdapter
+import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
+import org.schabi.newpipe.util.ThemeHelper
+import us.shandian.giga.get.MissionRecoveryInfo
+import us.shandian.giga.postprocessing.Postprocessing
+import us.shandian.giga.service.DownloadManager
+import us.shandian.giga.service.DownloadManagerService
+import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder
+import us.shandian.giga.service.MissionState
+import java.io.File
+import java.io.IOException
+import java.util.Locale
+import java.util.Objects
+import java.util.Optional
+import java.util.function.Function
+
+class DownloadDialog : DialogFragment, RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
+ @State
+ var currentInfo: StreamInfo? = null
+
+ @State
+ var wrappedVideoStreams: StreamInfoWrapper? = null
+
+ @State
+ var wrappedSubtitleStreams: StreamInfoWrapper? = null
+
+ @State
+ var wrappedAudioTracks: AudioTracksWrapper? = null
+
+ @State
+ var selectedAudioTrackIndex: Int = 0
+
+ @State
+ var selectedVideoIndex: Int = 0 // set in the constructor
+
+ @State
+ var selectedAudioIndex: Int = 0 // default to the first item
+
+ @State
+ var selectedSubtitleIndex: Int = 0 // default to the first item
+ private var mainStorageAudio: StoredDirectoryHelper? = null
+ private var mainStorageVideo: StoredDirectoryHelper? = null
+ private var downloadManager: DownloadManager? = null
+ private var okButton: ActionMenuItemView? = null
+ private var context: Context? = null
+ private var askForSavePath: Boolean = false
+ private var audioTrackAdapter: AudioTrackAdapter? = null
+ private var audioStreamsAdapter: StreamItemAdapter? = null
+ private var videoStreamsAdapter: StreamItemAdapter? = null
+ private var subtitleStreamsAdapter: StreamItemAdapter? = null
+ private val disposables: CompositeDisposable = CompositeDisposable()
+ private var dialogBinding: DownloadDialogBinding? = null
+ private var prefs: SharedPreferences? = null
+
+ // Variables for file name and MIME type when picking new folder because it's not set yet
+ private var filenameTmp: String? = null
+ private var mimeTmp: String? = null
+ private val requestDownloadSaveAsLauncher: ActivityResultLauncher = registerForActivityResult(
+ StartActivityForResult(), ActivityResultCallback({ result: ActivityResult -> requestDownloadSaveAsResult(result) }))
+ private val requestDownloadPickAudioFolderLauncher: ActivityResultLauncher = registerForActivityResult(
+ StartActivityForResult(), ActivityResultCallback({ result: ActivityResult -> requestDownloadPickAudioFolderResult(result) }))
+ private val requestDownloadPickVideoFolderLauncher: ActivityResultLauncher = registerForActivityResult(
+ StartActivityForResult(), ActivityResultCallback({ result: ActivityResult -> requestDownloadPickVideoFolderResult(result) }))
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Instance creation
+ ////////////////////////////////////////////////////////////////////////// */
+ constructor()
+
+ /**
+ * Create a new download dialog with the video, audio and subtitle streams from the provided
+ * stream info. Video streams and video-only streams will be put into a single list menu,
+ * sorted according to their resolution and the default video resolution will be selected.
+ *
+ * @param context the context to use just to obtain preferences and strings (will not be stored)
+ * @param info the info from which to obtain downloadable streams and other info (e.g. title)
+ */
+ constructor(context: Context, info: StreamInfo) {
+ currentInfo = info
+ val audioStreams: List = ListHelper.getStreamsOfSpecifiedDelivery(info.getAudioStreams(), DeliveryMethod.PROGRESSIVE_HTTP)
+ val groupedAudioStreams: List?>? = ListHelper.getGroupedAudioStreams(context, audioStreams)
+ wrappedAudioTracks = AudioTracksWrapper((groupedAudioStreams)!!, context)
+ selectedAudioTrackIndex = ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams)
+
+ // TODO: Adapt this code when the downloader support other types of stream deliveries
+ val videoStreams: List = ListHelper.getSortedStreamVideosList(
+ context,
+ ListHelper.getStreamsOfSpecifiedDelivery(info.getVideoStreams(), DeliveryMethod.PROGRESSIVE_HTTP),
+ ListHelper.getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), DeliveryMethod.PROGRESSIVE_HTTP),
+ false, // If there are multiple languages available, prefer streams without audio
+ // to allow language selection
+ wrappedAudioTracks!!.size() > 1
+ )
+ wrappedVideoStreams = StreamInfoWrapper(videoStreams, context)
+ wrappedSubtitleStreams = StreamInfoWrapper(
+ ListHelper.getStreamsOfSpecifiedDelivery(info.getSubtitles(), DeliveryMethod.PROGRESSIVE_HTTP), context)
+ selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams)
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Android lifecycle
+ ////////////////////////////////////////////////////////////////////////// */
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (DEBUG) {
+ Log.d(TAG, ("onCreate() called with: "
+ + "savedInstanceState = [" + savedInstanceState + "]"))
+ }
+ if (!PermissionHelper.checkStoragePermissions(getActivity(),
+ PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+ dismiss()
+ return
+ }
+
+ // context will remain null if dismiss() was called above, allowing to check whether the
+ // dialog is being dismissed in onViewCreated()
+ context = getContext()
+ setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context))
+ Icepick.restoreInstanceState(this, savedInstanceState)
+ audioTrackAdapter = AudioTrackAdapter(wrappedAudioTracks)
+ subtitleStreamsAdapter = StreamItemAdapter(wrappedSubtitleStreams)
+ updateSecondaryStreams()
+ val intent: Intent = Intent(context, DownloadManagerService::class.java)
+ context!!.startService(intent)
+ context!!.bindService(intent, object : ServiceConnection {
+ public override fun onServiceConnected(cname: ComponentName, service: IBinder) {
+ val mgr: DownloadManagerBinder = service as DownloadManagerBinder
+ mainStorageAudio = mgr.getMainStorageAudio()
+ mainStorageVideo = mgr.getMainStorageVideo()
+ downloadManager = mgr.getDownloadManager()
+ askForSavePath = mgr.askForSavePath()
+ okButton!!.setEnabled(true)
+ context!!.unbindService(this)
+ }
+
+ public override fun onServiceDisconnected(name: ComponentName) {
+ // nothing to do
+ }
+ }, Context.BIND_AUTO_CREATE)
+ }
+
+ /**
+ * Update the displayed video streams based on the selected audio track.
+ */
+ private fun updateSecondaryStreams() {
+ val audioStreams: StreamInfoWrapper? = getWrappedAudioStreams()
+ val secondaryStreams: SparseArrayCompat?> = SparseArrayCompat(4)
+ val videoStreams: List? = wrappedVideoStreams.getStreamsList()
+ wrappedVideoStreams!!.resetInfo()
+ for (i in videoStreams!!.indices) {
+ if (!videoStreams.get(i)!!.isVideoOnly()) {
+ continue
+ }
+ val audioStream: AudioStream? = SecondaryStreamHelper.Companion.getAudioStreamFor(
+ (context)!!, audioStreams.getStreamsList(), (videoStreams.get(i))!!)
+ if (audioStream != null) {
+ secondaryStreams.append(i, SecondaryStreamHelper(audioStreams, audioStream))
+ } else if (DEBUG) {
+ val mediaFormat: MediaFormat? = videoStreams.get(i)!!.getFormat()
+ if (mediaFormat != null) {
+ Log.w(TAG, ("No audio stream candidates for video format "
+ + mediaFormat.name))
+ } else {
+ Log.w(TAG, "No audio stream candidates for unknown video format")
+ }
+ }
+ }
+ videoStreamsAdapter = StreamItemAdapter(wrappedVideoStreams, secondaryStreams)
+ audioStreamsAdapter = StreamItemAdapter(audioStreams)
+ }
+
+ public override fun onCreateView(inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ if (DEBUG) {
+ Log.d(TAG, ("onCreateView() called with: "
+ + "inflater = [" + inflater + "], container = [" + container + "], "
+ + "savedInstanceState = [" + savedInstanceState + "]"))
+ }
+ return inflater.inflate(R.layout.download_dialog, container)
+ }
+
+ public override fun onViewCreated(view: View,
+ savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ dialogBinding = DownloadDialogBinding.bind(view)
+ if (context == null) {
+ return // the dialog is being dismissed, see the call to dismiss() in onCreate()
+ }
+ dialogBinding!!.fileName.setText(FilenameUtils.createFilename(getContext(),
+ currentInfo!!.getName()))
+ selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
+ getWrappedAudioStreams().getStreamsList())
+ selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll())
+ dialogBinding!!.qualitySpinner.setOnItemSelectedListener(this)
+ dialogBinding!!.audioStreamSpinner.setOnItemSelectedListener(this)
+ dialogBinding!!.audioTrackSpinner.setOnItemSelectedListener(this)
+ dialogBinding!!.videoAudioGroup.setOnCheckedChangeListener(this)
+ initToolbar(dialogBinding!!.toolbarLayout.toolbar)
+ setupDownloadOptions()
+ prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ val threads: Int = prefs.getInt(getString(R.string.default_download_threads), 3)
+ dialogBinding!!.threadsCount.setText(threads.toString())
+ dialogBinding!!.threads.setProgress(threads - 1)
+ dialogBinding!!.threads.setOnSeekBarChangeListener(object : SimpleOnSeekBarChangeListener() {
+ public override fun onProgressChanged(seekbar: SeekBar,
+ progress: Int,
+ fromUser: Boolean) {
+ val newProgress: Int = progress + 1
+ prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
+ .apply()
+ dialogBinding!!.threadsCount.setText(newProgress.toString())
+ }
+ })
+ fetchStreamsSize()
+ }
+
+ private fun initToolbar(toolbar: Toolbar) {
+ if (DEBUG) {
+ Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]")
+ }
+ toolbar.setTitle(R.string.download_dialog_title)
+ toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
+ toolbar.inflateMenu(R.menu.dialog_url)
+ toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? -> dismiss() }))
+ toolbar.setNavigationContentDescription(R.string.cancel)
+ okButton = toolbar.findViewById(R.id.okay)
+ okButton.setEnabled(false) // disable until the download service connection is done
+ toolbar.setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener({ item: MenuItem ->
+ if (item.getItemId() == R.id.okay) {
+ prepareSelectedDownload()
+ return@setOnMenuItemClickListener true
+ }
+ false
+ }))
+ }
+
+ public override fun onDestroy() {
+ super.onDestroy()
+ disposables.clear()
+ }
+
+ public override fun onDestroyView() {
+ dialogBinding = null
+ super.onDestroyView()
+ }
+
+ public override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ Icepick.saveInstanceState(this, outState)
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Video, audio and subtitle spinners
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun fetchStreamsSize() {
+ disposables.clear()
+ disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper(wrappedVideoStreams)
+ .subscribe(Consumer({ result: Boolean? ->
+ if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
+ == R.id.video_button)) {
+ setupVideoSpinner()
+ }
+ }), Consumer({ throwable: Throwable? ->
+ showSnackbar((context)!!,
+ ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
+ "Downloading video stream size",
+ currentInfo!!.getServiceId()))
+ })))
+ disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper(getWrappedAudioStreams())
+ .subscribe(Consumer({ result: Boolean? ->
+ if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
+ == R.id.audio_button)) {
+ setupAudioSpinner()
+ }
+ }), Consumer({ throwable: Throwable? ->
+ showSnackbar((context)!!,
+ ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
+ "Downloading audio stream size",
+ currentInfo!!.getServiceId()))
+ })))
+ disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
+ .subscribe(Consumer({ result: Boolean? ->
+ if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
+ == R.id.subtitle_button)) {
+ setupSubtitleSpinner()
+ }
+ }), Consumer({ throwable: Throwable? ->
+ showSnackbar((context)!!,
+ ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
+ "Downloading subtitle stream size",
+ currentInfo!!.getServiceId()))
+ })))
+ }
+
+ private fun setupAudioTrackSpinner() {
+ if (getContext() == null) {
+ return
+ }
+ dialogBinding!!.audioTrackSpinner.setAdapter(audioTrackAdapter)
+ dialogBinding!!.audioTrackSpinner.setSelection(selectedAudioTrackIndex)
+ }
+
+ private fun setupAudioSpinner() {
+ if (getContext() == null) {
+ return
+ }
+ dialogBinding!!.qualitySpinner.setVisibility(View.GONE)
+ setRadioButtonsState(true)
+ dialogBinding!!.audioStreamSpinner.setAdapter(audioStreamsAdapter)
+ dialogBinding!!.audioStreamSpinner.setSelection(selectedAudioIndex)
+ dialogBinding!!.audioStreamSpinner.setVisibility(View.VISIBLE)
+ dialogBinding!!.audioTrackSpinner.setVisibility(
+ if (wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
+ dialogBinding!!.audioTrackPresentInVideoText.setVisibility(View.GONE)
+ }
+
+ private fun setupVideoSpinner() {
+ if (getContext() == null) {
+ return
+ }
+ dialogBinding!!.qualitySpinner.setAdapter(videoStreamsAdapter)
+ dialogBinding!!.qualitySpinner.setSelection(selectedVideoIndex)
+ dialogBinding!!.qualitySpinner.setVisibility(View.VISIBLE)
+ setRadioButtonsState(true)
+ dialogBinding!!.audioStreamSpinner.setVisibility(View.GONE)
+ onVideoStreamSelected()
+ }
+
+ private fun onVideoStreamSelected() {
+ val isVideoOnly: Boolean = videoStreamsAdapter!!.getItem(selectedVideoIndex)!!.isVideoOnly()
+ dialogBinding!!.audioTrackSpinner.setVisibility(
+ if (isVideoOnly && wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
+ dialogBinding!!.audioTrackPresentInVideoText.setVisibility(
+ if (!isVideoOnly && wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
+ }
+
+ private fun setupSubtitleSpinner() {
+ if (getContext() == null) {
+ return
+ }
+ dialogBinding!!.qualitySpinner.setAdapter(subtitleStreamsAdapter)
+ dialogBinding!!.qualitySpinner.setSelection(selectedSubtitleIndex)
+ dialogBinding!!.qualitySpinner.setVisibility(View.VISIBLE)
+ setRadioButtonsState(true)
+ dialogBinding!!.audioStreamSpinner.setVisibility(View.GONE)
+ dialogBinding!!.audioTrackSpinner.setVisibility(View.GONE)
+ dialogBinding!!.audioTrackPresentInVideoText.setVisibility(View.GONE)
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Activity results
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun requestDownloadPickAudioFolderResult(result: ActivityResult) {
+ requestDownloadPickFolderResult(
+ result, getString(R.string.download_path_audio_key), DownloadManager.Companion.TAG_AUDIO)
+ }
+
+ private fun requestDownloadPickVideoFolderResult(result: ActivityResult) {
+ requestDownloadPickFolderResult(
+ result, getString(R.string.download_path_video_key), DownloadManager.Companion.TAG_VIDEO)
+ }
+
+ private fun requestDownloadSaveAsResult(result: ActivityResult) {
+ if (result.getResultCode() != Activity.RESULT_OK) {
+ return
+ }
+ if (result.getData() == null || result.getData()!!.getData() == null) {
+ showFailedDialog(R.string.general_error)
+ return
+ }
+ if (FilePickerActivityHelper.Companion.isOwnFileUri((context)!!, result.getData()!!.getData()!!)) {
+ val file: File = Utils.getFileForUri(result.getData()!!.getData()!!)
+ checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
+ StoredFileHelper.Companion.DEFAULT_MIME)
+ return
+ }
+ val docFile: DocumentFile? = DocumentFile.fromSingleUri((context)!!,
+ result.getData()!!.getData()!!)
+ if (docFile == null) {
+ showFailedDialog(R.string.general_error)
+ return
+ }
+
+ // check if the selected file was previously used
+ checkSelectedDownload(null, result.getData()!!.getData(), docFile.getName(),
+ docFile.getType())
+ }
+
+ private fun requestDownloadPickFolderResult(result: ActivityResult,
+ key: String,
+ tag: String) {
+ if (result.getResultCode() != Activity.RESULT_OK) {
+ return
+ }
+ if (result.getData() == null || result.getData()!!.getData() == null) {
+ showFailedDialog(R.string.general_error)
+ return
+ }
+ var uri: Uri? = result.getData()!!.getData()
+ if (FilePickerActivityHelper.Companion.isOwnFileUri((context)!!, (uri)!!)) {
+ uri = Uri.fromFile(Utils.getFileForUri((uri)))
+ } else {
+ context!!.grantUriPermission(context!!.getPackageName(), uri,
+ StoredDirectoryHelper.Companion.PERMISSION_FLAGS)
+ }
+ PreferenceManager.getDefaultSharedPreferences((context)!!).edit().putString(key,
+ uri.toString()).apply()
+ try {
+ val mainStorage: StoredDirectoryHelper = StoredDirectoryHelper((context)!!, (uri)!!, tag)
+ checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
+ filenameTmp, mimeTmp)
+ } catch (e: IOException) {
+ showFailedDialog(R.string.general_error)
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Listeners
+ ////////////////////////////////////////////////////////////////////////// */
+ public override fun onCheckedChanged(group: RadioGroup, @IdRes checkedId: Int) {
+ if (DEBUG) {
+ Log.d(TAG, ("onCheckedChanged() called with: "
+ + "group = [" + group + "], checkedId = [" + checkedId + "]"))
+ }
+ var flag: Boolean = true
+ when (checkedId) {
+ R.id.audio_button -> setupAudioSpinner()
+ R.id.video_button -> setupVideoSpinner()
+ R.id.subtitle_button -> {
+ setupSubtitleSpinner()
+ flag = false
+ }
+ }
+ dialogBinding!!.threads.setEnabled(flag)
+ }
+
+ public override fun onItemSelected(parent: AdapterView<*>,
+ view: View,
+ position: Int,
+ id: Long) {
+ if (DEBUG) {
+ Log.d(TAG, ("onItemSelected() called with: "
+ + "parent = [" + parent + "], view = [" + view + "], "
+ + "position = [" + position + "], id = [" + id + "]"))
+ }
+ when (parent.getId()) {
+ R.id.quality_spinner -> {
+ when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
+ R.id.video_button -> {
+ selectedVideoIndex = position
+ onVideoStreamSelected()
+ }
+
+ R.id.subtitle_button -> selectedSubtitleIndex = position
+ }
+ onItemSelectedSetFileName()
+ }
+
+ R.id.audio_track_spinner -> {
+ val trackChanged: Boolean = selectedAudioTrackIndex != position
+ selectedAudioTrackIndex = position
+ if (trackChanged) {
+ updateSecondaryStreams()
+ fetchStreamsSize()
+ }
+ }
+
+ R.id.audio_stream_spinner -> selectedAudioIndex = position
+ }
+ }
+
+ private fun onItemSelectedSetFileName() {
+ val fileName: String? = FilenameUtils.createFilename(getContext(), currentInfo!!.getName())
+ val prevFileName: String = Optional.ofNullable(dialogBinding!!.fileName.getText())
+ .map(Function({ obj: Editable -> obj.toString() }))
+ .orElse("")
+ if ((prevFileName.isEmpty()
+ || (prevFileName == fileName) || prevFileName.startsWith(getString(R.string.caption_file_name, fileName, "")))) {
+ // only update the file name field if it was not edited by the user
+ when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
+ R.id.audio_button, R.id.video_button -> if (!(prevFileName == fileName)) {
+ // since the user might have switched between audio and video, the correct
+ // text might already be in place, so avoid resetting the cursor position
+ dialogBinding!!.fileName.setText(fileName)
+ }
+
+ R.id.subtitle_button -> {
+ val setSubtitleLanguageCode: String = subtitleStreamsAdapter
+ .getItem(selectedSubtitleIndex)!!.getLanguageTag()
+ // this will reset the cursor position, which is bad UX, but it can't be avoided
+ dialogBinding!!.fileName.setText(getString(
+ R.string.caption_file_name, fileName, setSubtitleLanguageCode))
+ }
+ }
+ }
+ }
+
+ public override fun onNothingSelected(parent: AdapterView<*>?) {}
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Download
+ ////////////////////////////////////////////////////////////////////////// */
+ protected fun setupDownloadOptions() {
+ setRadioButtonsState(false)
+ setupAudioTrackSpinner()
+ val isVideoStreamsAvailable: Boolean = videoStreamsAdapter!!.getCount() > 0
+ val isAudioStreamsAvailable: Boolean = audioStreamsAdapter!!.getCount() > 0
+ val isSubtitleStreamsAvailable: Boolean = subtitleStreamsAdapter!!.getCount() > 0
+ dialogBinding!!.audioButton.setVisibility(if (isAudioStreamsAvailable) View.VISIBLE else View.GONE)
+ dialogBinding!!.videoButton.setVisibility(if (isVideoStreamsAvailable) View.VISIBLE else View.GONE)
+ dialogBinding!!.subtitleButton.setVisibility(if (isSubtitleStreamsAvailable) View.VISIBLE else View.GONE)
+ prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ val defaultMedia: String? = prefs.getString(getString(R.string.last_used_download_type),
+ getString(R.string.last_download_type_video_key))
+ if ((isVideoStreamsAvailable
+ && ((defaultMedia == getString(R.string.last_download_type_video_key))))) {
+ dialogBinding!!.videoButton.setChecked(true)
+ setupVideoSpinner()
+ } else if ((isAudioStreamsAvailable
+ && ((defaultMedia == getString(R.string.last_download_type_audio_key))))) {
+ dialogBinding!!.audioButton.setChecked(true)
+ setupAudioSpinner()
+ } else if ((isSubtitleStreamsAvailable
+ && ((defaultMedia == getString(R.string.last_download_type_subtitle_key))))) {
+ dialogBinding!!.subtitleButton.setChecked(true)
+ setupSubtitleSpinner()
+ } else if (isVideoStreamsAvailable) {
+ dialogBinding!!.videoButton.setChecked(true)
+ setupVideoSpinner()
+ } else if (isAudioStreamsAvailable) {
+ dialogBinding!!.audioButton.setChecked(true)
+ setupAudioSpinner()
+ } else if (isSubtitleStreamsAvailable) {
+ dialogBinding!!.subtitleButton.setChecked(true)
+ setupSubtitleSpinner()
+ } else {
+ Toast.makeText(getContext(), R.string.no_streams_available_download,
+ Toast.LENGTH_SHORT).show()
+ dismiss()
+ }
+ }
+
+ private fun setRadioButtonsState(enabled: Boolean) {
+ dialogBinding!!.audioButton.setEnabled(enabled)
+ dialogBinding!!.videoButton.setEnabled(enabled)
+ dialogBinding!!.subtitleButton.setEnabled(enabled)
+ }
+
+ private fun getWrappedAudioStreams(): StreamInfoWrapper? {
+ if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks!!.size()) {
+ return StreamInfoWrapper.Companion.empty()
+ }
+ return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex)
+ }
+
+ private fun getSubtitleIndexBy(streams: List): Int {
+ val preferredLocalization: Localization = NewPipe.getPreferredLocalization()
+ var candidate: Int = 0
+ for (i in streams.indices) {
+ val streamLocale: Locale = streams.get(i)!!.getLocale()
+ val languageEquals: Boolean = (streamLocale.getLanguage() != null
+ ) && (preferredLocalization.getLanguageCode() != null
+ ) && (streamLocale.getLanguage()
+ == Locale(preferredLocalization.getLanguageCode()).getLanguage())
+ val countryEquals: Boolean = (streamLocale.getCountry() != null
+ && (streamLocale.getCountry() == preferredLocalization.getCountryCode()))
+ if (languageEquals) {
+ if (countryEquals) {
+ return i
+ }
+ candidate = i
+ }
+ }
+ return candidate
+ }
+
+ private fun getNameEditText(): String {
+ val str: String = Objects.requireNonNull(dialogBinding!!.fileName.getText()).toString()
+ .trim({ it <= ' ' })
+ return FilenameUtils.createFilename(context, if (str.isEmpty()) currentInfo!!.getName() else str)
+ }
+
+ private fun showFailedDialog(@StringRes msg: Int) {
+ org.schabi.newpipe.util.Localization.assureCorrectAppLanguage(requireContext())
+ AlertDialog.Builder((context)!!)
+ .setTitle(R.string.general_error)
+ .setMessage(msg)
+ .setNegativeButton(getString(R.string.ok), null)
+ .show()
+ }
+
+ private fun launchDirectoryPicker(launcher: ActivityResultLauncher) {
+ NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.Companion.getPicker(context), TAG,
+ context)
+ }
+
+ private fun prepareSelectedDownload() {
+ val mainStorage: StoredDirectoryHelper?
+ val format: MediaFormat?
+ val selectedMediaType: String
+ val size: Long
+
+ // first, build the filename and get the output folder (if possible)
+ // later, run a very very very large file checking logic
+ filenameTmp = getNameEditText() + "."
+ when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
+ R.id.audio_button -> {
+ selectedMediaType = getString(R.string.last_download_type_audio_key)
+ mainStorage = mainStorageAudio
+ format = audioStreamsAdapter!!.getItem(selectedAudioIndex)!!.getFormat()
+ size = getWrappedAudioStreams()!!.getSizeInBytes(selectedAudioIndex)
+ if (format == MediaFormat.WEBMA_OPUS) {
+ mimeTmp = "audio/ogg"
+ filenameTmp += "opus"
+ } else if (format != null) {
+ mimeTmp = format.mimeType
+ filenameTmp += format.getSuffix()
+ }
+ }
+
+ R.id.video_button -> {
+ selectedMediaType = getString(R.string.last_download_type_video_key)
+ mainStorage = mainStorageVideo
+ format = videoStreamsAdapter!!.getItem(selectedVideoIndex)!!.getFormat()
+ size = wrappedVideoStreams!!.getSizeInBytes(selectedVideoIndex)
+ if (format != null) {
+ mimeTmp = format.mimeType
+ filenameTmp += format.getSuffix()
+ }
+ }
+
+ R.id.subtitle_button -> {
+ selectedMediaType = getString(R.string.last_download_type_subtitle_key)
+ mainStorage = mainStorageVideo // subtitle & video files go together
+ format = subtitleStreamsAdapter!!.getItem(selectedSubtitleIndex)!!.getFormat()
+ size = wrappedSubtitleStreams!!.getSizeInBytes(selectedSubtitleIndex)
+ if (format != null) {
+ mimeTmp = format.mimeType
+ }
+ if (format == MediaFormat.TTML) {
+ filenameTmp += MediaFormat.SRT.getSuffix()
+ } else if (format != null) {
+ filenameTmp += format.getSuffix()
+ }
+ }
+
+ else -> throw RuntimeException("No stream selected")
+ }
+ if (!askForSavePath && ((mainStorage == null
+ ) || (mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
+ ) || mainStorage.isInvalidSafStorage())) {
+ // Pick new download folder if one of:
+ // - Download folder is not set
+ // - Download folder uses SAF while SAF is disabled
+ // - Download folder doesn't use SAF while SAF is enabled
+ // - Download folder uses SAF but the user manually revoked access to it
+ Toast.makeText(context, getString(R.string.no_dir_yet),
+ Toast.LENGTH_LONG).show()
+ if (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
+ launchDirectoryPicker(requestDownloadPickAudioFolderLauncher)
+ } else {
+ launchDirectoryPicker(requestDownloadPickVideoFolderLauncher)
+ }
+ return
+ }
+ if (askForSavePath) {
+ val initialPath: Uri?
+ if (NewPipeSettings.useStorageAccessFramework(context)) {
+ initialPath = null
+ } else {
+ val initialSavePath: File
+ if (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
+ initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC)
+ } else {
+ initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES)
+ }
+ initialPath = Uri.parse(initialSavePath.getAbsolutePath())
+ }
+ NoFileManagerSafeGuard.launchSafe