From dfc27b2480ae8b267ca7882ad001230619cb4534 Mon Sep 17 00:00:00 2001
From: Roy Yosef <royosef10@gmail.com>
Date: Thu, 30 Apr 2020 23:52:47 +0300
Subject: [PATCH 1/4] Add playlist tab to main page

Add bookmarked playlist as tab in the main page (by Settings > Content > Content of main page)
---
 .../local/bookmark/BookmarkFragment.java      |  30 +--
 .../settings/SelectPlaylistFragment.java      | 240 ++++++++++++++++++
 .../settings/tabs/ChooseTabsFragment.java     |  30 +++
 .../org/schabi/newpipe/settings/tabs/Tab.java | 141 +++++++++-
 .../newpipe/util/PlaylistItemsUtils.java      |  38 +++
 .../res/layout/select_playlist_fragment.xml   |  43 ++++
 app/src/main/res/values/strings.xml           |   3 +
 7 files changed, 495 insertions(+), 30 deletions(-)
 create mode 100644 app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
 create mode 100644 app/src/main/res/layout/select_playlist_fragment.xml

diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
index d99a05976..7485a73ef 100644
--- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
@@ -29,9 +29,8 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
 import org.schabi.newpipe.report.UserAction;
 import org.schabi.newpipe.util.NavigationHelper;
 import org.schabi.newpipe.util.OnClickGesture;
+import org.schabi.newpipe.util.PlaylistItemsUtils;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import icepick.State;
@@ -54,31 +53,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
     // Fragment LifeCycle - Creation
     ///////////////////////////////////////////////////////////////////////////
 
