From 43674ae80a4e082b46917733fd7f017dcaf84518 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Tue, 4 Sep 2018 23:54:17 -0300 Subject: [PATCH 1/7] Improve tabs UX and saving/loading - Show icons in the tabs list and dialog chooser - Add a "restore to defaults" button - Make removing gesture more user intuitive --- .../newpipe/fragments/MainFragment.java | 278 ++++-------- .../newpipe/settings/AddTabsDialog.java | 41 -- .../newpipe/settings/ChoseTabsFragment.java | 291 ------------ .../settings/SelectChannelFragment.java | 4 +- .../newpipe/settings/SelectKioskFragment.java | 4 +- .../newpipe/settings/SettingsActivity.java | 3 +- .../newpipe/settings/tabs/AddTabDialog.java | 94 ++++ .../settings/tabs/ChooseTabsFragment.java | 386 ++++++++++++++++ .../org/schabi/newpipe/settings/tabs/Tab.java | 416 ++++++++++++++++++ .../newpipe/settings/tabs/TabsJsonHelper.java | 114 +++++ .../newpipe/settings/tabs/TabsManager.java | 93 ++++ .../res/drawable/ic_blank_page_black_24dp.xml | 9 + .../res/drawable/ic_blank_page_white_24dp.xml | 9 + .../ic_settings_backup_restore_black_24dp.xml | 9 + .../ic_settings_backup_restore_white_24dp.xml | 9 + ...hose_tabs.xml => fragment_choose_tabs.xml} | 24 +- app/src/main/res/layout/list_choose_tabs.xml | 62 +++ .../res/layout/list_choose_tabs_dialog.xml | 33 ++ .../main/res/layout/viewholder_chose_tabs.xml | 40 -- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/settings_keys.xml | 19 +- app/src/main/res/values/strings.xml | 13 +- app/src/main/res/values/styles.xml | 4 + app/src/main/res/xml/appearance_settings.xml | 2 +- .../schabi/newpipe/settings/tabs/TabTest.java | 20 + .../settings/tabs/TabsJsonHelperTest.java | 120 +++++ 26 files changed, 1493 insertions(+), 606 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/AddTabsDialog.java delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/ChoseTabsFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java create mode 100644 app/src/main/res/drawable/ic_blank_page_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_blank_page_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml rename app/src/main/res/layout/{fragment_chose_tabs.xml => fragment_choose_tabs.xml} (65%) create mode 100644 app/src/main/res/layout/list_choose_tabs.xml create mode 100644 app/src/main/res/layout/list_choose_tabs_dialog.xml delete mode 100644 app/src/main/res/layout/viewholder_chose_tabs.xml create mode 100644 app/src/test/java/org/schabi/newpipe/settings/tabs/TabTest.java create mode 100644 app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index a920ecfe6..de14997ef 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.fragments; -import android.content.Context; -import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -12,7 +10,6 @@ import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; -import android.support.v7.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -23,42 +20,26 @@ import android.view.ViewGroup; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.fragments.list.channel.ChannelFragment; -import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; -import org.schabi.newpipe.local.bookmark.BookmarkFragment; -import org.schabi.newpipe.local.feed.FeedFragment; -import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; -import org.schabi.newpipe.local.subscription.SubscriptionFragment; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.KioskTranslator; +import org.schabi.newpipe.settings.tabs.Tab; +import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; -import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { - - public int currentServiceId = -1; private ViewPager viewPager; - private List tabs = new ArrayList<>(); - static PagerAdapter adapter; - TabLayout tabLayout; - private SharedPreferences prefs; - private Bundle savedInstanceStateBundle; + private SelectedTabsPagerAdapter pagerAdapter; + private TabLayout tabLayout; - private static final String TAB_NUMBER_BLANK = "0"; - private static final String TAB_NUMBER_KIOSK = "1"; - private static final String TAB_NUMBER_SUBSCIRPTIONS = "2"; - private static final String TAB_NUMBER_FEED = "3"; - private static final String TAB_NUMBER_BOOKMARKS = "4"; - private static final String TAB_NUMBER_HISTORY = "5"; - private static final String TAB_NUMBER_CHANNEL = "6"; + private List tabsList = new ArrayList<>(); + private TabsManager tabsManager; - SharedPreferences.OnSharedPreferenceChangeListener listener; + private boolean hasTabsChanged = false; /*////////////////////////////////////////////////////////////////////////// // Fragment's LifeCycle @@ -66,23 +47,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public void onCreate(Bundle savedInstanceState) { - savedInstanceStateBundle = savedInstanceState; super.onCreate(savedInstanceState); setHasOptionsMenu(true); - listener = (prefs, key) -> { - if(key.equals("saveUsedTabs")) { - mainPageChanged(); + + tabsManager = TabsManager.getManager(activity); + tabsManager.setSavedTabsListener(() -> { + if (DEBUG) { + Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed()); } - }; + if (isResumed()) { + updateTabs(); + } else { + hasTabsChanged = true; + } + }); } @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - currentServiceId = ServiceHelper.getSelectedServiceId(activity); - - prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - prefs.registerOnSharedPreferenceChangeListener(listener); - return inflater.inflate(R.layout.fragment_main, container, false); } @@ -94,110 +76,28 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte viewPager = rootView.findViewById(R.id.pager); /* Nested fragment, use child fragment here to maintain backstack in view pager. */ - adapter = new PagerAdapter(getChildFragmentManager()); - viewPager.setAdapter(adapter); + pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager()); + viewPager.setAdapter(pagerAdapter); tabLayout.setupWithViewPager(viewPager); - - mainPageChanged(); + tabLayout.addOnTabSelectedListener(this); + updateTabs(); } + @Override + public void onResume() { + super.onResume(); - public void mainPageChanged() { - getTabOrder(); - adapter.notifyDataSetChanged(); - viewPager.setOffscreenPageLimit(adapter.getCount()); - setIcons(); - setFirstTitle(); - } - - private void setFirstTitle() { - if((tabs.size() > 0) - && activity != null) { - String tabInformation = tabs.get(0); - if (tabInformation.startsWith(TAB_NUMBER_KIOSK + "\t")) { - String kiosk[] = tabInformation.split("\t"); - if (kiosk.length == 3) { - setTitle(kiosk[1]); - } - } else if (tabInformation.startsWith(TAB_NUMBER_CHANNEL + "\t")) { - - String channelInfo[] = tabInformation.split("\t"); - if(channelInfo.length==4) { - setTitle(channelInfo[2]); - } - } else { - switch (tabInformation) { - case TAB_NUMBER_BLANK: - setTitle(getString(R.string.app_name)); - break; - case TAB_NUMBER_SUBSCIRPTIONS: - setTitle(getString(R.string.tab_subscriptions)); - break; - case TAB_NUMBER_FEED: - setTitle(getString(R.string.fragment_whats_new)); - break; - case TAB_NUMBER_BOOKMARKS: - setTitle(getString(R.string.tab_bookmarks)); - break; - case TAB_NUMBER_HISTORY: - setTitle(getString(R.string.title_activity_history)); - break; - } - } - - + if (hasTabsChanged) { + hasTabsChanged = false; + updateTabs(); } } - private void setIcons() { - for (int i = 0; i < tabs.size(); i++) { - String tabInformation = tabs.get(i); - - TabLayout.Tab tabToSet = tabLayout.getTabAt(i); - Context c = getContext(); - - if (tabToSet != null && c != null) { - - if (tabInformation.startsWith(TAB_NUMBER_KIOSK + "\t")) { - String kiosk[] = tabInformation.split("\t"); - if (kiosk.length == 3) { - tabToSet.setIcon(KioskTranslator.getKioskIcons(kiosk[1], getContext())); - } - } else if (tabInformation.startsWith(TAB_NUMBER_CHANNEL + "\t")) { - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_channel)); - } else { - switch (tabInformation) { - case TAB_NUMBER_BLANK: - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_hot)); - break; - case TAB_NUMBER_SUBSCIRPTIONS: - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_channel)); - break; - case TAB_NUMBER_FEED: - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.rss)); - break; - case TAB_NUMBER_BOOKMARKS: - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_bookmark)); - break; - case TAB_NUMBER_HISTORY: - tabToSet.setIcon(ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.history)); - break; - } - } - - } - } - } - - - private void getTabOrder() { - tabs.clear(); - - String save = prefs.getString("saveUsedTabs", "1\tTrending\t0\n2\n4\n"); - String tabsArray[] = save.trim().split("\n"); - - Collections.addAll(tabs, tabsArray); + @Override + public void onDestroy() { + super.onDestroy(); + tabsManager.unsetSavedTabsListener(); } /*////////////////////////////////////////////////////////////////////////// @@ -237,9 +137,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte // Tabs //////////////////////////////////////////////////////////////////////////*/ + public void updateTabs() { + tabsList.clear(); + tabsList.addAll(tabsManager.getTabs()); + pagerAdapter.notifyDataSetChanged(); + + viewPager.setOffscreenPageLimit(pagerAdapter.getCount()); + updateTabsIcon(); + updateCurrentTitle(); + } + + private void updateTabsIcon() { + for (int i = 0; i < tabsList.size(); i++) { + final TabLayout.Tab tabToSet = tabLayout.getTabAt(i); + if (tabToSet != null) { + tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity)); + } + } + } + + private void updateCurrentTitle() { + setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext())); + } + @Override - public void onTabSelected(TabLayout.Tab tab) { - viewPager.setCurrentItem(tab.getPosition()); + public void onTabSelected(TabLayout.Tab selectedTab) { + if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]"); + updateCurrentTitle(); } @Override @@ -248,68 +172,40 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public void onTabReselected(TabLayout.Tab tab) { + if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]"); + updateCurrentTitle(); } - private class PagerAdapter extends FragmentPagerAdapter { - PagerAdapter(FragmentManager fm) { - super(fm); + private class SelectedTabsPagerAdapter extends FragmentPagerAdapter { + private SelectedTabsPagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); } @Override public Fragment getItem(int position) { - String tabInformation = tabs.get(position); + final Tab tab = tabsList.get(position); - if(tabInformation.startsWith(TAB_NUMBER_KIOSK + "\t")) { - String kiosk[] = tabInformation.split("\t"); - if(kiosk.length==3) { - KioskFragment fragment = null; - try { - fragment = KioskFragment.getInstance(Integer.parseInt(kiosk[2]), kiosk[1]); - fragment.useAsFrontPage(true); - return fragment; - } catch (Exception e) { - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); - } - } - } else if(tabInformation.startsWith(TAB_NUMBER_CHANNEL + "\t")) { - String channelInfo[] = tabInformation.split("\t"); - if(channelInfo.length==4) { - ChannelFragment fragment = ChannelFragment.getInstance(Integer.parseInt(channelInfo[3]), channelInfo[1], channelInfo[2]); - fragment.useAsFrontPage(true); - return fragment; - } else { - return new BlankFragment(); - } - } else { - switch (tabInformation) { - case TAB_NUMBER_BLANK: - return new BlankFragment(); - case TAB_NUMBER_SUBSCIRPTIONS: - SubscriptionFragment sFragment = new SubscriptionFragment(); - sFragment.useAsFrontPage(true); - return sFragment; - case TAB_NUMBER_FEED: - FeedFragment fFragment = new FeedFragment(); - fFragment.useAsFrontPage(true); - return fFragment; - case TAB_NUMBER_BOOKMARKS: - BookmarkFragment bFragment = new BookmarkFragment(); - bFragment.useAsFrontPage(true); - return bFragment; - case TAB_NUMBER_HISTORY: - StatisticsPlaylistFragment cFragment = new StatisticsPlaylistFragment(); - cFragment.useAsFrontPage(true); - return cFragment; - } - } - - return new BlankFragment(); + Throwable throwable = null; + Fragment fragment = null; + try { + fragment = tab.getFragment(); + } catch (ExtractionException e) { + throwable = e; } + if (throwable != null) { + ErrorActivity.reportError(activity, throwable, activity.getClass(), null, + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + return new BlankFragment(); + } + + if (fragment instanceof BaseFragment) { + ((BaseFragment) fragment).useAsFrontPage(true); + } + + return fragment; + } + @Override public int getItemPosition(Object object) { // Causes adapter to reload all Fragments when @@ -319,14 +215,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public int getCount() { - return tabs.size(); + return tabsList.size(); } @Override public void destroyItem(ViewGroup container, int position, Object object) { - getFragmentManager() + getChildFragmentManager() .beginTransaction() - .remove((Fragment)object) + .remove((Fragment) object) .commitNowAllowingStateLoss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/AddTabsDialog.java b/app/src/main/java/org/schabi/newpipe/settings/AddTabsDialog.java deleted file mode 100644 index 5130df3bf..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/AddTabsDialog.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.View; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; - -public class AddTabsDialog { - private final AlertDialog dialog; - - public AddTabsDialog(@NonNull final Context context, - @NonNull final String title, - @NonNull final String[] commands, - @NonNull final DialogInterface.OnClickListener actions) { - - final View bannerView = View.inflate(context, R.layout.dialog_title, null); - bannerView.setSelected(true); - - TextView titleView = bannerView.findViewById(R.id.itemTitleView); - titleView.setText(title); - - TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails); - detailsView.setVisibility(View.GONE); - - dialog = new AlertDialog.Builder(context) - .setCustomTitle(bannerView) - .setItems(commands, actions) - .create(); - } - - public void show() { - dialog.show(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ChoseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ChoseTabsFragment.java deleted file mode 100644 index d6238c7c4..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/ChoseTabsFragment.java +++ /dev/null @@ -1,291 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.app.Dialog; -import android.content.SharedPreferences; -import android.content.res.ColorStateList; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.CardView; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.util.ThemeHelper; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ChoseTabsFragment extends Fragment { - - public ChoseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; - - RecyclerView selectedTabsView; - - List selectedTabs = new ArrayList<>(); - private String saveString; - public String[] availableTabs = new String[7]; - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - ((AppCompatActivity)getContext()).getSupportActionBar().setTitle(R.string.main_page_content); - return inflater.inflate(R.layout.fragment_chose_tabs, container, false); - } - - - @Override - public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { - super.onViewCreated(rootView, savedInstanceState); - - tabNames(); - initUsedTabs(); - initButton(rootView); - - selectedTabsView = rootView.findViewById(R.id.usedTabs); - selectedTabsView.setLayoutManager(new LinearLayoutManager(getContext())); - selectedTabsAdapter = new ChoseTabsFragment.SelectedTabsAdapter(); - - - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(selectedTabsView); - selectedTabsAdapter.setOnItemSelectedListener(itemTouchHelper); - - selectedTabsView.setAdapter(selectedTabsAdapter); - } - - private void saveChanges() { - StringBuilder save = new StringBuilder(); - if(selectedTabs.size()==0) { - save = new StringBuilder("0"); - } else { - for(String s: selectedTabs) { - save.append(s); - save.append("\n"); - } - } - saveString = save.toString(); - } - - @Override - public void onPause() { - saveChanges(); - SharedPreferences sharedPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(getContext()); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString("saveUsedTabs", saveString); - editor.commit(); - super.onPause(); - } - - private void initUsedTabs() { - String save = android.preference.PreferenceManager.getDefaultSharedPreferences(getContext()).getString("saveUsedTabs", "1\tTrending\t0\n2\n4\n"); - String tabs[] = save.trim().split("\n"); - selectedTabs.addAll(Arrays.asList(tabs)); - } - - private void tabNames() { - availableTabs[0] = getString(R.string.blank_page_summary); - availableTabs[1] = getString(R.string.kiosk_page_summary); - availableTabs[2] = getString(R.string.subscription_page_summary); - availableTabs[3] = getString(R.string.feed_page_summary); - availableTabs[4] = getString(R.string.tab_bookmarks); - availableTabs[5] = getString(R.string.title_activity_history); - availableTabs[6] = getString(R.string.channel_page_summary); - } - - private void initButton(View rootView) { - FloatingActionButton fab = rootView.findViewById(R.id.floatingActionButton); - fab.setImageResource(ThemeHelper.getIconByAttr(R.attr.ic_add, getContext())); - fab.setOnClickListener(v -> { - Dialog.OnClickListener onClickListener = (dialog, which) -> addTab(which); - - new AddTabsDialog(getContext(), - getString(R.string.tab_chose), - availableTabs, - onClickListener) - .show(); - }); - - TypedValue typedValue = new TypedValue(); - getActivity().getTheme().resolveAttribute(R.attr.colorPrimary, typedValue, true); - int color = typedValue.data; - fab.setBackgroundTintList(ColorStateList.valueOf(color)); - } - - - private void addTab(int position) { - if(position==6) { - SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); - selectChannelFragment.setOnSelectedLisener((String url, String name, int service) -> { - selectedTabs.add(position+"\t"+url+"\t"+name+"\t"+service); - selectedTabsAdapter.notifyDataSetChanged(); - saveChanges(); - }); - selectChannelFragment.show(getFragmentManager(), "select_channel"); - } else if(position==1) { - SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); - selectKioskFragment.setOnSelectedLisener((String kioskId, int service_id) -> { - selectedTabs.add(position+"\t"+kioskId+"\t"+service_id); - selectedTabsAdapter.notifyDataSetChanged(); - saveChanges(); - }); - selectKioskFragment.show(getFragmentManager(), "select_kiosk"); - } else { - selectedTabs.add(String.valueOf(position)); - selectedTabsAdapter.notifyDataSetChanged(); - saveChanges(); - } - } - - public class SelectedTabsAdapter extends RecyclerView.Adapter{ - private ItemTouchHelper itemTouchHelper; - - public void setOnItemSelectedListener(ItemTouchHelper mItemTouchHelper) { - itemTouchHelper = mItemTouchHelper; - } - - public void swapItems(int fromPosition, int toPosition) { - String temp = selectedTabs.get(fromPosition); - selectedTabs.set(fromPosition, selectedTabs.get(toPosition)); - selectedTabs.set(toPosition, temp); - notifyItemMoved(fromPosition, toPosition); - saveChanges(); - } - - @Override - public ChoseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - - LayoutInflater inflater = LayoutInflater.from(getContext()); - View view = inflater.inflate(R.layout.viewholder_chose_tabs, parent, false); - return new ChoseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ChoseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, int position) { - holder.bind(position, holder); - } - - @Override - public int getItemCount() { - return selectedTabs.size(); - } - - class TabViewHolder extends RecyclerView.ViewHolder { - - TextView text; - View view; - CardView cardView; - ImageView handle; - - public TabViewHolder(View itemView) { - super(itemView); - - text = itemView.findViewById(R.id.tabName); - cardView = itemView.findViewById(R.id.layoutCard); - handle = itemView.findViewById(R.id.handle); - view = itemView; - } - - void bind(int position, TabViewHolder holder) { - handle.setImageResource(ThemeHelper.getIconByAttr(R.attr.drag_handle, getContext())); - handle.setOnTouchListener(getOnTouchListener(holder)); - - view.setOnLongClickListener(getOnLongClickListener(holder)); - - if(selectedTabs.get(position).startsWith("6\t")) { - String channelInfo[] = selectedTabs.get(position).split("\t"); - String channelName = ""; - if (channelInfo.length == 4) channelName = channelInfo[2]; - String textToSet = availableTabs[6] + ": " + channelName; - text.setText(textToSet); - } else if(selectedTabs.get(position).startsWith("1\t")) { - String kioskInfo[] = selectedTabs.get(position).split("\t"); - String kioskName = ""; - if (kioskInfo.length == 3) kioskName = kioskInfo[1]; - String textToSet = availableTabs[1] + ": " + kioskName; - text.setText(textToSet); - } else { - text.setText(availableTabs[Integer.parseInt(selectedTabs.get(position))]); - } - } - - private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { - return (view, motionEvent) -> { - view.performClick(); - if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - if(itemTouchHelper != null) itemTouchHelper.startDrag(item); - } - return false; - }; - } - - private View.OnLongClickListener getOnLongClickListener(TabViewHolder holder) { - return (view) -> { - if(itemTouchHelper != null) itemTouchHelper.startSwipe(holder); - return false; - }; - } - - } - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, - ItemTouchHelper.START | ItemTouchHelper.END) { - @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { - final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, - viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int minimumAbsVelocity = Math.max(12, - Math.abs(standardSpeed)); - return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType() || - selectedTabsAdapter == null) { - return false; - } - - final int sourceIndex = source.getAdapterPosition(); - final int targetIndex = target.getAdapterPosition(); - selectedTabsAdapter.swapItems(sourceIndex, targetIndex); - return true; - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - int position = viewHolder.getAdapterPosition(); - selectedTabs.remove(position); - selectedTabsAdapter.notifyItemRemoved(position); - saveChanges(); - } - }; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index e961de969..0ebdbefe0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -66,7 +66,7 @@ public class SelectChannelFragment extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ public interface OnSelectedLisener { - void onChannelSelected(String url, String name, int service); + void onChannelSelected(int serviceId, String url, String name); } OnSelectedLisener onSelectedLisener = null; public void setOnSelectedLisener(OnSelectedLisener listener) { @@ -126,7 +126,7 @@ public class SelectChannelFragment extends DialogFragment { private void clickedItem(int position) { if(onSelectedLisener != null) { SubscriptionEntity entry = subscriptions.get(position); - onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId()); + onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 00b618889..44cb16682 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -56,7 +56,7 @@ public class SelectKioskFragment extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ public interface OnSelectedLisener { - void onKioskSelected(String kioskId, int service_id); + void onKioskSelected(int serviceId, String kioskId, String kioskName); } OnSelectedLisener onSelectedLisener = null; @@ -101,7 +101,7 @@ public class SelectKioskFragment extends DialogFragment { private void clickedItem(SelectKioskAdapter.Entry entry) { if(onSelectedLisener != null) { - onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId); + onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); } dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 7d6f8d633..a8482e0eb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc finish(); } else getSupportFragmentManager().popBackStack(); } - return true; + + return super.onOptionsItemSelected(item); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java new file mode 100644 index 000000000..695f81ff5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java @@ -0,0 +1,94 @@ +package org.schabi.newpipe.settings.tabs; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v7.widget.AppCompatImageView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ThemeHelper; + +public class AddTabDialog { + private final AlertDialog dialog; + + AddTabDialog(@NonNull final Context context, + @NonNull final ChooseTabListItem[] items, + @NonNull final DialogInterface.OnClickListener actions) { + + dialog = new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.tab_choose)) + .setAdapter(new DialogListAdapter(context, items), actions) + .create(); + } + + public void show() { + dialog.show(); + } + + public static final class ChooseTabListItem { + final int tabId; + final String itemName; + @DrawableRes final int itemIcon; + + ChooseTabListItem(Context context, Tab tab) { + this(tab.getTabId(), tab.getTabName(context), tab.getTabIconRes(context)); + } + + ChooseTabListItem(int tabId, String itemName, @DrawableRes int itemIcon) { + this.tabId = tabId; + this.itemName = itemName; + this.itemIcon = itemIcon; + } + } + + private static class DialogListAdapter extends BaseAdapter { + private final LayoutInflater inflater; + private final ChooseTabListItem[] items; + + @DrawableRes private final int fallbackIcon; + + private DialogListAdapter(Context context, ChooseTabListItem[] items) { + this.inflater = LayoutInflater.from(context); + this.items = items; + this.fallbackIcon = ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot); + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public ChooseTabListItem getItem(int position) { + return items[position]; + } + + @Override + public long getItemId(int position) { + return getItem(position).tabId; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.list_choose_tabs_dialog, parent, false); + } + + final ChooseTabListItem item = getItem(position); + final AppCompatImageView tabIconView = convertView.findViewById(R.id.tabIcon); + final TextView tabNameView = convertView.findViewById(R.id.tabName); + + tabIconView.setImageResource(item.itemIcon > 0 ? item.itemIcon : fallbackIcon); + tabNameView.setText(item.itemName); + + return convertView; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java new file mode 100644 index 000000000..b86f13d14 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -0,0 +1,386 @@ +package org.schabi.newpipe.settings.tabs; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.content.res.AppCompatResources; +import android.support.v7.widget.AppCompatImageView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.settings.SelectChannelFragment; +import org.schabi.newpipe.settings.SelectKioskFragment; +import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; +import org.schabi.newpipe.util.ThemeHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; + +public class ChooseTabsFragment extends Fragment { + + private TabsManager tabsManager; + private List tabList = new ArrayList<>(); + public ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; + + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + tabsManager = TabsManager.getManager(requireContext()); + updateTabList(); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_choose_tabs, container, false); + } + + @Override + public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); + + initButton(rootView); + + RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); + listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext())); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(listSelectedTabs); + + selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper); + listSelectedTabs.setAdapter(selectedTabsAdapter); + } + + @Override + public void onResume() { + super.onResume(); + updateTitle(); + } + + @Override + public void onPause() { + super.onPause(); + saveChanges(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + private final int MENU_ITEM_RESTORE_ID = 123456; + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); + restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + + final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); + restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon)); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == MENU_ITEM_RESTORE_ID) { + restoreDefaults(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void updateTabList() { + tabList.clear(); + tabList.addAll(tabsManager.getTabs()); + } + + private void updateTitle() { + if (getActivity() instanceof AppCompatActivity) { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) actionBar.setTitle(R.string.main_page_content); + } + } + + private void saveChanges() { + tabsManager.saveTabs(tabList); + } + + private void restoreDefaults() { + new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext())) + .setTitle(R.string.restore_defaults) + .setMessage(R.string.restore_defaults_confirmation) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.yes, (dialog, which) -> { + tabsManager.resetTabs(); + updateTabList(); + selectedTabsAdapter.notifyDataSetChanged(); + }) + .show(); + } + + private void initButton(View rootView) { + final FloatingActionButton fab = rootView.findViewById(R.id.addTabsButton); + fab.setOnClickListener(v -> { + final ChooseTabListItem[] availableTabs = getAvailableTabs(requireContext()); + + if (availableTabs.length == 0) { + //Toast.makeText(requireContext(), "No available tabs", Toast.LENGTH_SHORT).show(); + return; + } + + Dialog.OnClickListener actionListener = (dialog, which) -> { + final ChooseTabListItem selected = availableTabs[which]; + addTab(selected.tabId); + }; + + new AddTabDialog(requireContext(), availableTabs, actionListener) + .show(); + }); + } + + private void addTab(final Tab tab) { + tabList.add(tab); + selectedTabsAdapter.notifyDataSetChanged(); + } + + private void addTab(int tabId) { + final Tab.Type type = typeFrom(tabId); + + if (type == null) { + ErrorActivity.reportError(requireContext(), new IllegalStateException("Tab id not found: " + tabId), null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Choosing tabs on settings", 0)); + return; + } + + switch (type) { + case KIOSK: { + SelectKioskFragment selectFragment = new SelectKioskFragment(); + selectFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) -> + addTab(new Tab.KioskTab(serviceId, kioskId))); + selectFragment.show(requireFragmentManager(), "select_kiosk"); + return; + } + case CHANNEL: { + SelectChannelFragment selectFragment = new SelectChannelFragment(); + selectFragment.setOnSelectedLisener((serviceId, url, name) -> + addTab(new Tab.ChannelTab(serviceId, url, name))); + selectFragment.show(requireFragmentManager(), "select_channel"); + return; + } + default: + addTab(type.getTab()); + break; + } + } + + public ChooseTabListItem[] getAvailableTabs(Context context) { + final ArrayList returnList = new ArrayList<>(); + + for (Tab.Type type : Tab.Type.values()) { + final Tab tab = type.getTab(); + switch (type) { + case BLANK: + if (!tabList.contains(tab)) { + returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.blank_page_summary), + tab.getTabIconRes(context))); + } + break; + case KIOSK: + returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.kiosk_page_summary), + ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot))); + break; + case CHANNEL: + returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary), + tab.getTabIconRes(context))); + break; + default: + if (!tabList.contains(tab)) { + returnList.add(new ChooseTabListItem(context, tab)); + } + break; + } + } + + return returnList.toArray(new ChooseTabListItem[0]); + } + + /*////////////////////////////////////////////////////////////////////////// + // List Handling + //////////////////////////////////////////////////////////////////////////*/ + + private class SelectedTabsAdapter extends RecyclerView.Adapter { + private ItemTouchHelper itemTouchHelper; + private final LayoutInflater inflater; + + SelectedTabsAdapter(Context context, ItemTouchHelper itemTouchHelper) { + this.itemTouchHelper = itemTouchHelper; + this.inflater = LayoutInflater.from(context); + } + + public void swapItems(int fromPosition, int toPosition) { + Collections.swap(tabList, fromPosition, toPosition); + notifyItemMoved(fromPosition, toPosition); + } + + @NonNull + @Override + public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); + return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, int position) { + holder.bind(position, holder); + } + + @Override + public int getItemCount() { + return tabList.size(); + } + + class TabViewHolder extends RecyclerView.ViewHolder { + private AppCompatImageView tabIconView; + private TextView tabNameView; + private ImageView handle; + + TabViewHolder(View itemView) { + super(itemView); + + tabNameView = itemView.findViewById(R.id.tabName); + tabIconView = itemView.findViewById(R.id.tabIcon); + handle = itemView.findViewById(R.id.handle); + } + + @SuppressLint("ClickableViewAccessibility") + void bind(int position, TabViewHolder holder) { + handle.setOnTouchListener(getOnTouchListener(holder)); + + final Tab tab = tabList.get(position); + final Tab.Type type = Tab.typeFrom(tab.getTabId()); + + if (type == null) { + return; + } + + String tabName = tab.getTabName(requireContext()); + switch (type) { + case BLANK: + tabName = requireContext().getString(R.string.blank_page_summary); + break; + case KIOSK: + tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName; + break; + case CHANNEL: + tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName; + break; + } + + + tabNameView.setText(tabName); + tabIconView.setImageResource(tab.getTabIconRes(requireContext())); + } + + @SuppressLint("ClickableViewAccessibility") + private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { + return (view, motionEvent) -> { + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (itemTouchHelper != null && getItemCount() > 1) { + itemTouchHelper.startDrag(item); + return true; + } + } + return false; + }; + } + } + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.START | ItemTouchHelper.END) { + @Override + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(12, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + selectedTabsAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + selectedTabsAdapter.swapItems(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { + int position = viewHolder.getAdapterPosition(); + tabList.remove(position); + selectedTabsAdapter.notifyItemRemoved(position); + + if (tabList.isEmpty()) { + tabList.add(Tab.Type.BLANK.getTab()); + selectedTabsAdapter.notifyItemInserted(0); + } + } + }; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java new file mode 100644 index 000000000..d7c249a3e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -0,0 +1,416 @@ +package org.schabi.newpipe.settings.tabs; + +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonSink; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.fragments.BlankFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.local.bookmark.BookmarkFragment; +import org.schabi.newpipe.local.feed.FeedFragment; +import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.subscription.SubscriptionFragment; +import org.schabi.newpipe.util.KioskTranslator; +import org.schabi.newpipe.util.ThemeHelper; + +public abstract class Tab { + Tab() { + } + + Tab(@NonNull JsonObject jsonObject) { + readDataFromJson(jsonObject); + } + + public abstract int getTabId(); + public abstract String getTabName(Context context); + @DrawableRes public abstract int getTabIconRes(Context context); + + /** + * Return a instance of the fragment that this tab represent. + */ + public abstract Fragment getFragment() throws ExtractionException; + + @Override + public boolean equals(Object obj) { + return obj instanceof Tab && obj.getClass().equals(this.getClass()) + && ((Tab) obj).getTabId() == this.getTabId(); + } + + /*////////////////////////////////////////////////////////////////////////// + // JSON Handling + //////////////////////////////////////////////////////////////////////////*/ + + private static final String JSON_TAB_ID_KEY = "tab_id"; + + public void writeJsonOn(JsonSink jsonSink) { + jsonSink.object(); + + jsonSink.value(JSON_TAB_ID_KEY, getTabId()); + writeDataToJson(jsonSink); + + jsonSink.end(); + } + + protected void writeDataToJson(JsonSink writerSink) { + // No-op + } + + protected void readDataFromJson(JsonObject jsonObject) { + // No-op + } + + /*////////////////////////////////////////////////////////////////////////// + // Tab Handling + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + public static Tab from(@NonNull JsonObject jsonObject) { + final int tabId = jsonObject.getInt(Tab.JSON_TAB_ID_KEY, -1); + + if (tabId == -1) { + return null; + } + + return from(tabId, jsonObject); + } + + @Nullable + public static Tab from(final int tabId) { + return from(tabId, null); + } + + @Nullable + public static Type typeFrom(int tabId) { + for (Type available : Type.values()) { + if (available.getTabId() == tabId) { + return available; + } + } + return null; + } + + @Nullable + private static Tab from(final int tabId, @Nullable JsonObject jsonObject) { + final Type type = typeFrom(tabId); + + if (type == null) { + return null; + } + + if (jsonObject != null) { + switch (type) { + case KIOSK: + return new KioskTab(jsonObject); + case CHANNEL: + return new ChannelTab(jsonObject); + } + } + + return type.getTab(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Implementations + //////////////////////////////////////////////////////////////////////////*/ + + public enum Type { + BLANK(new BlankTab()), + SUBSCRIPTIONS(new SubscriptionsTab()), + FEED(new FeedTab()), + BOOKMARKS(new BookmarksTab()), + HISTORY(new HistoryTab()), + KIOSK(new KioskTab()), + CHANNEL(new ChannelTab()); + + private Tab tab; + + Type(Tab tab) { + this.tab = tab; + } + + public int getTabId() { + return tab.getTabId(); + } + + public Tab getTab() { + return tab; + } + } + + public static class BlankTab extends Tab { + public static final int ID = 0; + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return "NewPipe"; //context.getString(R.string.blank_page_summary); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_blank_page); + } + + @Override + public BlankFragment getFragment() { + return new BlankFragment(); + } + } + + public static class SubscriptionsTab extends Tab { + public static final int ID = 1; + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return context.getString(R.string.tab_subscriptions); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); + } + + @Override + public SubscriptionFragment getFragment() { + return new SubscriptionFragment(); + } + + } + + public static class FeedTab extends Tab { + public static final int ID = 2; + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return context.getString(R.string.fragment_whats_new); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.rss); + } + + @Override + public FeedFragment getFragment() { + return new FeedFragment(); + } + } + + public static class BookmarksTab extends Tab { + public static final int ID = 3; + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return context.getString(R.string.tab_bookmarks); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); + } + + @Override + public BookmarkFragment getFragment() { + return new BookmarkFragment(); + } + } + + public static class HistoryTab extends Tab { + public static final int ID = 4; + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return context.getString(R.string.title_activity_history); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.history); + } + + @Override + public StatisticsPlaylistFragment getFragment() { + return new StatisticsPlaylistFragment(); + } + } + + public static class KioskTab extends Tab { + public static final int ID = 5; + + private int kioskServiceId; + private String kioskId; + + private static final String JSON_KIOSK_SERVICE_ID_KEY = "service_id"; + private static final String JSON_KIOSK_ID_KEY = "kiosk_id"; + + private KioskTab() { + this(-1, ""); + } + + public KioskTab(int kioskServiceId, String kioskId) { + this.kioskServiceId = kioskServiceId; + this.kioskId = kioskId; + } + + public KioskTab(JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return KioskTranslator.getTranslatedKioskName(kioskId, context); + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context); + + if (kioskIcon <= 0) { + throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\""); + } + + return kioskIcon; + } + + @Override + public KioskFragment getFragment() throws ExtractionException { + return KioskFragment.getInstance(kioskServiceId, kioskId); + } + + @Override + protected void writeDataToJson(JsonSink writerSink) { + writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId) + .value(JSON_KIOSK_ID_KEY, kioskId); + } + + @Override + protected void readDataFromJson(JsonObject jsonObject) { + kioskServiceId = jsonObject.getInt(JSON_KIOSK_SERVICE_ID_KEY, -1); + kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, ""); + } + + public int getKioskServiceId() { + return kioskServiceId; + } + + public String getKioskId() { + return kioskId; + } + } + + public static class ChannelTab extends Tab { + public static final int ID = 6; + + private int channelServiceId; + private String channelUrl; + private String channelName; + + private static final String JSON_CHANNEL_SERVICE_ID_KEY = "channel_service_id"; + private static final String JSON_CHANNEL_URL_KEY = "channel_url"; + private static final String JSON_CHANNEL_NAME_KEY = "channel_name"; + + private ChannelTab() { + this(-1, "", ""); + } + + public ChannelTab(int channelServiceId, String channelUrl, String channelName) { + this.channelServiceId = channelServiceId; + this.channelUrl = channelUrl; + this.channelName = channelName; + } + + public ChannelTab(JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(Context context) { + return channelName; + } + + @DrawableRes + @Override + public int getTabIconRes(Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); + } + + @Override + public ChannelFragment getFragment() { + return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); + } + + @Override + protected void writeDataToJson(JsonSink writerSink) { + writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId) + .value(JSON_CHANNEL_URL_KEY, channelUrl) + .value(JSON_CHANNEL_NAME_KEY, channelName); + } + + @Override + protected void readDataFromJson(JsonObject jsonObject) { + channelServiceId = jsonObject.getInt(JSON_CHANNEL_SERVICE_ID_KEY, -1); + channelUrl = jsonObject.getString(JSON_CHANNEL_URL_KEY, ""); + channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, ""); + } + + public int getChannelServiceId() { + return channelServiceId; + } + + public String getChannelUrl() { + return channelUrl; + } + + public String getChannelName() { + return channelName; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java new file mode 100644 index 000000000..332e244c8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java @@ -0,0 +1,114 @@ +package org.schabi.newpipe.settings.tabs; + +import android.support.annotation.Nullable; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonStringWriter; +import com.grack.nanojson.JsonWriter; + +import org.schabi.newpipe.settings.tabs.Tab.Type; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.schabi.newpipe.extractor.ServiceList.YouTube; + +/** + * Class to get a JSON representation of a list of tabs, and the other way around. + */ +public class TabsJsonHelper { + private static final String JSON_TABS_ARRAY_KEY = "tabs"; + + protected static final List FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList( + new Tab.KioskTab(YouTube.getServiceId(), "Trending"), + Type.SUBSCRIPTIONS.getTab(), + Type.BOOKMARKS.getTab() + )); + + public static class InvalidJsonException extends Exception { + private InvalidJsonException() { + super(); + } + + private InvalidJsonException(String message) { + super(message); + } + + private InvalidJsonException(Throwable cause) { + super(cause); + } + } + + /** + * Try to reads the passed JSON and returns the list of tabs if no error were encountered. + *

+ * If the JSON is null or empty, or the list of tabs that it represents is empty, the + * {@link #FALLBACK_INITIAL_TABS_LIST fallback list} will be returned. + *

+ * Tabs with invalid ids (i.e. not in the {@link Tab.Type} enum) will be ignored. + * + * @param tabsJson a JSON string got from {@link #getJsonToSave(List)}. + * @return a list of {@link Tab tabs}. + * @throws InvalidJsonException if the JSON string is not valid + */ + public static List getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException { + if (tabsJson == null || tabsJson.isEmpty()) { + return FALLBACK_INITIAL_TABS_LIST; + } + + final List returnTabs = new ArrayList<>(); + + final JsonObject outerJsonObject; + try { + outerJsonObject = JsonParser.object().from(tabsJson); + final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY); + + if (tabsArray == null) { + throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY + "\" array"); + } + + for (Object o : tabsArray) { + if (!(o instanceof JsonObject)) continue; + + final Tab tab = Tab.from((JsonObject) o); + + if (tab != null) { + returnTabs.add(tab); + } + } + } catch (JsonParserException e) { + throw new InvalidJsonException(e); + } + + if (returnTabs.isEmpty()) { + return FALLBACK_INITIAL_TABS_LIST; + } + + return returnTabs; + } + + /** + * Get a JSON representation from a list of tabs. + * + * @param tabList a list of {@link Tab tabs}. + * @return a JSON string representing the list of tabs + */ + public static String getJsonToSave(@Nullable List tabList) { + final JsonStringWriter jsonWriter = JsonWriter.string(); + jsonWriter.object(); + + jsonWriter.array(JSON_TABS_ARRAY_KEY); + if (tabList != null) for (Tab tab : tabList) { + tab.writeJsonOn(jsonWriter); + } + jsonWriter.end(); + + jsonWriter.end(); + return jsonWriter.done(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java new file mode 100644 index 000000000..a7d8dffa4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java @@ -0,0 +1,93 @@ +package org.schabi.newpipe.settings.tabs; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import org.schabi.newpipe.R; + +import java.util.List; + +public class TabsManager { + private final SharedPreferences sharedPreferences; + private final String savedTabsKey; + private final Context context; + + public static TabsManager getManager(Context context) { + return new TabsManager(context); + } + + private TabsManager(Context context) { + this.context = context; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.savedTabsKey = context.getString(R.string.saved_tabs_key); + } + + public List getTabs() { + final String savedJson = sharedPreferences.getString(savedTabsKey, null); + try { + return TabsJsonHelper.getTabsFromJson(savedJson); + } catch (TabsJsonHelper.InvalidJsonException e) { + Toast.makeText(context, R.string.saved_tabs_invalid_json, Toast.LENGTH_SHORT).show(); + return getDefaultTabs(); + } + } + + public void saveTabs(List tabList) { + final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList); + sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply(); + } + + public void resetTabs() { + sharedPreferences.edit().remove(savedTabsKey).apply(); + } + + public List getDefaultTabs() { + return TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST; + } + + /*////////////////////////////////////////////////////////////////////////// + // Listener + //////////////////////////////////////////////////////////////////////////*/ + + public interface SavedTabsChangeListener { + void onTabsChanged(); + } + + private SavedTabsChangeListener savedTabsChangeListener; + private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; + + public void setSavedTabsListener(SavedTabsChangeListener listener) { + if (preferenceChangeListener != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); + } + savedTabsChangeListener = listener; + preferenceChangeListener = getPreferenceChangeListener(); + sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + } + + public void unsetSavedTabsListener() { + if (preferenceChangeListener != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); + } + preferenceChangeListener = null; + savedTabsChangeListener = null; + } + + private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() { + return (sharedPreferences, key) -> { + if (key.equals(savedTabsKey)) { + if (savedTabsChangeListener != null) savedTabsChangeListener.onTabsChanged(); + } + }; + } + +} + + + + + + + diff --git a/app/src/main/res/drawable/ic_blank_page_black_24dp.xml b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml new file mode 100644 index 000000000..e8c60a1a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_page_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_blank_page_white_24dp.xml b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml new file mode 100644 index 000000000..86a68484f --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_page_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml new file mode 100644 index 000000000..aa424c0d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml new file mode 100644 index 000000000..03a26f550 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_chose_tabs.xml b/app/src/main/res/layout/fragment_choose_tabs.xml similarity index 65% rename from app/src/main/res/layout/fragment_chose_tabs.xml rename to app/src/main/res/layout/fragment_choose_tabs.xml index 2bdf261f4..0097a409a 100644 --- a/app/src/main/res/layout/fragment_chose_tabs.xml +++ b/app/src/main/res/layout/fragment_choose_tabs.xml @@ -1,32 +1,32 @@ - - - + tools:listitem="@layout/list_choose_tabs"/> + android:clickable="true" + android:focusable="true" + app:backgroundTint="?attr/colorPrimary" + app:fabSize="auto" + app:srcCompat="?attr/ic_add"/> \ No newline at end of file diff --git a/app/src/main/res/layout/list_choose_tabs.xml b/app/src/main/res/layout/list_choose_tabs.xml new file mode 100644 index 000000000..e62cf24f1 --- /dev/null +++ b/app/src/main/res/layout/list_choose_tabs.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_choose_tabs_dialog.xml b/app/src/main/res/layout/list_choose_tabs_dialog.xml new file mode 100644 index 000000000..8c6574e6d --- /dev/null +++ b/app/src/main/res/layout/list_choose_tabs_dialog.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/viewholder_chose_tabs.xml b/app/src/main/res/layout/viewholder_chose_tabs.xml deleted file mode 100644 index 311d1441f..000000000 --- a/app/src/main/res/layout/viewholder_chose_tabs.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 731e18853..7b879fb4c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -36,6 +36,8 @@ + + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 02f065285..9b39fec26 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -8,6 +8,8 @@ service @string/youtube + saved_tabs_key + download_path download_path_audio @@ -143,22 +145,7 @@ enable_search_history enable_watch_history main_page_content - blank_page - feed_page - subscription_page_key - kiosk_page - channel_page - - @string/blank_page_key - @string/kiosk_page_key - @string/feed_page_key - @string/subscription_page_key - @string/channel_page_key - - main_page_selected_service - main_page_selected_channel_name - main_page_selected_channel_url - main_page_selectd_kiosk_id + import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eaeeb2685..00da99b9e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,7 +36,7 @@ Subscriptions Bookmarks New Tab - Chose Tab + Choose Tab What\'s New @@ -197,6 +197,9 @@ File name cannot be empty An error occurred: %1$s No streams available to download + Using default tabs, error while reading saved tabs + Restore defaults + Do you want to restore the defaults? Sorry, that should not have happened. @@ -363,19 +366,11 @@ Content of main page What tabs are shown on the main page Selection - Your tabs Blank Page Kiosk Page Subscription Page Feed Page Channel Page - - @string/blank_page_summary - @string/kiosk_page_summary - @string/feed_page_summary - @string/subscription_page_summary - @string/channel_page_summary - Select a channel No channel subscribed yet Select a kiosk diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1f7280e6f..87e19cede 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -52,6 +52,8 @@ @drawable/ic_save_black_24dp @drawable/ic_backup_black_24dp @drawable/ic_add_black_24dp + @drawable/ic_settings_backup_restore_black_24dp + @drawable/ic_blank_page_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -110,6 +112,8 @@ @drawable/ic_save_white_24dp @drawable/ic_backup_white_24dp @drawable/ic_add_white_24dp + @drawable/ic_settings_backup_restore_white_24dp + @drawable/ic_blank_page_white_24dp @color/dark_separator_color @color/dark_contrast_background_color diff --git a/app/src/main/res/xml/appearance_settings.xml b/app/src/main/res/xml/appearance_settings.xml index 8835e7705..1f711b510 100644 --- a/app/src/main/res/xml/appearance_settings.xml +++ b/app/src/main/res/xml/appearance_settings.xml @@ -28,7 +28,7 @@ android:summary="@string/caption_setting_description"/> diff --git a/app/src/test/java/org/schabi/newpipe/settings/tabs/TabTest.java b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabTest.java new file mode 100644 index 000000000..45c7c0fff --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabTest.java @@ -0,0 +1,20 @@ +package org.schabi.newpipe.settings.tabs; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +public class TabTest { + @Test + public void checkIdDuplication() { + final Set usedIds = new HashSet<>(); + + for (Tab.Type type : Tab.Type.values()) { + final boolean added = usedIds.add(type.getTabId()); + assertTrue("Id was already used: " + type.getTabId(), added); + } + } +} \ No newline at end of file diff --git a/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java new file mode 100644 index 000000000..c78b9c9b1 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java @@ -0,0 +1,120 @@ +package org.schabi.newpipe.settings.tabs; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.util.Objects.requireNonNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TabsJsonHelperTest { + private static final String JSON_TABS_ARRAY_KEY = "tabs"; + private static final String JSON_TAB_ID_KEY = "tab_id"; + + @Test + public void testEmptyAndNullRead() throws TabsJsonHelper.InvalidJsonException { + final String emptyTabsJson = "{\"" + JSON_TABS_ARRAY_KEY + "\":[]}"; + List items = TabsJsonHelper.getTabsFromJson(emptyTabsJson); + // Check if instance is the same + assertTrue(items == TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST); + + final String nullSource = null; + items = TabsJsonHelper.getTabsFromJson(nullSource); + assertTrue(items == TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST); + } + + @Test + public void testInvalidIdRead() throws TabsJsonHelper.InvalidJsonException { + final int blankTabId = Tab.Type.BLANK.getTabId(); + final String emptyTabsJson = "{\"" + JSON_TABS_ARRAY_KEY + "\":[" + + "{\"" + JSON_TAB_ID_KEY + "\":" + blankTabId + "}," + + "{\"" + JSON_TAB_ID_KEY + "\":" + 12345678 + "}" + + "]}"; + final List items = TabsJsonHelper.getTabsFromJson(emptyTabsJson); + + assertEquals("Should ignore the tab with invalid id", 1, items.size()); + assertEquals(blankTabId, items.get(0).getTabId()); + } + + @Test + public void testInvalidRead() { + final List invalidList = Arrays.asList( + "{\"notTabsArray\":[]}", + "{invalidJSON]}", + "{}" + ); + + for (String invalidContent : invalidList) { + try { + TabsJsonHelper.getTabsFromJson(invalidContent); + + fail("didn't throw exception"); + } catch (Exception e) { + boolean isExpectedException = e instanceof TabsJsonHelper.InvalidJsonException; + assertTrue("\"" + e.getClass().getSimpleName() + "\" is not the expected exception", isExpectedException); + } + } + } + + @Test + public void testEmptyAndNullSave() throws JsonParserException { + final List emptyList = Collections.emptyList(); + String returnedJson = TabsJsonHelper.getJsonToSave(emptyList); + assertTrue(isTabsArrayEmpty(returnedJson)); + + final List nullList = null; + returnedJson = TabsJsonHelper.getJsonToSave(nullList); + assertTrue(isTabsArrayEmpty(returnedJson)); + } + + private boolean isTabsArrayEmpty(String returnedJson) throws JsonParserException { + JsonObject jsonObject = JsonParser.object().from(returnedJson); + assertTrue(jsonObject.containsKey(JSON_TABS_ARRAY_KEY)); + return jsonObject.getArray(JSON_TABS_ARRAY_KEY).size() == 0; + } + + @Test + public void testSaveAndReading() throws JsonParserException { + // Saving + final Tab.BlankTab blankTab = new Tab.BlankTab(); + final Tab.SubscriptionsTab subscriptionsTab = new Tab.SubscriptionsTab(); + final Tab.ChannelTab channelTab = new Tab.ChannelTab(666, "https://example.org", "testName"); + final Tab.KioskTab kioskTab = new Tab.KioskTab(123, "trending_key"); + + final List tabs = Arrays.asList(blankTab, subscriptionsTab, channelTab, kioskTab); + String returnedJson = TabsJsonHelper.getJsonToSave(tabs); + + // Reading + final JsonObject jsonObject = JsonParser.object().from(returnedJson); + assertTrue(jsonObject.containsKey(JSON_TABS_ARRAY_KEY)); + final JsonArray tabsFromArray = jsonObject.getArray(JSON_TABS_ARRAY_KEY); + + assertEquals(tabs.size(), tabsFromArray.size()); + + final Tab.BlankTab blankTabFromReturnedJson = requireNonNull((Tab.BlankTab) Tab.from(((JsonObject) tabsFromArray.get(0)))); + assertEquals(blankTab.getTabId(), blankTabFromReturnedJson.getTabId()); + + final Tab.SubscriptionsTab subscriptionsTabFromReturnedJson = requireNonNull((Tab.SubscriptionsTab) Tab.from(((JsonObject) tabsFromArray.get(1)))); + assertEquals(subscriptionsTab.getTabId(), subscriptionsTabFromReturnedJson.getTabId()); + + final Tab.ChannelTab channelTabFromReturnedJson = requireNonNull((Tab.ChannelTab) Tab.from(((JsonObject) tabsFromArray.get(2)))); + assertEquals(channelTab.getTabId(), channelTabFromReturnedJson.getTabId()); + assertEquals(channelTab.getChannelServiceId(), channelTabFromReturnedJson.getChannelServiceId()); + assertEquals(channelTab.getChannelUrl(), channelTabFromReturnedJson.getChannelUrl()); + assertEquals(channelTab.getChannelName(), channelTabFromReturnedJson.getChannelName()); + + final Tab.KioskTab kioskTabFromReturnedJson = requireNonNull((Tab.KioskTab) Tab.from(((JsonObject) tabsFromArray.get(3)))); + assertEquals(kioskTab.getTabId(), kioskTabFromReturnedJson.getTabId()); + assertEquals(kioskTab.getKioskServiceId(), kioskTabFromReturnedJson.getKioskServiceId()); + assertEquals(kioskTab.getKioskId(), kioskTabFromReturnedJson.getKioskId()); + } +} \ No newline at end of file From 07256e2e34e7bd3ed6742afb2b4b5cbb504619bb Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Tue, 4 Sep 2018 23:54:17 -0300 Subject: [PATCH 2/7] Handle case where subscribers count is not available --- .../fragments/list/channel/ChannelFragment.java | 11 ++++++----- app/src/main/res/layout/channel_header.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 4df5982f7..9a52b8d12 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -33,15 +33,14 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.subscription.SubscriptionService; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.local.subscription.SubscriptionService; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -422,10 +421,12 @@ public class ChannelFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - if (result.getSubscriberCount() != -1) { + headerSubscribersTextView.setVisibility(View.VISIBLE); + if (result.getSubscriberCount() >= 0) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); - headerSubscribersTextView.setVisibility(View.VISIBLE); - } else headerSubscribersTextView.setVisibility(View.GONE); + } else { + headerSubscribersTextView.setText(R.string.subscribers_count_not_available); + } if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml index ca795d9db..0947cb307 100644 --- a/app/src/main/res/layout/channel_header.xml +++ b/app/src/main/res/layout/channel_header.xml @@ -61,7 +61,7 @@ android:layout_below="@+id/channel_title_view" android:ellipsize="end" android:gravity="left|center" - android:lines="1" + android:maxLines="2" android:textSize="@dimen/channel_subscribers_text_size" android:visibility="gone" tools:ignore="RtlHardcoded" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00da99b9e..0dc837ae8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -249,6 +249,7 @@ %s subscriber %s subscribers + Subscribers count not available No views From 9883a386984598012602ebc9635345080d19f2a9 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Tue, 4 Sep 2018 23:54:17 -0300 Subject: [PATCH 3/7] Fix registering of broadcast receiver --- .../org/schabi/newpipe/player/BasePlayer.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 06f2e9721..01a0614fa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -70,7 +70,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.SerializedCache; @@ -88,7 +87,6 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static org.schabi.newpipe.report.UserAction.PLAY_STREAM; /** * Base for the players, joining the common properties @@ -175,7 +173,6 @@ public abstract class BasePlayer implements }; this.intentFilter = new IntentFilter(); setupBroadcastReceiver(intentFilter); - context.registerReceiver(broadcastReceiver, intentFilter); this.recordManager = new HistoryRecordManager(context); @@ -212,6 +209,8 @@ public abstract class BasePlayer implements audioReactor = new AudioReactor(context, simpleExoPlayer); mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, new BasePlayerMediaSession(this)); + + registerBroadcastReceiver(); } public void initListeners() {} @@ -362,11 +361,17 @@ public abstract class BasePlayer implements } } - public void unregisterBroadcastReceiver() { + protected void registerBroadcastReceiver() { + // Try to unregister current first + unregisterBroadcastReceiver(); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + protected void unregisterBroadcastReceiver() { try { context.unregisterReceiver(broadcastReceiver); } catch (final IllegalArgumentException unregisteredException) { - Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException); + Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); } } From 6e75d41956eb2dc18763c967a471f8b913b05c34 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Tue, 4 Sep 2018 23:54:17 -0300 Subject: [PATCH 4/7] Use current volume as the start value in the volume gesture - Renamed some variables/classes to increase readability --- .../newpipe/player/MainVideoPlayer.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 41e7c305d..4e8398ff2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -460,7 +460,7 @@ public final class MainVideoPlayer extends AppCompatActivity public void initListeners() { super.initListeners(); - MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); + PlayerGestureListener listener = new PlayerGestureListener(); gestureDetector = new GestureDetector(context, listener); gestureDetector.setIsLongpressEnabled(false); getRootView().setOnTouchListener(listener); @@ -489,6 +489,8 @@ public final class MainVideoPlayer extends AppCompatActivity volumeProgressBar.setMax(maxGestureLength); brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); } }); } @@ -799,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity // Utils //////////////////////////////////////////////////////////////////////////*/ + private void setInitialGestureValues() { + if (getAudioReactor() != null) { + final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + @Override public void showControlsThenHide() { if (queueVisible) return; @@ -939,7 +948,7 @@ public final class MainVideoPlayer extends AppCompatActivity } } - private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { private boolean isMoving; @Override @@ -978,31 +987,30 @@ public final class MainVideoPlayer extends AppCompatActivity return super.onDown(e); } - private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); + private static final int MOVEMENT_THRESHOLD = 40; + private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); - private final int MOVEMENT_THRESHOLD = 40; - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { if (!isPlayerGestureEnabled) return false; //noinspection PointlessBooleanExpression if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + - ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + - ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); - if (!isMoving && ( - Math.abs(e2.getY() - e1.getY()) <= MOVEMENT_THRESHOLD - || Math.abs(distanceX) > Math.abs(distanceY) - ) || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) + final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; + if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) + || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { return false; + } isMoving = true; - if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { + if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) { playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); float currentProgressPercent = (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); From 612228bb73fd2a65e891517de665a9ef4c68e9a4 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Wed, 5 Sep 2018 07:29:15 -0300 Subject: [PATCH 5/7] Update extractor version - Handle case where subscribers count is not available - Fix NPE when a YouTube playlist is empty - Quick fix for the kiosks in SoundCloud --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 49d9386e5..8e6e3782c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:834382111b98e629' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:850670917fce' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.8.9' From 818ae039287d90d6294b291315fba3c5a78def66 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 7 Sep 2018 21:51:14 +0200 Subject: [PATCH 6/7] fix decrypt url and move on to v0.14.1 --- app/build.gradle | 6 ++-- .../metadata/android/en-US/changelogs/68.txt | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/68.txt diff --git a/app/build.gradle b/app/build.gradle index 8e6e3782c..b7cfff281 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 27 - versionCode 67 - versionName "0.14.0" + versionCode 68 + versionName "0.14.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:850670917fce' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:4469d1130799' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.8.9' diff --git a/fastlane/metadata/android/en-US/changelogs/68.txt b/fastlane/metadata/android/en-US/changelogs/68.txt new file mode 100644 index 000000000..238b1e0b1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/68.txt @@ -0,0 +1,31 @@ +# changes of v0.14.1 + +### Fixed +- Fixed failed to decrypt video url #1659 +- Fixed description link not extract well #1657 + +# changes of v0.14.0 + +### New +- New Drawer design #1461 +- New customizable front page #1461 + +### Improvements +- Reworked Gesture controls #1604 +- New way to close the popup player #1597 + +### Fixed +- Fix error when subscription count is not available. Closes #1649. + - Show "Subscriber count not available" in those cases +- Fix NPE when a YouTube playlist is empty +- Quick fix for the kiosks in SoundCloud +- Refactor and bugfix #1623 + - Fix Cyclic search result #1562 + - Fix Seek bar not statically lay outed + - Fix YT Premium video are not blocked correctly + - Fix Videos sometimes not loading (due to DASH parsing) + - Fix links in video description + - Show warning when someone tries to download to external sdcard + - fix nothing shown exception triggers report + - thumbnail not shown in background player for android 8.1 [see here](https://github.com/TeamNewPipe/NewPipe/issues/943) +- Fix registering of broadcast receiver. Closes #1641. From 05f8ee9747151f89bf75c22f28447321ab03b0f8 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 7 Sep 2018 22:23:32 +0200 Subject: [PATCH 7/7] fix channel links in description part 2 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b7cfff281..4b717cafc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:4469d1130799' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:66c3c3f45241d4b0c909' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.8.9'