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
This commit is contained in:
Mauricio Colli 2018-09-04 23:54:17 -03:00
parent c066ebd76f
commit 43674ae80a
No known key found for this signature in database
GPG Key ID: F200BFD6F29DDD85
26 changed files with 1493 additions and 606 deletions

View File

@ -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<String> 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<Tab> 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();
}
}

View File

@ -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();
}
}

View File

@ -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<String> 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<ChoseTabsFragment.SelectedTabsAdapter.TabViewHolder>{
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();
}
};
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -77,7 +77,8 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc
finish();
} else getSupportFragmentManager().popBackStack();
}
return true;
return super.onOptionsItemSelected(item);
}
@Override

View File

@ -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;
}
}
}

View File

@ -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<Tab> 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<ChooseTabListItem> 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<ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder> {
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);
}
}
};
}
}

View File

@ -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, "<no-id>");
}
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, "<no-id>");
}
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, "<no-url>", "<no-name>");
}
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, "<no-url>");
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
}
public int getChannelServiceId() {
return channelServiceId;
}
public String getChannelUrl() {
return channelUrl;
}
public String getChannelName() {
return channelName;
}
}
}

View File

@ -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<Tab> 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.
* <p>
* 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.
* <p>
* 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<Tab> getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException {
if (tabsJson == null || tabsJson.isEmpty()) {
return FALLBACK_INITIAL_TABS_LIST;
}
final List<Tab> 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<Tab> 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();
}
}

View File

@ -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<Tab> 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<Tab> tabList) {
final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList);
sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply();
}
public void resetTabs() {
sharedPreferences.edit().remove(savedTabsKey).apply();
}
public List<Tab> 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();
}
};
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,5c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,5c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/>
</vector>

View File

@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/relLay"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/usedTabs"
android:id="@+id/selectedTabs"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:paddingBottom="0dp"
android:paddingTop="0dp" >
</android.support.v7.widget.RecyclerView>
tools:listitem="@layout/list_choose_tabs"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/floatingActionButton"
android:id="@+id/addTabsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:clickable="true"
android:layout_alignParentRight="true"
android:layout_marginRight="16dp"
android:focusable="true" />
android:clickable="true"
android:focusable="true"
app:backgroundTint="?attr/colorPrimary"
app:fabSize="auto"
app:srcCompat="?attr/ic_add"/>
</RelativeLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="3dp"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
app:cardCornerRadius="5dp"
app:cardElevation="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/tabIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
tools:ignore="ContentDescription,RtlHardcoded"
tools:src="?attr/ic_hot"/>
<TextView
android:id="@+id/tabName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginBottom="6dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="6dp"
android:layout_toLeftOf="@+id/handle"
android:layout_toRightOf="@+id/tabIcon"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?textAppearanceListItem"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:paddingBottom="12dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:src="?attr/drag_handle"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/tabIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="?dialogPreferredPadding"
tools:ignore="ContentDescription,RtlHardcoded"
tools:src="?attr/ic_hot"/>
<TextView
android:id="@+id/tabName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="?dialogPreferredPadding"
android:layout_marginRight="?dialogPreferredPadding"
android:layout_toRightOf="@+id/tabIcon"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?textAppearanceListItem"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet"/>
</RelativeLayout>

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="3dp"
android:orientation="horizontal"
android:paddingBottom="4dp"
android:paddingTop="4dp"
app:cardCornerRadius="5dp"
app:cardElevation="4dp">
<ImageView
android:id="@+id/handle"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical|end"
android:layout_marginRight="10dp" />
<TextView
android:id="@+id/tabName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:paddingBottom="9dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingStart="3dp"
android:paddingTop="9dp"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
</android.support.v7.widget.CardView>

View File

@ -36,6 +36,8 @@
<attr name="ic_save" format="reference"/>
<attr name="ic_backup" format="reference"/>
<attr name="ic_add" format="reference"/>
<attr name="ic_restore_defaults" format="reference"/>
<attr name="ic_blank_page" format="reference"/>
<!-- Can't refer to colors directly in drawable's xml-->
<attr name="toolbar_shadow_drawable" format="reference"/>

View File