-    private static List<PlaylistLocalItem> merge(
-            final List<PlaylistMetadataEntry> localPlaylists,
-            final List<PlaylistRemoteEntity> remotePlaylists) {
-        List<PlaylistLocalItem> items = new ArrayList<>(
-                localPlaylists.size() + remotePlaylists.size());
-        items.addAll(localPlaylists);
-        items.addAll(remotePlaylists);
-
-        Collections.sort(items, (left, right) -> {
-            String on1 = left.getOrderingName();
-            String on2 = right.getOrderingName();
-            if (on1 == null && on2 == null) {
-                return 0;
-            } else if (on1 != null && on2 == null) {
-                return -1;
-            } else if (on1 == null && on2 != null) {
-                return 1;
-            } else {
-                return on1.compareToIgnoreCase(on2);
-            }
-        });
-
-        return items;
-    }
-
     @Override
     public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -164,7 +138,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
         super.startLoading(forceLoad);
 
         Flowable.combineLatest(localPlaylistManager.getPlaylists(),
-                remotePlaylistManager.getPlaylists(), BookmarkFragment::merge)
+                remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge)
                 .onBackpressureLatest()
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(getPlaylistsSubscriber());
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
new file mode 100644
index 000000000..dc0b173ed
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
@@ -0,0 +1,240 @@
+package org.schabi.newpipe.settings;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.ImageLoader;
+
+import org.schabi.newpipe.NewPipeDatabase;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.AppDatabase;
+import org.schabi.newpipe.database.LocalItem;
+import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
+import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
+import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
+import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
+import org.schabi.newpipe.report.ErrorActivity;
+import org.schabi.newpipe.report.UserAction;
+import org.schabi.newpipe.util.PlaylistItemsUtils;
+
+import java.util.List;
+import java.util.Vector;
+
+import io.reactivex.Flowable;
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+public class SelectPlaylistFragment extends DialogFragment {
+    /**
+     * This contains the base display options for images.
+     */
+    private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
+            = new DisplayImageOptions.Builder().cacheInMemory(true).build();
+
+    private final ImageLoader imageLoader = ImageLoader.getInstance();
+
+    private OnSelectedLisener onSelectedLisener = null;
+    private OnCancelListener onCancelListener = null;
+
+    private ProgressBar progressBar;
+    private TextView emptyView;
+    private RecyclerView recyclerView;
+
+    private List<PlaylistLocalItem> playlists = new Vector<>();
+
+    public void setOnSelectedLisener(final OnSelectedLisener listener) {
+        onSelectedLisener = listener;
+    }
+
+    public void setOnCancelListener(final OnCancelListener listener) {
+        onCancelListener = listener;
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Init
+    //////////////////////////////////////////////////////////////////////////*/
+
+    @Override
+    public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
+                             final Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.select_playlist_fragment, container, false);
+        recyclerView = v.findViewById(R.id.items_list);
+        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
+        recyclerView.setAdapter(playlistAdapter);
+
+        progressBar = v.findViewById(R.id.progressBar);
+        emptyView = v.findViewById(R.id.empty_state_view);
+        progressBar.setVisibility(View.VISIBLE);
+        recyclerView.setVisibility(View.GONE);
+        emptyView.setVisibility(View.GONE);
+
+        final AppDatabase database = NewPipeDatabase.getInstance(this.getContext());
+        LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
+        RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
+
+        Flowable.combineLatest(localPlaylistManager.getPlaylists(),
+                remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge)
+                .toObservable()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(getPlaylistsObserver());
+
+        return v;
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Handle actions
+    //////////////////////////////////////////////////////////////////////////*/
+
+    @Override
+    public void onCancel(final DialogInterface dialogInterface) {
+        super.onCancel(dialogInterface);
+        if (onCancelListener != null) {
+            onCancelListener.onCancel();
+        }
+    }
+
+    private void clickedItem(final int position) {
+        if (onSelectedLisener != null) {
+            LocalItem selectedItem = playlists.get(position);
+
+            if (selectedItem instanceof PlaylistMetadataEntry) {
+                final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
+                onSelectedLisener
+                        .onLocalPlaylistSelected(entry.uid, entry.name);
+
+            } else if (selectedItem instanceof PlaylistRemoteEntity) {
+                final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
+                onSelectedLisener.onRemotePlaylistSelected(
+                        entry.getServiceId(), entry.getUrl(), entry.getName());
+            }
+        }
+        dismiss();
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Item handling
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private void displayPlaylists(final List<PlaylistLocalItem> newPlaylists) {
+        this.playlists = newPlaylists;
+        progressBar.setVisibility(View.GONE);
+        if (newPlaylists.isEmpty()) {
+            emptyView.setVisibility(View.VISIBLE);
+            return;
+        }
+        recyclerView.setVisibility(View.VISIBLE);
+
+    }
+
+    private Observer<List<PlaylistLocalItem>> getPlaylistsObserver() {
+        return new Observer<List<PlaylistLocalItem>>() {
+            @Override
+            public void onSubscribe(final Disposable d) { }
+
+            @Override
+            public void onNext(final List<PlaylistLocalItem> newPlaylists) {
+                displayPlaylists(newPlaylists);
+            }
+
+            @Override
+            public void onError(final Throwable exception) {
+                SelectPlaylistFragment.this.onError(exception);
+            }
+
+            @Override
+            public void onComplete() { }
+        };
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Error
+    //////////////////////////////////////////////////////////////////////////*/
+
+    protected void onError(final Throwable e) {
+        final Activity activity = getActivity();
+        ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo
+                .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Interfaces
+    //////////////////////////////////////////////////////////////////////////*/
+
+    public interface OnSelectedLisener {
+        void onLocalPlaylistSelected(long id, String name);
+        void onRemotePlaylistSelected(int serviceId, String url, String name);
+    }
+
+    public interface OnCancelListener {
+        void onCancel();
+    }
+
+    private class SelectPlaylistAdapter
+            extends RecyclerView.Adapter<SelectPlaylistAdapter.SelectPlaylistItemHolder> {
+        @Override
+        public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent,
+                                                          final int viewType) {
+            View item = LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.list_playlist_mini_item, parent, false);
+            return new SelectPlaylistItemHolder(item);
+        }
+
+        @Override
+        public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) {
+            PlaylistLocalItem selectedItem = playlists.get(position);
+
+            if (selectedItem instanceof PlaylistMetadataEntry) {
+                final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
+
+                holder.titleView.setText(entry.name);
+                holder.view.setOnClickListener(view -> clickedItem(position));
+                imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView,
+                        DISPLAY_IMAGE_OPTIONS);
+
+            } else if (selectedItem instanceof PlaylistRemoteEntity) {
+                final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
+
+                holder.titleView.setText(entry.getName());
+                holder.view.setOnClickListener(view -> clickedItem(position));
+                imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView,
+                        DISPLAY_IMAGE_OPTIONS);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return playlists.size();
+        }
+
+        public class SelectPlaylistItemHolder extends RecyclerView.ViewHolder {
+            public final View view;
+            final ImageView thumbnailView;
+            final TextView titleView;
+
+            SelectPlaylistItemHolder(final View v) {
+                super(v);
+                this.view = v;
+                thumbnailView = v.findViewById(R.id.itemThumbnailView);
+                titleView = v.findViewById(R.id.itemTitleView);
+            }
+        }
+    }
+}
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
index 6ebfbd73c..ad8ee043b 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
@@ -34,6 +34,7 @@ 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.SelectPlaylistFragment;
 import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem;
 import org.schabi.newpipe.util.ThemeHelper;
 
@@ -211,6 +212,23 @@ public class ChooseTabsFragment extends Fragment {
                         addTab(new Tab.ChannelTab(serviceId, url, name)));
                 selectChannelFragment.show(requireFragmentManager(), "select_channel");
                 return;
+            case PLAYLIST:
+                SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment();
+                selectPlaylistFragment.setOnSelectedLisener(
+                        new SelectPlaylistFragment.OnSelectedLisener() {
+                            @Override
+                            public void onLocalPlaylistSelected(final long id, final String name) {
+                                addTab(new Tab.PlaylistTab(id, name));
+                            }
+
+                            @Override
+                            public void onRemotePlaylistSelected(
+                                    final int serviceId, final String url, final String name) {
+                                addTab(new Tab.PlaylistTab(serviceId, url, name));
+                            }
+                        });
+                selectPlaylistFragment.show(requireFragmentManager(), "select_playlist");
+                return;
             default:
                 addTab(type.getTab());
                 break;
@@ -248,6 +266,11 @@ public class ChooseTabsFragment extends Fragment {
                                         R.attr.ic_kiosk_hot)));
                     }
                     break;
+                case PLAYLIST:
+                    returnList.add(new ChooseTabListItem(tab.getTabId(),
+                            getString(R.string.playlist_page_summary),
+                            tab.getTabIconRes(context)));
+                    break;
                 default:
                     if (!tabList.contains(tab)) {
                         returnList.add(new ChooseTabListItem(context, tab));
@@ -393,6 +416,13 @@ public class ChooseTabsFragment extends Fragment {
                         tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab)
                                 .getChannelServiceId()) + "/" + tab.getTabName(requireContext());
                         break;
+                    case PLAYLIST:
+                        int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId();
+                        String serviceName = serviceId == -1
+                                ? getString(R.string.local)
+                                : NewPipe.getNameOfService(serviceId);
+                        tabName = serviceName + "/" + tab.getTabName(requireContext());
+                        break;
                     default:
                         tabName = tab.getTabName(requireContext());
                         break;
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
index d06b4b14e..28a4e2723 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
@@ -11,6 +11,7 @@ import com.grack.nanojson.JsonObject;
 import com.grack.nanojson.JsonSink;
 
 import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.LocalItem.LocalItemType;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -18,9 +19,11 @@ import org.schabi.newpipe.fragments.BlankFragment;
 import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
 import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
 import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
+import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
 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.playlist.LocalPlaylistFragment;
 import org.schabi.newpipe.local.subscription.SubscriptionFragment;
 import org.schabi.newpipe.report.ErrorActivity;
 import org.schabi.newpipe.report.UserAction;
@@ -33,7 +36,8 @@ import java.util.Objects;
 public abstract class Tab {
     private static final String JSON_TAB_ID_KEY = "tab_id";
 
-    Tab() { }
+    Tab() {
+    }
 
     Tab(@NonNull final JsonObject jsonObject) {
         readDataFromJson(jsonObject);
@@ -83,6 +87,8 @@ public abstract class Tab {
                     return new KioskTab(jsonObject);
                 case CHANNEL:
                     return new ChannelTab(jsonObject);
+                case PLAYLIST:
+                    return new PlaylistTab(jsonObject);
             }
         }
 
@@ -147,7 +153,8 @@ public abstract class Tab {
         BOOKMARKS(new BookmarksTab()),
         HISTORY(new HistoryTab()),
         KIOSK(new KioskTab()),
-        CHANNEL(new ChannelTab());
+        CHANNEL(new ChannelTab()),
+        PLAYLIST(new PlaylistTab());
 
         private Tab tab;
 
@@ -482,4 +489,134 @@ public abstract class Tab {
             return kioskId;
         }
     }
+
+    public static class PlaylistTab extends Tab {
+        public static final int ID = 8;
+        private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id";
+        private static final String JSON_PLAYLIST_URL_KEY = "playlist_url";
+        private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name";
+        private static final String JSON_PLAYLIST_ID_KEY = "playlist_id";
+        private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type";
+        private int playlistServiceId;
+        private String playlistUrl;
+        private String playlistName;
+        private long playlistId;
+        private LocalItemType playlistType;
+
+        private PlaylistTab() {
+            this.playlistName = "<no-name>";
+            this.playlistId = -1;
+            this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM;
+            this.playlistServiceId = -1;
+            this.playlistUrl = "<no-url>";
+        }
+
+        public PlaylistTab(final long playlistId, final String playlistName) {
+            this.playlistName = playlistName;
+            this.playlistId = playlistId;
+            this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM;
+            this.playlistServiceId = -1;
+            this.playlistUrl = "<no-url>";
+        }
+
+        public PlaylistTab(final int playlistServiceId, final String playlistUrl,
+                           final String playlistName) {
+            this.playlistServiceId = playlistServiceId;
+            this.playlistUrl = playlistUrl;
+            this.playlistName = playlistName;
+            this.playlistType = LocalItemType.PLAYLIST_REMOTE_ITEM;
+            this.playlistId = -1;
+        }
+
+        public PlaylistTab(final JsonObject jsonObject) {
+            super(jsonObject);
+        }
+
+        @Override
+        public int getTabId() {
+            return ID;
+        }
+
+        @Override
+        public String getTabName(final Context context) {
+            return playlistName;
+        }
+
+        @DrawableRes
+        @Override
+        public int getTabIconRes(final Context context) {
+            return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_list);
+        }
+
+        @Override
+        public Fragment getFragment(final Context context) {
+            if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) {
+                return LocalPlaylistFragment.getInstance(playlistId,
+                        playlistName == null ? "" : playlistName);
+
+            } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM
+                return PlaylistFragment.getInstance(playlistServiceId, playlistUrl,
+                        playlistName == null ? "" : playlistName);
+            }
+        }
+
+        @Override
+        protected void writeDataToJson(final JsonSink writerSink) {
+            writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId)
+                    .value(JSON_PLAYLIST_URL_KEY, playlistUrl)
+                    .value(JSON_PLAYLIST_NAME_KEY, playlistName)
+                    .value(JSON_PLAYLIST_ID_KEY, playlistId)
+                    .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString());
+        }
+
+        @Override
+        protected void readDataFromJson(final JsonObject jsonObject) {
+            playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1);
+            playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, "<no-url>");
+            playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, "<no-name>");
+            playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1);
+            playlistType = LocalItemType.valueOf(
+                    jsonObject.getString(JSON_PLAYLIST_TYPE_KEY,
+                            LocalItemType.PLAYLIST_LOCAL_ITEM.toString())
+            );
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            boolean baseEqual = super.equals(obj)
+                    && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType)
+                    && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName);
+
+            if (!baseEqual) {
+                return false;
+            }
+
+            boolean localPlaylistEquals = playlistId == ((PlaylistTab) obj).playlistId;
+            boolean remotePlaylistEquals =
+                    playlistServiceId == ((PlaylistTab) obj).playlistServiceId
+                            && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl);
+
+            return localPlaylistEquals || remotePlaylistEquals;
+        }
+
+        public int getPlaylistServiceId() {
+            return playlistServiceId;
+        }
+
+        public String getPlaylistUrl() {
+            return playlistUrl;
+        }
+
+        public String getPlaylistName() {
+            return playlistName;
+        }
+
+        public long getPlaylistId() {
+            return playlistId;
+        }
+
+        public LocalItemType getPlaylistType() {
+            return playlistType;
+        }
+    }
 }