@ -8,6 +8,8 @@
<string name="current_service_key" translatable="false">service</string>
<string name="default_service_value" translatable="false">@string/youtube</string>
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
<!-- Key values -->
<string name="download_path_key" translatable="false">download_path</string>
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
@ -143,22 +145,7 @@
<string name="enable_search_history_key" translatable="false">enable_search_history</string>
<string name="enable_watch_history_key" translatable="false">enable_watch_history</string>
<string name="main_page_content_key" translatable="false">main_page_content</string>
<string name="blank_page_key" translatable="false">blank_page</string>
<string name="feed_page_key" translatable="false">feed_page</string>
<string name="subscription_page_key" translatable="false">subscription_page_key</string>
<string name="kiosk_page_key" translatable="false">kiosk_page</string>
<string name="channel_page_key" translatable="false">channel_page</string>
<string-array name="main_page_content_pages" translatable="false">
<item>@string/blank_page_key</item>
<item>@string/kiosk_page_key</item>
<item>@string/feed_page_key</item>
<item>@string/subscription_page_key</item>
<item>@string/channel_page_key</item>
</string-array>
<string name="main_page_selected_service" translatable="false">main_page_selected_service</string>
<string name="main_page_selected_channel_name" translatable="false">main_page_selected_channel_name</string>
<string name="main_page_selected_channel_url" translatable="false">main_page_selected_channel_url</string>
<string name="main_page_selectd_kiosk_id" translatable="false">main_page_selectd_kiosk_id</string>
<string name="import_data">import_data</string>
<string name="export_data">export_data</string>

View File

@ -36,7 +36,7 @@
<string name="tab_subscriptions">Subscriptions</string>
<string name="tab_bookmarks">Bookmarks</string>
<string name="tab_new">New Tab</string>
<string name="tab_chose">Chose Tab</string>
<string name="tab_choose">Choose Tab</string>
<string name="fragment_whats_new">What\'s New</string>
@ -197,6 +197,9 @@
<string name="file_name_empty_error">File name cannot be empty</string>
<string name="error_occurred_detail">An error occurred: %1$s</string>
<string name="no_streams_available_download">No streams available to download</string>
<string name="saved_tabs_invalid_json">Using default tabs, error while reading saved tabs</string>
<string name="restore_defaults">Restore defaults</string>
<string name="restore_defaults_confirmation">Do you want to restore the defaults?</string>
<!-- error activity -->
<string name="sorry_string">Sorry, that should not have happened.</string>
@ -363,19 +366,11 @@
<string name="main_page_content">Content of main page</string>
<string name="main_page_content_summary">What tabs are shown on the main page</string>
<string name="selection">Selection</string>
<string name="chosenTabs">Your tabs</string>
<string name="blank_page_summary">Blank Page</string>
<string name="kiosk_page_summary">Kiosk Page</string>
<string name="subscription_page_summary">Subscription Page</string>
<string name="feed_page_summary">Feed Page</string>
<string name="channel_page_summary">Channel Page</string>
<string-array name="main_page_content_names">
<item>@string/blank_page_summary</item>
<item>@string/kiosk_page_summary</item>
<item>@string/feed_page_summary</item>
<item>@string/subscription_page_summary</item>
<item>@string/channel_page_summary</item>
</string-array>
<string name="select_a_channel">Select a channel</string>
<string name="no_channel_subscribed_yet">No channel subscribed yet</string>
<string name="select_a_kiosk">Select a kiosk</string>

View File

@ -52,6 +52,8 @@
<item name="ic_save">@drawable/ic_save_black_24dp</item>
<item name="ic_backup">@drawable/ic_backup_black_24dp</item>
<item name="ic_add">@drawable/ic_add_black_24dp</item>
<item name="ic_restore_defaults">@drawable/ic_settings_backup_restore_black_24dp</item>
<item name="ic_blank_page">@drawable/ic_blank_page_black_24dp</item>
<item name="separator_color">@color/light_separator_color</item>
<item name="contrast_background_color">@color/light_contrast_background_color</item>
@ -110,6 +112,8 @@
<item name="ic_save">@drawable/ic_save_white_24dp</item>
<item name="ic_backup">@drawable/ic_backup_white_24dp</item>
<item name="ic_add">@drawable/ic_add_white_24dp</item>
<item name="ic_restore_defaults">@drawable/ic_settings_backup_restore_white_24dp</item>
<item name="ic_blank_page">@drawable/ic_blank_page_white_24dp</item>
<item name="separator_color">@color/dark_separator_color</item>
<item name="contrast_background_color">@color/dark_contrast_background_color</item>

View File

@ -28,7 +28,7 @@
android:summary="@string/caption_setting_description"/>
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.ChoseTabsFragment"
android:fragment="org.schabi.newpipe.settings.tabs.ChooseTabsFragment"
android:summary="@string/main_page_content_summary"
android:key="@string/main_page_content_key"
android:title="@string/main_page_content"/>

View File

@ -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<Integer> 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);
}
}
}

View File

@ -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<Tab> 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<Tab> 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<String> 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<Tab> emptyList = Collections.emptyList();
String returnedJson = TabsJsonHelper.getJsonToSave(emptyList);
assertTrue(isTabsArrayEmpty(returnedJson));
final List<Tab> 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<Tab> 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());
}
}