diff --git a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
new file mode 100644
index 000000000..230be4d28
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
@@ -0,0 +1,38 @@
+package org.schabi.newpipe.util;
+
+import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
+import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class PlaylistItemsUtils {
+    private PlaylistItemsUtils() { }
+
+    public static List<PlaylistLocalItem> merge(
+            final List<PlaylistMetadataEntry> localPlaylists,
+            final List<PlaylistRemoteEntity> remotePlaylists) {
+        List<PlaylistLocalItem> items = new ArrayList<>(
+                localPlaylists.size() + remotePlaylists.size());
+        items.addAll(localPlaylists);
+        items.addAll(remotePlaylists);
+
+        Collections.sort(items, (left, right) -> {
+            String on1 = left.getOrderingName();
+            String on2 = right.getOrderingName();
+            if (on1 == null && on2 == null) {
+                return 0;
+            } else if (on1 != null && on2 == null) {
+                return -1;
+            } else if (on1 == null && on2 != null) {
+                return 1;
+            } else {
+                return on1.compareToIgnoreCase(on2);
+            }
+        });
+
+        return items;
+    }
+}
diff --git a/app/src/main/res/layout/select_playlist_fragment.xml b/app/src/main/res/layout/select_playlist_fragment.xml
new file mode 100644
index 000000000..14462662a
--- /dev/null
+++ b/app/src/main/res/layout/select_playlist_fragment.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="13dp">
+
+    <TextView
+        android:id="@+id/titleTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10dp"
+        android:layout_marginLeft="10dp"
+        android:layout_marginTop="5dp"
+        android:layout_marginEnd="5dp"
+        android:layout_marginRight="5dp"
+        android:layout_marginBottom="10dp"
+        android:text="@string/select_a_playlist"
+        android:textAppearance="?android:attr/textAppearanceLarge" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/items_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:listitem="@layout/list_playlist_mini_item"></androidx.recyclerview.widget.RecyclerView>
+
+
+    <TextView
+        android:id="@+id/empty_state_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="10dp"
+        android:text="@string/no_playlist_bookmarked_yet"
+        android:textAppearance="?android:attr/textAppearanceListItem" />
+
+    <ProgressBar
+        android:id="@+id/progressBar"
+        style="?android:attr/progressBarStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="5dp"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a4dfd255d..79b86d2ad 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -399,6 +399,8 @@
     <string name="channel_page_summary">Channel Page</string>
     <string name="select_a_channel">Select a channel</string>
     <string name="no_channel_subscribed_yet">No channel subscriptions yet</string>
+    <string name="select_a_playlist">Select a playlist</string>
+    <string name="no_playlist_bookmarked_yet">No playlists bookmarks yet</string>
     <string name="select_a_kiosk">Select a kiosk</string>
     <string name="export_complete_toast">Exported</string>
     <string name="import_complete_toast">Imported</string>
@@ -651,4 +653,5 @@
     <string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
     <string name="channel_created_by">Created by %s</string>
     <string name="video_detail_by">By %s</string>
+    <string name="playlist_page_summary">Playlist Page</string>
 </resources>

From 13c0fdef082e2205b1e7345e856ecdde23d664a0 Mon Sep 17 00:00:00 2001
From: Roy Yosef <royosef10@gmail.com>
Date: Sun, 3 May 2020 00:14:31 +0300
Subject: [PATCH 2/4] Final declarations, naming & redundant code

* add final declarations where missing
* fix typo "onSelectedLisener" to "onSelectedListener"
* rename "baseEqual" to "baseEquals"
* replace getPlaylistsObserver code with functions pointers
* remove duplicate code in constructors
* remove useless null checks
---
 .../settings/SelectChannelFragment.java       | 12 ++--
 .../newpipe/settings/SelectKioskFragment.java | 12 ++--
 .../settings/SelectPlaylistFragment.java      | 70 ++++++++-----------
 .../settings/tabs/ChooseTabsFragment.java     | 22 +++---
 .../org/schabi/newpipe/settings/tabs/Tab.java | 22 +++---
 .../newpipe/util/PlaylistItemsUtils.java      |  2 +-
 .../res/layout/select_playlist_fragment.xml   |  6 +-
 7 files changed, 64 insertions(+), 82 deletions(-)

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 5b452430b..df529fee0 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
@@ -64,7 +64,7 @@ public class SelectChannelFragment extends DialogFragment {
 
     private final ImageLoader imageLoader = ImageLoader.getInstance();
 
-    private OnSelectedLisener onSelectedLisener = null;
+    private OnSelectedListener onSelectedListener = null;
     private OnCancelListener onCancelListener = null;
 
     private ProgressBar progressBar;
@@ -73,8 +73,8 @@ public class SelectChannelFragment extends DialogFragment {
 
     private List<SubscriptionEntity> subscriptions = new Vector<>();
 
-    public void setOnSelectedLisener(final OnSelectedLisener listener) {
-        onSelectedLisener = listener;
+    public void setOnSelectedListener(final OnSelectedListener listener) {
+        onSelectedListener = listener;
     }
 
     public void setOnCancelListener(final OnCancelListener listener) {
@@ -129,9 +129,9 @@ public class SelectChannelFragment extends DialogFragment {
     }
 
     private void clickedItem(final int position) {
-        if (onSelectedLisener != null) {
+        if (onSelectedListener != null) {
             SubscriptionEntity entry = subscriptions.get(position);
-            onSelectedLisener
+            onSelectedListener
                     .onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName());
         }
         dismiss();
@@ -186,7 +186,7 @@ public class SelectChannelFragment extends DialogFragment {
     // Interfaces
     //////////////////////////////////////////////////////////////////////////*/
 
-    public interface OnSelectedLisener {
+    public interface OnSelectedListener {
         void onChannelSelected(int serviceId, String url, String name);
     }
 
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 4df70ccec..13d34dec8 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java
@@ -52,11 +52,11 @@ public class SelectKioskFragment extends DialogFragment {
     private RecyclerView recyclerView = null;
     private SelectKioskAdapter selectKioskAdapter = null;
 
-    private OnSelectedLisener onSelectedLisener = null;
+    private OnSelectedListener onSelectedListener = null;
     private OnCancelListener onCancelListener = null;
 
-    public void setOnSelectedLisener(final OnSelectedLisener listener) {
-        onSelectedLisener = listener;
+    public void setOnSelectedListener(final OnSelectedListener listener) {
+        onSelectedListener = listener;
     }
 
     public void setOnCancelListener(final OnCancelListener listener) {
@@ -102,8 +102,8 @@ public class SelectKioskFragment extends DialogFragment {
     }
 
     private void clickedItem(final SelectKioskAdapter.Entry entry) {
-        if (onSelectedLisener != null) {
-            onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
+        if (onSelectedListener != null) {
+            onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
         }
         dismiss();
     }
@@ -122,7 +122,7 @@ public class SelectKioskFragment extends DialogFragment {
     // Interfaces
     //////////////////////////////////////////////////////////////////////////*/
 
-    public interface OnSelectedLisener {
+    public interface OnSelectedListener {
         void onKioskSelected(int serviceId, String kioskId, String kioskName);
     }
 
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
index dc0b173ed..795ced5fa 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
@@ -35,10 +35,7 @@ import java.util.List;
 import java.util.Vector;
 
 import io.reactivex.Flowable;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
 
 public class SelectPlaylistFragment extends DialogFragment {
     /**
@@ -49,17 +46,18 @@ public class SelectPlaylistFragment extends DialogFragment {
 
     private final ImageLoader imageLoader = ImageLoader.getInstance();
 
-    private OnSelectedLisener onSelectedLisener = null;
+    private OnSelectedListener onSelectedListener = null;
     private OnCancelListener onCancelListener = null;
 
     private ProgressBar progressBar;
     private TextView emptyView;
     private RecyclerView recyclerView;
+    private Disposable playlistsSubscriber;
 
     private List<PlaylistLocalItem> playlists = new Vector<>();
 
-    public void setOnSelectedLisener(final OnSelectedLisener listener) {
-        onSelectedLisener = listener;
+    public void setOnSelectedListener(final OnSelectedListener listener) {
+        onSelectedListener = listener;
     }
 
     public void setOnCancelListener(final OnCancelListener listener) {
@@ -67,13 +65,14 @@ public class SelectPlaylistFragment extends DialogFragment {
     }
 
     /*//////////////////////////////////////////////////////////////////////////
-    // Init
+    // Fragment's Lifecycle
     //////////////////////////////////////////////////////////////////////////*/
 
     @Override
     public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
                              final Bundle savedInstanceState) {
-        View v = inflater.inflate(R.layout.select_playlist_fragment, container, false);
+        final View v =
+                inflater.inflate(R.layout.select_playlist_fragment, container, false);
         recyclerView = v.findViewById(R.id.items_list);
         recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
         SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
@@ -86,19 +85,26 @@ public class SelectPlaylistFragment extends DialogFragment {
         emptyView.setVisibility(View.GONE);
 
         final AppDatabase database = NewPipeDatabase.getInstance(this.getContext());
-        LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
-        RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
+        final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
+        final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
 
-        Flowable.combineLatest(localPlaylistManager.getPlaylists(),
+        playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
                 remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge)
-                .toObservable()
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(getPlaylistsObserver());
+                .subscribe(this::displayPlaylists, this::onError);
 
         return v;
     }
 
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (playlistsSubscriber != null) {
+            playlistsSubscriber.dispose();
+            playlistsSubscriber = null;
+        }
+    }
+
     /*//////////////////////////////////////////////////////////////////////////
     // Handle actions
     //////////////////////////////////////////////////////////////////////////*/
@@ -112,17 +118,17 @@ public class SelectPlaylistFragment extends DialogFragment {
     }
 
     private void clickedItem(final int position) {
-        if (onSelectedLisener != null) {
-            LocalItem selectedItem = playlists.get(position);
+        if (onSelectedListener != null) {
+            final LocalItem selectedItem = playlists.get(position);
 
             if (selectedItem instanceof PlaylistMetadataEntry) {
                 final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
-                onSelectedLisener
+                onSelectedListener
                         .onLocalPlaylistSelected(entry.uid, entry.name);
 
             } else if (selectedItem instanceof PlaylistRemoteEntity) {
                 final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
-                onSelectedLisener.onRemotePlaylistSelected(
+                onSelectedListener.onRemotePlaylistSelected(
                         entry.getServiceId(), entry.getUrl(), entry.getName());
             }
         }
@@ -144,26 +150,6 @@ public class SelectPlaylistFragment extends DialogFragment {
 
     }
 
-    private Observer<List<PlaylistLocalItem>> getPlaylistsObserver() {
-        return new Observer<List<PlaylistLocalItem>>() {
-            @Override
-            public void onSubscribe(final Disposable d) { }
-
-            @Override
-            public void onNext(final List<PlaylistLocalItem> newPlaylists) {
-                displayPlaylists(newPlaylists);
-            }
-
-            @Override
-            public void onError(final Throwable exception) {
-                SelectPlaylistFragment.this.onError(exception);
-            }
-
-            @Override
-            public void onComplete() { }
-        };
-    }
-
     /*//////////////////////////////////////////////////////////////////////////
     // Error
     //////////////////////////////////////////////////////////////////////////*/
@@ -178,7 +164,7 @@ public class SelectPlaylistFragment extends DialogFragment {
     // Interfaces
     //////////////////////////////////////////////////////////////////////////*/
 
-    public interface OnSelectedLisener {
+    public interface OnSelectedListener {
         void onLocalPlaylistSelected(long id, String name);
         void onRemotePlaylistSelected(int serviceId, String url, String name);
     }
@@ -192,14 +178,14 @@ public class SelectPlaylistFragment extends DialogFragment {
         @Override
         public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent,
                                                           final int viewType) {
-            View item = LayoutInflater.from(parent.getContext())
+            final View item = LayoutInflater.from(parent.getContext())
                     .inflate(R.layout.list_playlist_mini_item, parent, false);
             return new SelectPlaylistItemHolder(item);
         }
 
         @Override
         public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) {
-            PlaylistLocalItem selectedItem = playlists.get(position);
+            final PlaylistLocalItem selectedItem = playlists.get(position);
 
             if (selectedItem instanceof PlaylistMetadataEntry) {
                 final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
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
index ad8ee043b..1b26cd529 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java
@@ -49,7 +49,7 @@ public class ChooseTabsFragment extends Fragment {
 
     private TabsManager tabsManager;
 
-    private List<Tab> tabList = new ArrayList<>();
+    private final List<Tab> tabList = new ArrayList<>();
     private ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter;
 
     /*//////////////////////////////////////////////////////////////////////////
@@ -79,10 +79,10 @@ public class ChooseTabsFragment extends Fragment {
 
         initButton(rootView);
 
-        RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs);
+        final RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs);
         listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext()));
 
-        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
+        final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
         itemTouchHelper.attachToRecyclerView(listSelectedTabs);
 
         selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper);
@@ -139,7 +139,7 @@ public class ChooseTabsFragment extends Fragment {
 
     private void updateTitle() {
         if (getActivity() instanceof AppCompatActivity) {
-            ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+            final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
             if (actionBar != null) {
                 actionBar.setTitle(R.string.main_page_content);
             }
@@ -202,20 +202,20 @@ public class ChooseTabsFragment extends Fragment {
         switch (type) {
             case KIOSK:
                 SelectKioskFragment selectKioskFragment = new SelectKioskFragment();
-                selectKioskFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) ->
+                selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) ->
                         addTab(new Tab.KioskTab(serviceId, kioskId)));
                 selectKioskFragment.show(requireFragmentManager(), "select_kiosk");
                 return;
             case CHANNEL:
                 SelectChannelFragment selectChannelFragment = new SelectChannelFragment();
-                selectChannelFragment.setOnSelectedLisener((serviceId, url, name) ->
+                selectChannelFragment.setOnSelectedListener((serviceId, url, name) ->
                         addTab(new Tab.ChannelTab(serviceId, url, name)));
                 selectChannelFragment.show(requireFragmentManager(), "select_channel");
                 return;
             case PLAYLIST:
                 SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment();
-                selectPlaylistFragment.setOnSelectedLisener(
-                        new SelectPlaylistFragment.OnSelectedLisener() {
+                selectPlaylistFragment.setOnSelectedListener(
+                        new SelectPlaylistFragment.OnSelectedListener() {
                             @Override
                             public void onLocalPlaylistSelected(final long id, final String name) {
                                 addTab(new Tab.PlaylistTab(id, name));
@@ -360,7 +360,7 @@ public class ChooseTabsFragment extends Fragment {
         @Override
         public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(
                 @NonNull final ViewGroup parent, final int viewType) {
-            View view = inflater.inflate(R.layout.list_choose_tabs, parent, false);
+            final View view = inflater.inflate(R.layout.list_choose_tabs, parent, false);
             return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view);
         }
 
@@ -417,8 +417,8 @@ public class ChooseTabsFragment extends Fragment {
                                 .getChannelServiceId()) + "/" + tab.getTabName(requireContext());
                         break;
                     case PLAYLIST:
-                        int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId();
-                        String serviceName = serviceId == -1
+                        final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId();
+                        final String serviceName = serviceId == -1
                                 ? getString(R.string.local)
                                 : NewPipe.getNameOfService(serviceId);
                         tabName = serviceName + "/" + tab.getTabName(requireContext());
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
index 28a4e2723..85de8462d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
@@ -504,11 +504,7 @@ public abstract class Tab {
         private LocalItemType playlistType;
 
         private PlaylistTab() {
-            this.playlistName = "<no-name>";
-            this.playlistId = -1;
-            this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM;
-            this.playlistServiceId = -1;
-            this.playlistUrl = "<no-url>";
+            this(-1, "<no-name>");
         }
 
         public PlaylistTab(final long playlistId, final String playlistName) {
@@ -545,18 +541,16 @@ public abstract class Tab {
         @DrawableRes
         @Override
         public int getTabIconRes(final Context context) {
-            return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_list);
+            return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark);
         }
 
         @Override
         public Fragment getFragment(final Context context) {
             if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) {
-                return LocalPlaylistFragment.getInstance(playlistId,
-                        playlistName == null ? "" : playlistName);
+                return LocalPlaylistFragment.getInstance(playlistId, playlistName);
 
             } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM
-                return PlaylistFragment.getInstance(playlistServiceId, playlistUrl,
-                        playlistName == null ? "" : playlistName);
+                return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName);
             }
         }
 
@@ -583,16 +577,16 @@ public abstract class Tab {
 
         @Override
         public boolean equals(final Object obj) {
-            boolean baseEqual = super.equals(obj)
+            final boolean baseEquals = super.equals(obj)
                     && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType)
                     && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName);
 
-            if (!baseEqual) {
+            if (!baseEquals) {
                 return false;
             }
 
-            boolean localPlaylistEquals = playlistId == ((PlaylistTab) obj).playlistId;
-            boolean remotePlaylistEquals =
+            final boolean localPlaylistEquals = playlistId == ((PlaylistTab) obj).playlistId;
+            final boolean remotePlaylistEquals =
                     playlistServiceId == ((PlaylistTab) obj).playlistServiceId
                             && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl);
 
diff --git a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
index 230be4d28..e5309210c 100644
--- a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
@@ -14,7 +14,7 @@ public final class PlaylistItemsUtils {
     public static List<PlaylistLocalItem> merge(
             final List<PlaylistMetadataEntry> localPlaylists,
             final List<PlaylistRemoteEntity> remotePlaylists) {
-        List<PlaylistLocalItem> items = new ArrayList<>(
+        final List<PlaylistLocalItem> items = new ArrayList<>(
                 localPlaylists.size() + remotePlaylists.size());
         items.addAll(localPlaylists);
         items.addAll(remotePlaylists);
diff --git a/app/src/main/res/layout/select_playlist_fragment.xml b/app/src/main/res/layout/select_playlist_fragment.xml
index 14462662a..ca0d49e32 100644
--- a/app/src/main/res/layout/select_playlist_fragment.xml
+++ b/app/src/main/res/layout/select_playlist_fragment.xml
@@ -23,7 +23,9 @@
         android:id="@+id/items_list"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        tools:listitem="@layout/list_playlist_mini_item"></androidx.recyclerview.widget.RecyclerView>
+        tools:listitem="@layout/list_playlist_mini_item">
+
+    </androidx.recyclerview.widget.RecyclerView>
 
 
     <TextView
@@ -40,4 +42,4 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_margin="5dp"/>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>

From 248212588dfe6ad14814a92f8e17516a427635cc Mon Sep 17 00:00:00 2001
From: Stypox <stypox@pm.me>
Date: Sun, 28 Jun 2020 22:55:18 +0200
Subject: [PATCH 3/4] Fix style issues

---
 .../org/schabi/newpipe/settings/tabs/Tab.java   | 17 ++++++-----------
 app/src/main/res/values/strings.xml             |  2 +-
 2 files changed, 7 insertions(+), 12 deletions(-)

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
index 85de8462d..b0511cd11 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java
@@ -577,20 +577,15 @@ public abstract class Tab {
 
         @Override
         public boolean equals(final Object obj) {
-            final boolean baseEquals = super.equals(obj)
+            if (!(super.equals(obj)
                     && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType)
-                    && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName);
-
-            if (!baseEquals) {
-                return false;
+                    && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName))) {
+                return false; // base objects are different
             }
 
-            final boolean localPlaylistEquals = playlistId == ((PlaylistTab) obj).playlistId;
-            final boolean remotePlaylistEquals =
-                    playlistServiceId == ((PlaylistTab) obj).playlistServiceId
-                            && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl);
-
-            return localPlaylistEquals || remotePlaylistEquals;
+            return (playlistId == ((PlaylistTab) obj).playlistId)                     // local
+                    || (playlistServiceId == ((PlaylistTab) obj).playlistServiceId    // remote
+                    && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl));
         }
 
         public int getPlaylistServiceId() {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 79b86d2ad..79624032b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -653,5 +653,5 @@
     <string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
     <string name="channel_created_by">Created by %s</string>
     <string name="video_detail_by">By %s</string>
-    <string name="playlist_page_summary">Playlist Page</string>
+    <string name="playlist_page_summary">Playlist page</string>
 </resources>

From 8cc21920b75c0744f31cb8f09d1d97e69c6c80bd Mon Sep 17 00:00:00 2001
From: Stypox <stypox@pm.me>
Date: Sat, 4 Jul 2020 11:31:24 +0200
Subject: [PATCH 4/4] Move local/remote playlist merge() to PlaylistLocalItem
 class

In order not to have a utils class just for one function
---
 .../database/playlist/PlaylistLocalItem.java  | 26 +++++++++++++
 .../local/bookmark/BookmarkFragment.java      |  3 +-
 .../settings/SelectPlaylistFragment.java      |  5 +--
 .../newpipe/util/PlaylistItemsUtils.java      | 38 -------------------
 4 files changed, 29 insertions(+), 43 deletions(-)
 delete mode 100644 app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java

diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
index fd99f84a1..3ce95631c 100644
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
@@ -1,7 +1,33 @@
 package org.schabi.newpipe.database.playlist;
 
 import org.schabi.newpipe.database.LocalItem;
+import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public interface PlaylistLocalItem extends LocalItem {
     String getOrderingName();
+
+    static List<PlaylistLocalItem> merge(
+            final List<PlaylistMetadataEntry> localPlaylists,
+            final List<PlaylistRemoteEntity> remotePlaylists) {
+        final List<PlaylistLocalItem> items = new ArrayList<>(
+                localPlaylists.size() + remotePlaylists.size());
+        items.addAll(localPlaylists);
+        items.addAll(remotePlaylists);
+
+        Collections.sort(items, (left, right) -> {
+            final String on1 = left.getOrderingName();
+            final String on2 = right.getOrderingName();
+            if (on1 == null) {
+                return on2 == null ? 0 : 1;
+            } else {
+                return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
+            }
+        });
+
+        return items;
+    }
 }
diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
index 7485a73ef..7e11d7a2e 100644
--- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
@@ -29,7 +29,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
 import org.schabi.newpipe.report.UserAction;
 import org.schabi.newpipe.util.NavigationHelper;
 import org.schabi.newpipe.util.OnClickGesture;
-import org.schabi.newpipe.util.PlaylistItemsUtils;
 
 import java.util.List;
 
@@ -138,7 +137,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
         super.startLoading(forceLoad);
 
         Flowable.combineLatest(localPlaylistManager.getPlaylists(),
-                remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge)
+                remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
                 .onBackpressureLatest()
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(getPlaylistsSubscriber());
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
index 795ced5fa..1d5c94421 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java
@@ -29,7 +29,6 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
 import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
 import org.schabi.newpipe.report.ErrorActivity;
 import org.schabi.newpipe.report.UserAction;
-import org.schabi.newpipe.util.PlaylistItemsUtils;
 
 import java.util.List;
 import java.util.Vector;
@@ -84,12 +83,12 @@ public class SelectPlaylistFragment extends DialogFragment {
         recyclerView.setVisibility(View.GONE);
         emptyView.setVisibility(View.GONE);
 
-        final AppDatabase database = NewPipeDatabase.getInstance(this.getContext());
+        final AppDatabase database = NewPipeDatabase.getInstance(requireContext());
         final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
         final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
 
         playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
-                remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge)
+                remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
                 .subscribe(this::displayPlaylists, this::onError);
 
         return v;
diff --git a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
deleted file mode 100644
index e5309210c..000000000
--- a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.schabi.newpipe.util;
-
-import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
-import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
-import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public final class PlaylistItemsUtils {
-    private PlaylistItemsUtils() { }
-
-    public static List<PlaylistLocalItem> merge(
-            final List<PlaylistMetadataEntry> localPlaylists,
-            final List<PlaylistRemoteEntity> remotePlaylists) {
-        final List<PlaylistLocalItem> items = new ArrayList<>(
-                localPlaylists.size() + remotePlaylists.size());
-        items.addAll(localPlaylists);
-        items.addAll(remotePlaylists);
-
-        Collections.sort(items, (left, right) -> {
-            String on1 = left.getOrderingName();
-            String on2 = right.getOrderingName();
-            if (on1 == null && on2 == null) {
-                return 0;
-            } else if (on1 != null && on2 == null) {
-                return -1;
-            } else if (on1 == null && on2 != null) {
-                return 1;
-            } else {
-                return on1.compareToIgnoreCase(on2);
-            }
-        });
-
-        return items;
-    }
-}