From 10c4f7b465f37020ab8aab43a040fb60439cf239 Mon Sep 17 00:00:00 2001
From: Tonelico <karyogamy@users.noreply.github.com>
Date: Mon, 7 Aug 2017 06:02:30 -0700
Subject: [PATCH 1/2] Added basic channel subscription and feed pages (#620)

Added basic channel subscription and feed pages

- Room Persistence for sqlite support.
- RxJava2 for reactive async support.
- Stetho for database inspection support.
- Enabled Multidex for debug build.
---
 app/build.gradle                              |  18 +
 app/src/debug/AndroidManifest.xml             |  17 +
 .../java/org/schabi/newpipe/DebugApp.java     |  63 +++
 app/src/main/java/org/schabi/newpipe/App.java |   3 +
 .../java/org/schabi/newpipe/MainActivity.java |   7 +
 .../org/schabi/newpipe/NewPipeDatabase.java   |  34 ++
 .../schabi/newpipe/database/AppDatabase.java  |  15 +
 .../org/schabi/newpipe/database/BasicDAO.java |  48 ++
 .../subscription/SubscriptionDAO.java         |  30 ++
 .../subscription/SubscriptionEntity.java      | 113 ++++
 .../newpipe/fragments/BlankFragment.java      |  22 +
 .../newpipe/fragments/FeedFragment.java       | 495 ++++++++++++++++++
 .../newpipe/fragments/MainFragment.java       |  65 ++-
 .../fragments/SubscriptionFragment.java       | 278 ++++++++++
 .../fragments/SubscriptionService.java        | 170 ++++++
 .../fragments/channel/ChannelFragment.java    | 201 ++++++-
 .../newpipe/info_list/InfoItemBuilder.java    |   3 +-
 .../newpipe/info_list/InfoListAdapter.java    |  12 +-
 .../schabi/newpipe/util/NavigationHelper.java |   9 +
 .../main/res/layout-land/channel_header.xml   |   5 +-
 app/src/main/res/layout/channel_header.xml    |   7 +-
 app/src/main/res/layout/empty_view_panel.xml  |  17 +
 app/src/main/res/layout/fragment_blank.xml    |  19 +
 app/src/main/res/layout/fragment_channel.xml  |  10 +
 app/src/main/res/layout/fragment_main.xml     |  17 +-
 .../main/res/layout/fragment_subscription.xml |  49 ++
 app/src/main/res/layout/load_item_footer.xml  |  28 +
 .../main/res/layout/subscription_header.xml   |  33 ++
 app/src/main/res/menu/menu_channel.xml        |   5 +
 app/src/main/res/values-land/dimens.xml       |   1 +
 app/src/main/res/values/dimens.xml            |   1 +
 app/src/main/res/values/strings.xml           |  10 +
 app/src/main/res/values/styles.xml            |   2 +-
 33 files changed, 1775 insertions(+), 32 deletions(-)
 create mode 100644 app/src/debug/AndroidManifest.xml
 create mode 100644 app/src/debug/java/org/schabi/newpipe/DebugApp.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/database/AppDatabase.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java
 create mode 100644 app/src/main/res/layout/empty_view_panel.xml
 create mode 100644 app/src/main/res/layout/fragment_blank.xml
 create mode 100644 app/src/main/res/layout/fragment_subscription.xml
 create mode 100644 app/src/main/res/layout/load_item_footer.xml
 create mode 100644 app/src/main/res/layout/subscription_header.xml

diff --git a/app/build.gradle b/app/build.gradle
index 7d0ce971c..b7e216a02 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,6 +18,12 @@ android {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
+        debug {
+            multiDexEnabled true
+
+            debuggable true
+            applicationIdSuffix ".debug"
+        }
     }
 
     lintOptions {
@@ -58,4 +64,16 @@ dependencies {
     compile 'com.github.nirhart:parallaxscroll:1.0'
     compile 'com.nononsenseapps:filepicker:3.0.0'
     compile 'com.google.android.exoplayer:exoplayer:r2.4.2'
+
+    debugCompile 'com.facebook.stetho:stetho:1.5.0'
+    debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0'
+    debugCompile 'com.android.support:multidex:1.0.1'
+
+    compile "android.arch.persistence.room:runtime:1.0.0-alpha8"
+    annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha8"
+
+    compile "io.reactivex.rxjava2:rxjava:2.1.2"
+    compile "io.reactivex.rxjava2:rxandroid:2.0.1"
+    compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
+    compile "android.arch.persistence.room:rxjava2:1.0.0-alpha8"
 }
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..614f93faf
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+    package="org.schabi.newpipe"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+
+    <application
+        tools:replace="android:name"
+        android:name=".DebugApp"/>
+
+</manifest>
\ No newline at end of file
diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java
new file mode 100644
index 000000000..964d7c099
--- /dev/null
+++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java
@@ -0,0 +1,63 @@
+package org.schabi.newpipe;
+
+import android.content.Context;
+import android.support.multidex.MultiDex;
+
+import com.facebook.stetho.Stetho;
+
+/**
+ * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
+ * App.java is part of NewPipe.
+ *
+ * NewPipe is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * NewPipe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+public class DebugApp extends App {
+    private static final String TAG = DebugApp.class.toString();
+
+    @Override
+    protected void attachBaseContext(Context base) {
+        super.attachBaseContext(base);
+        MultiDex.install(this);
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        initStetho();
+    }
+
+    private void initStetho() {
+        // Create an InitializerBuilder
+        Stetho.InitializerBuilder initializerBuilder =
+                Stetho.newInitializerBuilder(this);
+
+        // Enable Chrome DevTools
+        initializerBuilder.enableWebKitInspector(
+                Stetho.defaultInspectorModulesProvider(this)
+        );
+
+        // Enable command line interface
+        initializerBuilder.enableDumpapp(
+                Stetho.defaultDumperPluginsProvider(getApplicationContext())
+        );
+
+        // Use the InitializerBuilder to generate an Initializer
+        Stetho.Initializer initializer = initializerBuilder.build();
+
+        // Initialize Stetho with the Initializer
+        Stetho.initialize(initializer);
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java
index ee1de0196..94a3daf4f 100644
--- a/app/src/main/java/org/schabi/newpipe/App.java
+++ b/app/src/main/java/org/schabi/newpipe/App.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe;
 import android.app.Application;
 import android.content.Context;
 
+import com.facebook.stetho.Stetho;
 import com.nostra13.universalimageloader.core.ImageLoader;
 import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
 
@@ -64,6 +65,8 @@ public class App extends Application {
                             "Could not initialize ACRA crash report", R.string.app_ui_crash));
         }
 
+        NewPipeDatabase.getInstance( getApplicationContext() );
+
         //init NewPipe
         NewPipe.init(Downloader.getInstance());
 
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 9cc43bb44..c3782965e 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -24,7 +24,11 @@ import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
+import android.support.design.widget.TabLayout;
 import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
@@ -36,6 +40,9 @@ import android.view.View;
 
 import org.schabi.newpipe.download.DownloadActivity;
 import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.fragments.FeedFragment;
+import org.schabi.newpipe.fragments.MainFragment;
+import org.schabi.newpipe.fragments.SubscriptionFragment;
 import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
 import org.schabi.newpipe.fragments.search.SearchFragment;
 import org.schabi.newpipe.settings.SettingsActivity;
diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
new file mode 100644
index 000000000..3e3c4d9db
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
@@ -0,0 +1,34 @@
+package org.schabi.newpipe;
+
+import android.arch.persistence.room.Room;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.schabi.newpipe.database.AppDatabase;
+
+import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
+
+public class NewPipeDatabase {
+
+    private static AppDatabase sInstance;
+
+    // For Singleton instantiation
+    private static final Object LOCK = new Object();
+
+    @NonNull
+    public synchronized static AppDatabase getInstance(Context context) {
+        if (sInstance == null) {
+            synchronized (LOCK) {
+                if (sInstance == null) {
+
+                    sInstance = Room.databaseBuilder(
+                            context.getApplicationContext(),
+                            AppDatabase.class,
+                            DATABASE_NAME
+                    ).build();
+                }
+            }
+        }
+        return sInstance;
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java
new file mode 100644
index 000000000..8ce33d32d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java
@@ -0,0 +1,15 @@
+package org.schabi.newpipe.database;
+
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.RoomDatabase;
+
+import org.schabi.newpipe.database.subscription.SubscriptionDAO;
+import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+
+@Database(entities = {SubscriptionEntity.class}, version = 1, exportSchema = false)
+public abstract class AppDatabase extends RoomDatabase{
+
+    public static final String DATABASE_NAME = "newpipe.db";
+
+    public abstract SubscriptionDAO subscriptionDAO();
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
new file mode 100644
index 000000000..beb5f4b77
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
@@ -0,0 +1,48 @@
+package org.schabi.newpipe.database;
+
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Delete;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.OnConflictStrategy;
+import android.arch.persistence.room.Update;
+
+import java.util.Collection;
+import java.util.List;
+
+import io.reactivex.Completable;
+import io.reactivex.Flowable;
+
+@Dao
+public interface BasicDAO<Entity> {
+    /* Inserts */
+    @Insert(onConflict = OnConflictStrategy.FAIL)
+    long insert(final Entity entity);
+
+    @Insert(onConflict = OnConflictStrategy.FAIL)
+    List<Long> insertAll(final Entity... entities);
+
+    @Insert(onConflict = OnConflictStrategy.FAIL)
+    List<Long> insertAll(final Collection<Entity> entities);
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    long upsert(final Entity entity);
+
+    /* Searches */
+    Flowable<List<Entity>> findAll();
+
+    Flowable<List<Entity>> listByService(int serviceId);
+
+    /* Deletes */
+    @Delete
+    int delete(final Entity entity);
+
+    @Delete
+    int delete(final Collection<Entity> entities);
+
+    /* Updates */
+    @Update
+    int update(final Entity entity);
+
+    @Update
+    int update(final Collection<Entity> entities);
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java
new file mode 100644
index 000000000..c34048a3e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java
@@ -0,0 +1,30 @@
+package org.schabi.newpipe.database.subscription;
+
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Query;
+
+import org.schabi.newpipe.database.BasicDAO;
+
+import java.util.List;
+
+import io.reactivex.Flowable;
+
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
+
+@Dao
+public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
+    @Override
+    @Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
+    Flowable<List<SubscriptionEntity>> findAll();
+
+    @Override
+    @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
+    Flowable<List<SubscriptionEntity>> listByService(int serviceId);
+
+    @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
+            SUBSCRIPTION_URL + " LIKE :url AND " +
+            SUBSCRIPTION_SERVICE_ID + " = :serviceId")
+    Flowable<List<SubscriptionEntity>> findAll(int serviceId, String url);
+}
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java
new file mode 100644
index 000000000..1e0a63bcd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java
@@ -0,0 +1,113 @@
+package org.schabi.newpipe.database.subscription;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Index;
+import android.arch.persistence.room.PrimaryKey;
+
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
+import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
+
+@Entity(tableName = SUBSCRIPTION_TABLE,
+        indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
+public class SubscriptionEntity {
+
+    final static String SUBSCRIPTION_TABLE              = "subscriptions";
+    final static String SUBSCRIPTION_SERVICE_ID         = "service_id";
+    final static String SUBSCRIPTION_URL                = "url";
+    final static String SUBSCRIPTION_TITLE              = "title";
+    final static String SUBSCRIPTION_THUMBNAIL_URL      = "thumbnail_url";
+    final static String SUBSCRIPTION_SUBSCRIBER_COUNT   = "subscriber_count";
+    final static String SUBSCRIPTION_DESCRIPTION        = "description";
+
+    @PrimaryKey(autoGenerate = true)
+    private long uid = 0;
+
+    @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
+    private int serviceId = -1;
+
+    @ColumnInfo(name = SUBSCRIPTION_URL)
+    private String url;
+
+    @ColumnInfo(name = SUBSCRIPTION_TITLE)
+    private String title;
+
+    @ColumnInfo(name = SUBSCRIPTION_THUMBNAIL_URL)
+    private String thumbnailUrl;
+
+    @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
+    private Long subscriberCount;
+
+    @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
+    private String description;
+
+    public long getUid() {
+        return uid;
+    }
+
+    /* Keep this package-private since UID should always be auto generated by Room impl */
+    void setUid(long uid) {
+        this.uid = uid;
+    }
+
+    public int getServiceId() {
+        return serviceId;
+    }
+
+    public void setServiceId(int serviceId) {
+        this.serviceId = serviceId;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getThumbnailUrl() {
+        return thumbnailUrl;
+    }
+
+    public void setThumbnailUrl(String thumbnailUrl) {
+        this.thumbnailUrl = thumbnailUrl;
+    }
+
+    public Long getSubscriberCount() {
+        return subscriberCount;
+    }
+
+    public void setSubscriberCount(Long subscriberCount) {
+        this.subscriberCount = subscriberCount;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Ignore
+    public void setData(final String title,
+                        final String thumbnailUrl,
+                        final String description,
+                        final Long subscriberCount) {
+        this.setTitle(title);
+        this.setThumbnailUrl(thumbnailUrl);
+        this.setDescription(description);
+        this.setSubscriberCount(subscriberCount);
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
new file mode 100644
index 000000000..ef92622e6
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
@@ -0,0 +1,22 @@
+package org.schabi.newpipe.fragments;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.schabi.newpipe.R;
+
+public class BlankFragment extends BaseFragment {
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_blank, container, false);
+    }
+
+    @Override
+    protected void reloadContent() {
+
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java
new file mode 100644
index 000000000..155f1ba00
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java
@@ -0,0 +1,495 @@
+package org.schabi.newpipe.fragments;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.jakewharton.rxbinding2.view.RxView;
+
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+import org.schabi.newpipe.MainActivity;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+import org.schabi.newpipe.extractor.InfoItem;
+import org.schabi.newpipe.extractor.channel.ChannelInfo;
+import org.schabi.newpipe.info_list.InfoItemBuilder;
+import org.schabi.newpipe.info_list.InfoListAdapter;
+import org.schabi.newpipe.report.ErrorActivity;
+import org.schabi.newpipe.util.NavigationHelper;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.reactivex.Flowable;
+import io.reactivex.MaybeObserver;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+
+import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL;
+import static org.schabi.newpipe.util.AnimationUtils.animateView;
+
+public class FeedFragment extends BaseFragment {
+    private static final String VIEW_STATE_KEY = "view_state_key";
+    private static final String INFO_ITEMS_KEY = "info_items_key";
+
+    private static final int FEED_LOAD_SIZE = 4;
+    private static final int LOAD_ITEM_DEBOUNCE_INTERVAL = 500;
+
+    private final String TAG = "FeedFragment@" + Integer.toHexString(hashCode());
+
+    private View inflatedView;
+    private View emptyPanel;
+    private View loadItemFooter;
+
+    private InfoListAdapter infoListAdapter;
+    private RecyclerView resultRecyclerView;
+
+    private Parcelable viewState;
+    private AtomicBoolean retainFeedItems;
+
+    private SubscriptionService subscriptionService;
+
+    private Disposable loadItemObserver;
+    private Disposable subscriptionObserver;
+    private Subscription feedSubscriber;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment LifeCycle
+    ///////////////////////////////////////////////////////////////////////////
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        subscriptionService = SubscriptionService.getInstance(getContext());
+
+        retainFeedItems = new AtomicBoolean(false);
+
+        if (infoListAdapter == null) {
+            infoListAdapter = new InfoListAdapter(getActivity());
+        }
+
+        if (savedInstanceState != null) {
+            // Get recycler view state
+            viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY);
+
+            // Deserialize and get recycler adapter list
+            final Object[] serializedInfoItems = (Object[]) savedInstanceState.getSerializable(INFO_ITEMS_KEY);
+            if (serializedInfoItems != null) {
+                final InfoItem[] infoItems = Arrays.copyOf(
+                        serializedInfoItems,
+                        serializedInfoItems.length,
+                        InfoItem[].class
+                );
+                final List<InfoItem> feedInfos = Arrays.asList(infoItems);
+                infoListAdapter.addInfoItemList( feedInfos );
+            }
+
+            // Already displayed feed items survive configuration changes
+            retainFeedItems.set(true);
+        }
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+        if (inflatedView == null) {
+            inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false);
+        }
+        return inflatedView;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        if (resultRecyclerView != null) {
+            outState.putParcelable(
+                    VIEW_STATE_KEY,
+                    resultRecyclerView.getLayoutManager().onSaveInstanceState()
+            );
+        }
+
+        if (infoListAdapter != null) {
+            outState.putSerializable(INFO_ITEMS_KEY, infoListAdapter.getItemsList().toArray());
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        // Do not monitor for updates when user is not viewing the feed fragment.
+        // This is a waste of bandwidth.
+        if (loadItemObserver != null) loadItemObserver.dispose();
+        if (subscriptionObserver != null) subscriptionObserver.dispose();
+        if (feedSubscriber != null) feedSubscriber.cancel();
+
+        loadItemObserver = null;
+        subscriptionObserver = null;
+        feedSubscriber = null;
+
+        loadItemFooter = null;
+
+        // Retain the already displayed items for backstack pops
+        retainFeedItems.set(true);
+
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onDestroy() {
+        subscriptionService = null;
+
+        super.onDestroy();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment Views
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
+        super.onCreateOptionsMenu(menu, inflater);
+
+        ActionBar supportActionBar = activity.getSupportActionBar();
+        if (supportActionBar != null) {
+            supportActionBar.setDisplayShowTitleEnabled(true);
+            supportActionBar.setDisplayHomeAsUpEnabled(true);
+        }
+    }
+
+    private RecyclerView.OnScrollListener getOnScrollListener() {
+        return new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                super.onScrollStateChanged(recyclerView, newState);
+                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                    viewState = recyclerView.getLayoutManager().onSaveInstanceState();
+                }
+            }
+        };
+    }
+
+    @Override
+    protected void initViews(View rootView, Bundle savedInstanceState) {
+        super.initViews(rootView, savedInstanceState);
+
+        if (infoListAdapter == null) return;
+
+        animateView(errorPanel, false, 200);
+        animateView(loadingProgressBar, true, 200);
+
+        emptyPanel = rootView.findViewById(R.id.empty_panel);
+
+        resultRecyclerView = rootView.findViewById(R.id.result_list_view);
+        resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
+
+        loadItemFooter = activity.getLayoutInflater().inflate(R.layout.load_item_footer, resultRecyclerView, false);
+        infoListAdapter.setFooter(loadItemFooter);
+        infoListAdapter.showFooter(false);
+        infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
+            @Override
+            public void selected(int serviceId, String url, String title) {
+                NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
+            }
+        });
+
+        resultRecyclerView.setAdapter(infoListAdapter);
+        resultRecyclerView.addOnScrollListener(getOnScrollListener());
+
+        if (viewState != null) {
+            resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState);
+            viewState = null;
+        }
+
+        if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(R.string.fragment_whats_new);
+
+        populateFeed();
+    }
+
+    private void resetFragment() {
+        if (subscriptionObserver != null) subscriptionObserver.dispose();
+        if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
+    }
+
+    @Override
+    protected void reloadContent() {
+        resetFragment();
+        populateFeed();
+    }
+
+    @Override
+    protected void setErrorMessage(String message, boolean showRetryButton) {
+        super.setErrorMessage(message, showRetryButton);
+
+        resetFragment();
+    }
+
+    /**
+     * Changes the state of the load item footer.
+     *
+     * If the current state of the feed is loaded, this displays the load item button and
+     * starts its reactor.
+     *
+     * Otherwise, show a spinner in place of the loader button. */
+    private void setLoader(final boolean isLoaded) {
+        if (loadItemFooter == null) return;
+
+        if (loadItemObserver != null) loadItemObserver.dispose();
+
+        if (isLoaded) {
+            loadItemObserver = getLoadItemObserver(loadItemFooter);
+        }
+
+        loadItemFooter.findViewById(R.id.paginate_progress_bar).setVisibility(isLoaded ? View.GONE : View.VISIBLE);
+        loadItemFooter.findViewById(R.id.load_more_text).setVisibility(isLoaded ? View.VISIBLE : View.GONE);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Feeds Loader
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Responsible for reacting to subscription database updates and displaying feeds.
+     *
+     * Upon each update, the feed info list is cleared unless the fragment is
+     * recently recovered from a configuration change or backstack.
+     *
+     * All existing and pending feed requests are dropped.
+     *
+     * The newly received list of subscriptions is then transformed into a
+     * flowable, reacting to pulling requests.
+     *
+     * Pulled requests are transformed first into ChannelInfo, then Stream Info items and
+     * displayed on the feed fragment.
+     **/
+    private void populateFeed() {
+        final Consumer<List<SubscriptionEntity>> consumer = new Consumer<List<SubscriptionEntity>>() {
+            @Override
+            public void accept(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception {
+                animateView(loadingProgressBar, false, 200);
+
+                if (subscriptionEntities.isEmpty()) {
+                    infoListAdapter.clearStreamItemList();
+                    emptyPanel.setVisibility(View.VISIBLE);
+                } else {
+                    emptyPanel.setVisibility(View.INVISIBLE);
+                }
+
+                // show progress bar on receiving a non-empty updated list of subscriptions
+                if (!retainFeedItems.get() && !subscriptionEntities.isEmpty()) {
+                    infoListAdapter.clearStreamItemList();
+                    animateView(loadingProgressBar, true, 200);
+                }
+
+                retainFeedItems.set(false);
+                Flowable.fromIterable(subscriptionEntities)
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .subscribe(getSubscriptionObserver());
+            }
+        };
+
+        final Consumer<Throwable> onError = new Consumer<Throwable>() {
+            @Override
+            public void accept(@NonNull Throwable exception) throws Exception {
+                onRxError(exception, "Subscription Database Reactor");
+            }
+        };
+
+        if (subscriptionObserver != null) subscriptionObserver.dispose();
+        subscriptionObserver = subscriptionService.getSubscription()
+                .onErrorReturnItem(Collections.<SubscriptionEntity>emptyList())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(consumer, onError);
+    }
+
+    /**
+     * Responsible for reacting to user pulling request and starting a request for new feed stream.
+     *
+     * On initialization, it automatically requests the amount of feed needed to display
+     * a minimum amount required (FEED_LOAD_SIZE).
+     *
+     * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo
+     * containing the feed streams.
+     **/
+    private Subscriber<SubscriptionEntity> getSubscriptionObserver() {
+        return new Subscriber<SubscriptionEntity>() {
+            @Override
+            public void onSubscribe(Subscription s) {
+                if (feedSubscriber != null) feedSubscriber.cancel();
+                feedSubscriber = s;
+
+                final int requestSize = FEED_LOAD_SIZE - infoListAdapter.getItemsList().size();
+                if (requestSize > 0) {
+                    requestFeed(requestSize);
+                } else {
+                    setLoader(true);
+                }
+
+                animateView(loadingProgressBar, false, 200);
+                // Footer spinner persists until subscription list is exhausted.
+                infoListAdapter.showFooter(true);
+            }
+
+            @Override
+            public void onNext(SubscriptionEntity subscriptionEntity) {
+                setLoader(false);
+
+                subscriptionService.getChannelInfo(subscriptionEntity)
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .onErrorComplete()
+                        .subscribe(getChannelInfoObserver());
+            }
+
+            @Override
+            public void onError(Throwable exception) {
+                onRxError(exception, "Feed Pull Reactor");
+            }
+
+            @Override
+            public void onComplete() {
+                infoListAdapter.showFooter(false);
+            }
+        };
+    }
+
+    /**
+     * On each request, a subscription item from the updated table is transformed
+     * into a ChannelInfo, containing the latest streams from the channel.
+     *
+     * Currently, the feed uses the first into from the list of streams.
+     *
+     * If chosen feed already displayed, then we request another feed from another
+     * subscription, until the subscription table runs out of new items.
+     *
+     * This Observer is self-contained and will dispose itself when complete. However, this
+     * does not obey the fragment lifecycle and may continue running in the background
+     * until it is complete. This is done due to RxJava2 no longer propagate errors once
+     * an observer is unsubscribed while the thread process is still running.
+     *
+     * To solve the above issue, we can either set a global RxJava Error Handler, or
+     * manage exceptions case by case. This should be done if the current implementation is
+     * too costly when dealing with larger subscription sets.
+     **/
+    private MaybeObserver<ChannelInfo> getChannelInfoObserver() {
+        return new MaybeObserver<ChannelInfo>() {
+            Disposable observer;
+            @Override
+            public void onSubscribe(Disposable d) {
+                observer = d;
+            }
+
+            // Called only when response is non-empty
+            @Override
+            public void onSuccess(ChannelInfo channelInfo) {
+                emptyPanel.setVisibility(View.INVISIBLE);
+
+                if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) return;
+
+                final InfoItem item = channelInfo.related_streams.get(0);
+                // Keep requesting new items if the current one already exists
+                if (!doesItemExist(infoListAdapter.getItemsList(), item)) {
+                    infoListAdapter.addInfoItem(item);
+                } else {
+                    requestFeed(1);
+                }
+                onDone();
+            }
+
+            @Override
+            public void onError(Throwable exception) {
+                onRxError(exception, "Feed Display Reactor");
+                onDone();
+            }
+
+            // Called only when response is empty
+            @Override
+            public void onComplete() {
+                onDone();
+            }
+
+            private void onDone() {
+                setLoader(true);
+
+                observer.dispose();
+                observer = null;
+            }
+        };
+    }
+
+    private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
+        for (final InfoItem existingItem: items) {
+            if (existingItem.infoType() == item.infoType() &&
+                    existingItem.getTitle().equals(item.getTitle()) &&
+                    existingItem.getLink().equals(item.getLink())) return true;
+        }
+        return false;
+    }
+
+    private void requestFeed(final int count) {
+        if (feedSubscriber == null) return;
+
+        feedSubscriber.request(count);
+    }
+
+    private Disposable getLoadItemObserver(@NonNull final View itemLoader) {
+        final Consumer<Object> onNext = new Consumer<Object>() {
+            @Override
+            public void accept(Object o) throws Exception {
+                requestFeed(FEED_LOAD_SIZE);
+            }
+        };
+
+        final Consumer<Throwable> onError = new Consumer<Throwable>() {
+            @Override
+            public void accept(Throwable throwable) throws Exception {
+                onRxError(throwable, "Load Button Reactor");
+            }
+        };
+
+        return RxView.clicks(itemLoader)
+                .debounce(LOAD_ITEM_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
+                .subscribe(onNext, onError);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment Error Handling
+    ///////////////////////////////////////////////////////////////////////////
+
+    private void onRxError(final Throwable exception, final String tag) {
+        if (exception instanceof IOException) {
+            onRecoverableError(R.string.network_error);
+        } else {
+            onUnrecoverableError(exception, tag);
+        }
+    }
+
+    private void onRecoverableError(int messageId) {
+        if (!this.isAdded()) return;
+
+        if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
+        setErrorMessage(getString(messageId), true);
+    }
+
+    private void onUnrecoverableError(Throwable exception, final String tag) {
+        if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
+        ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "Feed", tag, R.string.general_error));
+
+        activity.finish();
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
index f8e460123..f7a59c1c9 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
@@ -3,7 +3,11 @@ package org.schabi.newpipe.fragments;
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.design.widget.TabLayout;
 import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+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.util.Log;
@@ -18,12 +22,14 @@ import org.schabi.newpipe.MainActivity;
 import org.schabi.newpipe.R;
 import org.schabi.newpipe.util.NavigationHelper;
 
-public class MainFragment extends Fragment {
+public class MainFragment extends Fragment implements TabLayout.OnTabSelectedListener {
     private final String TAG = "MainFragment@" + Integer.toHexString(hashCode());
     private static final boolean DEBUG = MainActivity.DEBUG;
 
     private AppCompatActivity activity;
 
+    private ViewPager viewPager;
+
     /*//////////////////////////////////////////////////////////////////////////
     // Fragment's LifeCycle
     //////////////////////////////////////////////////////////////////////////*/
@@ -45,7 +51,19 @@ public class MainFragment extends Fragment {
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
         if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
-        return inflater.inflate(R.layout.fragment_main, container, false);
+        View inflatedView = inflater.inflate(R.layout.fragment_main, container, false);
+
+        TabLayout tabLayout = (TabLayout) inflatedView.findViewById(R.id.main_tab_layout);
+        viewPager = (ViewPager) inflatedView.findViewById(R.id.pager);
+
+        /*  Nested fragment, use child fragment here to maintain backstack in view pager. */
+        PagerAdapter adapter = new PagerAdapter(getChildFragmentManager());
+        viewPager.setAdapter(adapter);
+        viewPager.setOffscreenPageLimit(adapter.getCount());
+
+        tabLayout.setupWithViewPager(viewPager);
+
+        return inflatedView;
     }
 
     /*//////////////////////////////////////////////////////////////////////////
@@ -74,4 +92,47 @@ public class MainFragment extends Fragment {
         }
         return super.onOptionsItemSelected(item);
     }
+
+    @Override
+    public void onTabSelected(TabLayout.Tab tab) {
+        viewPager.setCurrentItem(tab.getPosition());
+    }
+
+    @Override
+    public void onTabUnselected(TabLayout.Tab tab) {}
+
+    @Override
+    public void onTabReselected(TabLayout.Tab tab) {}
+
+    private class PagerAdapter extends FragmentPagerAdapter {
+
+        private int[] tabTitles = new int[]{
+                R.string.tab_main,
+                R.string.tab_subscriptions
+        };
+
+        PagerAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            switch ( position ) {
+                case 1:
+                    return new SubscriptionFragment();
+                default:
+                    return new BlankFragment();
+            }
+        }
+
+        @Override
+        public CharSequence getPageTitle(int position) {
+            return getString(this.tabTitles[position]);
+        }
+
+        @Override
+        public int getCount() {
+            return this.tabTitles.length;
+        }
+    }
 }
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java
new file mode 100644
index 000000000..f2db9018d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java
@@ -0,0 +1,278 @@
+package org.schabi.newpipe.fragments;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.schabi.newpipe.MainActivity;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+import org.schabi.newpipe.extractor.InfoItem;
+import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
+import org.schabi.newpipe.info_list.InfoItemBuilder;
+import org.schabi.newpipe.info_list.InfoListAdapter;
+import org.schabi.newpipe.report.ErrorActivity;
+import org.schabi.newpipe.util.NavigationHelper;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL;
+import static org.schabi.newpipe.util.AnimationUtils.animateView;
+
+public class SubscriptionFragment extends BaseFragment {
+    private static final String VIEW_STATE_KEY = "view_state_key";
+    private final String TAG = "SubscriptionFragment@" + Integer.toHexString(hashCode());
+
+    private View inflatedView;
+    private View emptyPanel;
+    private View headerRootLayout;
+    private View whatsNewView;
+
+    private InfoListAdapter infoListAdapter;
+    private RecyclerView resultRecyclerView;
+    private Parcelable viewState;
+
+    /* Used for independent events */
+    private CompositeDisposable disposables;
+    private SubscriptionService subscriptionService;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment LifeCycle
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        disposables = new CompositeDisposable();
+        subscriptionService = SubscriptionService.getInstance( getContext() );
+
+        if (savedInstanceState != null) {
+            viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY);
+        }
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+        if (inflatedView == null) {
+            inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false);
+        }
+        return inflatedView;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putParcelable(VIEW_STATE_KEY, viewState);
+    }
+
+    @Override
+    public void onDestroyView() {
+        if (disposables != null) disposables.clear();
+
+        headerRootLayout = null;
+        whatsNewView = null;
+
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onDestroy() {
+        if (disposables != null) disposables.dispose();
+        disposables = null;
+
+        subscriptionService = null;
+
+        super.onDestroy();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment Views
+    ///////////////////////////////////////////////////////////////////////////
+
+    private RecyclerView.OnScrollListener getOnScrollListener() {
+        return new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                super.onScrollStateChanged(recyclerView, newState);
+                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                    viewState = recyclerView.getLayoutManager().onSaveInstanceState();
+                }
+            }
+        };
+    }
+
+    private View.OnClickListener getWhatsNewOnClickListener() {
+        return new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
+            }
+        };
+    }
+
+    @Override
+    protected void initViews(View rootView, Bundle savedInstanceState) {
+        super.initViews(rootView, savedInstanceState);
+
+        emptyPanel = rootView.findViewById(R.id.empty_panel);
+
+        resultRecyclerView = rootView.findViewById(R.id.result_list_view);
+        resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
+        resultRecyclerView.addOnScrollListener(getOnScrollListener());
+
+        if (infoListAdapter == null) {
+            infoListAdapter = new InfoListAdapter(getActivity());
+            infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false));
+            infoListAdapter.showFooter(false);
+            infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
+                @Override
+                public void selected(int serviceId, String url, String title) {
+                    /* Requires the parent fragment to find holder for fragment replacement */
+                    NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), serviceId, url, title);
+                }
+            });
+        }
+
+        headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, resultRecyclerView, false);
+        infoListAdapter.setHeader(headerRootLayout);
+
+        whatsNewView = headerRootLayout.findViewById(R.id.whatsNew);
+        whatsNewView.setOnClickListener(getWhatsNewOnClickListener());
+
+        resultRecyclerView.setAdapter(infoListAdapter);
+
+        populateView();
+    }
+
+    @Override
+    protected void reloadContent() {
+        populateView();
+    }
+
+    @Override
+    protected void setErrorMessage(String message, boolean showRetryButton) {
+        super.setErrorMessage(message, showRetryButton);
+        resetFragment();
+    }
+
+    private void resetFragment() {
+        if (disposables != null) disposables.clear();
+        if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Subscriptions Loader
+    ///////////////////////////////////////////////////////////////////////////
+
+    private void populateView() {
+        resetFragment();
+
+        animateView(loadingProgressBar, true, 200);
+        animateView(errorPanel, false, 200);
+
+        subscriptionService.getSubscription().toObservable()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(getSubscriptionObserver());
+    }
+
+    private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
+        return new Observer<List<SubscriptionEntity>>() {
+            @Override
+            public void onSubscribe(Disposable d) {
+                animateView(loadingProgressBar, true, 200);
+
+                disposables.add( d );
+            }
+
+            @Override
+            public void onNext(List<SubscriptionEntity> subscriptions) {
+                animateView(loadingProgressBar, true, 200);
+
+                infoListAdapter.clearStreamItemList();
+                infoListAdapter.addInfoItemList( getSubscriptionItems(subscriptions) );
+
+                animateView(loadingProgressBar, false, 200);
+
+                emptyPanel.setVisibility(subscriptions.isEmpty() ? View.VISIBLE : View.INVISIBLE);
+
+                if (viewState != null && resultRecyclerView != null) {
+                    resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState);
+                }
+            }
+
+            @Override
+            public void onError(Throwable exception) {
+                if (exception instanceof IOException) {
+                    onRecoverableError(R.string.network_error);
+                } else {
+                    onUnrecoverableError(exception);
+                }
+            }
+
+            @Override
+            public void onComplete() {
+
+            }
+        };
+    }
+
+    private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
+        List<InfoItem> items = new ArrayList<>();
+        for (final SubscriptionEntity subscription: subscriptions) {
+            ChannelInfoItem item = new ChannelInfoItem();
+            item.webPageUrl = subscription.getUrl();
+            item.serviceId = subscription.getServiceId();
+            item.channelName = subscription.getTitle();
+            item.thumbnailUrl = subscription.getThumbnailUrl();
+            item.subscriberCount = subscription.getSubscriberCount();
+            item.description = subscription.getDescription();
+
+            items.add( item );
+        }
+        Collections.sort(items, new Comparator<InfoItem>() {
+            @Override
+            public int compare(InfoItem o1, InfoItem o2) {
+                return o1.getTitle().compareToIgnoreCase(o2.getTitle());
+            }
+        });
+
+        return items;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Fragment Error Handling
+    ///////////////////////////////////////////////////////////////////////////
+
+    private void onRecoverableError(int messageId) {
+        if (!this.isAdded()) return;
+
+        if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
+        setErrorMessage(getString(messageId), true);
+    }
+
+    private void onUnrecoverableError(Throwable exception) {
+        if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
+        ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "unknown", "unknown", R.string.general_error));
+        activity.finish();
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java
new file mode 100644
index 000000000..369d65664
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java
@@ -0,0 +1,170 @@
+package org.schabi.newpipe.fragments;
+
+import android.content.Context;
+
+import org.schabi.newpipe.NewPipeDatabase;
+import org.schabi.newpipe.database.AppDatabase;
+import org.schabi.newpipe.database.subscription.SubscriptionDAO;
+import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.extractor.channel.ChannelExtractor;
+import org.schabi.newpipe.extractor.channel.ChannelInfo;
+import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Completable;
+import io.reactivex.CompletableSource;
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Scheduler;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.functions.Function;
+import io.reactivex.schedulers.Schedulers;
+
+/** Subscription Service singleton:
+ *  Provides a basis for channel Subscriptions.
+ *  Provides access to subscription table in database as well as
+ *  up-to-date observations on the subscribed channels
+ *  */
+public class SubscriptionService {
+
+    private static SubscriptionService sInstance;
+    private static final Object LOCK = new Object();
+
+    public static SubscriptionService getInstance(Context context) {
+        if (sInstance == null) {
+            synchronized (LOCK) {
+                if (sInstance == null) {
+                    sInstance = new SubscriptionService(context);
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
+    private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500;
+    private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4;
+
+    private AppDatabase db;
+    private Flowable<List<SubscriptionEntity>> subscription;
+
+    private Scheduler subscriptionScheduler;
+
+    private SubscriptionService(Context context) {
+        db = NewPipeDatabase.getInstance( context );
+        subscription = getSubscriptionInfos();
+
+        final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
+        subscriptionScheduler = Schedulers.from(subscriptionExecutor);
+    }
+
+    /** Part of subscription observation pipeline
+     * @see SubscriptionService#getSubscription()
+     */
+    private Flowable<List<SubscriptionEntity>> getSubscriptionInfos() {
+        return subscriptionTable().findAll()
+                // Wait for a period of infrequent updates and return the latest update
+                .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
+                .share()            // Share allows multiple subscribers on the same observable
+                .replay(1)          // Replay synchronizes subscribers to the last emitted result
+                .autoConnect();
+    }
+
+    /**
+     * Provides an observer to the latest update to the subscription table.
+     *
+     *  This observer may be subscribed multiple times, where each subscriber obtains
+     *  the latest synchronized changes available, effectively share the same data
+     *  across all subscribers.
+     *
+     *  This observer has a debounce cooldown, meaning if multiple updates are observed
+     *  in the cooldown interval, only the latest changes are emitted to the subscribers.
+     *  This reduces the amount of observations caused by frequent updates to the database.
+     *  */
+    @android.support.annotation.NonNull
+    public Flowable<List<SubscriptionEntity>> getSubscription() {
+        return subscription;
+    }
+
+    public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) {
+        final StreamingService service = getService(subscriptionEntity.getServiceId());
+        if (service == null) return Maybe.empty();
+
+        final String url = subscriptionEntity.getUrl();
+        final Callable<ChannelInfo> callable = new Callable<ChannelInfo>() {
+            @Override
+            public ChannelInfo call() throws Exception {
+                final ChannelExtractor extractor = service.getChannelExtractorInstance(url, 0);
+                return ChannelInfo.getInfo(extractor);
+            }
+        };
+
+        return Maybe.fromCallable(callable).subscribeOn(subscriptionScheduler);
+    }
+
+    private StreamingService getService(final int serviceId) {
+        try {
+            return NewPipe.getService(serviceId);
+        } catch (ExtractionException e) {
+            return null;
+        }
+    }
+
+    /** Returns the database access interface for subscription table. */
+    public SubscriptionDAO subscriptionTable() {
+        return db.subscriptionDAO();
+    }
+
+    public Completable updateChannelInfo(final int serviceId,
+                                          final String channelUrl,
+                                          final ChannelInfo info) {
+        final Function<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() {
+            @Override
+            public CompletableSource apply(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception {
+                if (subscriptionEntities.size() == 1) {
+                    SubscriptionEntity subscription = subscriptionEntities.get(0);
+
+                    // Subscriber count changes very often, making this check almost unnecessary.
+                    // Consider removing it later.
+                    if (isSubscriptionUpToDate(channelUrl, info, subscription)) {
+                        subscription.setData(info.channel_name, info.avatar_url, "", info.subscriberCount);
+
+                        return update(subscription);
+                    }
+                }
+
+                return Completable.complete();
+            }
+        };
+
+        return subscriptionTable().findAll(serviceId, channelUrl)
+                .firstOrError()
+                .flatMapCompletable(update);
+    }
+
+    private Completable update(final SubscriptionEntity updatedSubscription) {
+        return Completable.fromRunnable(new Runnable() {
+            @Override
+            public void run() {
+                subscriptionTable().update(updatedSubscription);
+            }
+        });
+    }
+
+    private boolean isSubscriptionUpToDate(final String channelUrl,
+                                           final ChannelInfo info,
+                                           final SubscriptionEntity entity) {
+        return channelUrl.equals( entity.getUrl() ) &&
+                info.service_id == entity.getServiceId() &&
+                info.channel_name.equals( entity.getTitle() ) &&
+                info.avatar_url.equals( entity.getThumbnailUrl() ) &&
+                info.subscriberCount == entity.getSubscriberCount();
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java
index 1bb7ddee7..583f00f0c 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java
@@ -20,12 +20,17 @@ import android.view.ViewGroup;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
+import android.widget.Toast;
+
+import com.jakewharton.rxbinding2.view.RxView;
 
 import org.schabi.newpipe.ImageErrorLoadingListener;
 import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.subscription.SubscriptionEntity;
 import org.schabi.newpipe.extractor.InfoItem;
 import org.schabi.newpipe.extractor.channel.ChannelInfo;
 import org.schabi.newpipe.fragments.BaseFragment;
+import org.schabi.newpipe.fragments.SubscriptionService;
 import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener;
 import org.schabi.newpipe.info_list.InfoItemBuilder;
 import org.schabi.newpipe.info_list.InfoListAdapter;
@@ -36,16 +41,30 @@ import org.schabi.newpipe.workers.ChannelExtractorWorker;
 import java.io.Serializable;
 import java.text.NumberFormat;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Action;
+import io.reactivex.functions.Consumer;
+import io.reactivex.functions.Function;
+import io.reactivex.schedulers.Schedulers;
 
 import static org.schabi.newpipe.util.AnimationUtils.animateView;
 
 public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive {
-    private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
+private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
 
     private static final String INFO_LIST_KEY = "info_list_key";
     private static final String CHANNEL_INFO_KEY = "channel_info_key";
     private static final String PAGE_NUMBER_KEY = "page_number_key";
 
+    private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
+
     private InfoListAdapter infoListAdapter;
 
     private ChannelExtractorWorker currentChannelWorker;
@@ -53,9 +72,15 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
     private int serviceId = -1;
     private String channelName = "";
     private String channelUrl = "";
+    private String feedUrl = "";
     private int pageNumber = 0;
     private boolean hasNextPage = true;
 
+    private SubscriptionService subscriptionService;
+
+    private CompositeDisposable disposables;
+    private Disposable subscribeButtonMonitor;
+
     /*//////////////////////////////////////////////////////////////////////////
     // Views
     //////////////////////////////////////////////////////////////////////////*/
@@ -67,7 +92,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
     private ImageView headerAvatarView;
     private TextView headerTitleView;
     private TextView headerSubscribersTextView;
-    private Button headerRssButton;
+    private Button headerSubscribeButton;
 
     /*////////////////////////////////////////////////////////////////////////*/
 
@@ -127,7 +152,13 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
         headerAvatarView = null;
         headerTitleView = null;
         headerSubscribersTextView = null;
-        headerRssButton = null;
+        headerSubscribeButton = null;
+
+        if (disposables != null) disposables.dispose();
+        if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
+        disposables = null;
+        subscribeButtonMonitor = null;
+        subscriptionService = null;
 
         super.onDestroyView();
     }
@@ -176,6 +207,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
             supportActionBar.setDisplayShowTitleEnabled(true);
             supportActionBar.setDisplayHomeAsUpEnabled(true);
         }
+        menu.findItem(R.id.menu_item_rss).setVisible( !TextUtils.isEmpty(feedUrl) );
     }
 
     @Override
@@ -190,13 +222,21 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
                 startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
                 return true;
             }
-            case R.id.menu_item_share:
+            case R.id.menu_item_rss: {
+                Intent intent = new Intent();
+                intent.setAction(Intent.ACTION_VIEW);
+                intent.setData(Uri.parse(currentChannelInfo.feed_url));
+                startActivity(intent);
+                return true;
+            }
+            case R.id.menu_item_share: {
                 Intent intent = new Intent();
                 intent.setAction(Intent.ACTION_SEND);
                 intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
                 intent.setType("text/plain");
                 startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
                 return true;
+            }
             default:
                 return super.onOptionsItemSelected(item);
         }
@@ -231,7 +271,10 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
         headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view);
         headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view);
         headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view);
-        headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button);
+        headerSubscribeButton = (Button) headerRootLayout.findViewById(R.id.channel_subscribe_button);
+
+        disposables = new CompositeDisposable();
+        subscriptionService = SubscriptionService.getInstance( getContext() );
     }
 
     protected void initListeners() {
@@ -255,17 +298,9 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
                 }
             }
         });
-
-        headerRssButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url);
-                Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url));
-                startActivity(i);
-            }
-        });
     }
 
+
     @Override
     protected void reloadContent() {
         if (DEBUG) Log.d(TAG, "reloadContent() called");
@@ -274,6 +309,133 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
         loadPage(0);
     }
 
+    /*//////////////////////////////////////////////////////////////////////////
+    // Channel Subscription
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private void monitorSubscription(final int serviceId,
+                                     final String channelUrl,
+                                     final ChannelInfo info) {
+        subscriptionService.subscriptionTable().findAll(serviceId, channelUrl)
+                .toObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(getSubscribeButtonMonitor(serviceId, channelUrl, info));
+    }
+
+    private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
+        return new Function<Object, Object>() {
+            @Override
+            public Object apply(@NonNull Object o) throws Exception {
+                subscriptionService.subscriptionTable().insert( subscription );
+                return o;
+            }
+        };
+    }
+
+    private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
+        return new Function<Object, Object>() {
+            @Override
+            public Object apply(@NonNull Object o) throws Exception {
+                subscriptionService.subscriptionTable().delete( subscription );
+                return o;
+            }
+        };
+    }
+
+    private Observer<List<SubscriptionEntity>> getSubscribeButtonMonitor(final int serviceId,
+                                                                         final String channelUrl,
+                                                                         final ChannelInfo info) {
+        return new Observer<List<SubscriptionEntity>>() {
+            @Override
+            public void onSubscribe(Disposable d) {
+                disposables.add( d );
+            }
+
+            @Override
+            public void onNext(List<SubscriptionEntity> subscriptionEntities) {
+                if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
+
+                if (subscriptionEntities.isEmpty()) {
+                    if (DEBUG) Log.d(TAG, "No subscription to this channel!");
+                    SubscriptionEntity channel = new SubscriptionEntity();
+                    channel.setServiceId( serviceId );
+                    channel.setUrl( channelUrl );
+                    channel.setData(info.channel_name, info.avatar_url, "", info.subscriberCount);
+
+                    subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
+
+                    headerSubscribeButton.setText(R.string.subscribe_button_title);
+                } else {
+                    if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
+                    final SubscriptionEntity subscription = subscriptionEntities.get(0);
+                    subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
+
+                    headerSubscribeButton.setText(R.string.subscribed_button_title);
+                }
+
+                headerSubscribeButton.setVisibility(View.VISIBLE);
+            }
+
+            @Override
+            public void onError(Throwable throwable) {
+                Log.e(TAG, "Status get failed", throwable);
+                headerSubscribeButton.setVisibility(View.INVISIBLE);
+            }
+
+            @Override
+            public void onComplete() {}
+        };
+    }
+
+    private Disposable monitorSubscribeButton(final Button subscribeButton,
+                                              final Function<Object, Object> action) {
+        final Consumer<Object> onNext = new Consumer<Object>() {
+            @Override
+            public void accept(@NonNull Object o) throws Exception {
+                if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
+            }
+        };
+
+        final Consumer<Throwable> onError = new Consumer<Throwable>() {
+            @Override
+            public void accept(@NonNull Throwable throwable) throws Exception {
+                if (DEBUG) Log.e(TAG, "Subscription Fatal Error: ", throwable.getCause());
+                Toast.makeText(getContext(), R.string.subscription_change_failed, Toast.LENGTH_SHORT).show();
+            }
+        };
+
+        /* Emit clicks from main thread unto io thread */
+        return RxView.clicks(subscribeButton)
+                .subscribeOn(AndroidSchedulers.mainThread())
+                .observeOn(Schedulers.io())
+                .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
+                .map(action)
+                .subscribe(onNext, onError);
+    }
+
+    private Disposable updateSubscription(final int serviceId,
+                                          final String channelUrl,
+                                          final ChannelInfo info) {
+        final Action onComplete = new Action() {
+            @Override
+            public void run() throws Exception {
+                if (DEBUG) Log.d(TAG, "Updated subscription: " + channelUrl);
+            }
+        };
+
+        final Consumer<Throwable> onError = new Consumer<Throwable>() {
+            @Override
+            public void accept(@NonNull Throwable throwable) throws Exception {
+                Log.e(TAG, "Subscription Update Fatal Error: ", throwable);
+                Toast.makeText(getContext(), R.string.subscription_update_failed, Toast.LENGTH_SHORT).show();
+            }
+        };
+
+        return subscriptionService.updateChannelInfo(serviceId, channelUrl, info)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(onComplete, onError);
+    }
+
     /*//////////////////////////////////////////////////////////////////////////
     // Utils
     //////////////////////////////////////////////////////////////////////////*/
@@ -297,7 +459,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
         imageLoader.cancelDisplayTask(headerChannelBanner);
         imageLoader.cancelDisplayTask(headerAvatarView);
 
-        headerRssButton.setVisibility(View.GONE);
+        headerSubscribeButton.setVisibility(View.GONE);
         headerSubscribersTextView.setVisibility(View.GONE);
 
         headerTitleView.setText(channelName != null ? channelName : "");
@@ -331,6 +493,9 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
         animateView(loadingProgressBar, false, 200);
 
         if (!onlyVideos) {
+            feedUrl = info.feed_url;
+            if (activity.getSupportActionBar() != null) activity.getSupportActionBar().invalidateOptionsMenu();
+
             headerRootLayout.setVisibility(View.VISIBLE);
             //animateView(loadingProgressBar, false, 200, null);
 
@@ -354,8 +519,10 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor
                 headerSubscribersTextView.setVisibility(View.VISIBLE);
             } else headerSubscribersTextView.setVisibility(View.GONE);
 
-            if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE);
-            else headerRssButton.setVisibility(View.INVISIBLE);
+            if (disposables != null) disposables.clear();
+            if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
+            disposables.add( updateSubscription(serviceId, channelUrl, info) );
+            monitorSubscription(serviceId, channelUrl, info);
 
             infoListAdapter.showFooter(true);
         }
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
index 1bed5f22d..8033d281b 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
@@ -231,14 +231,13 @@ public class InfoItemBuilder {
         holder.itemRoot.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                if(onStreamInfoItemSelectedListener != null) {
+                if(onChannelInfoItemSelectedListener != null) {
                     onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName);
                 }
             }
         });
     }
 
-
     public String shortViewCount(Long viewCount) {
         if (viewCount >= 1000000000) {
             return Long.toString(viewCount / 1000000000) + billion + " " + viewsS;
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
index df0d9ac9f..0881801f5 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
@@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list;
 
 import android.app.Activity;
 import android.support.v7.widget.RecyclerView;
+import android.text.Layout;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -9,6 +10,7 @@ import android.view.ViewGroup;
 
 import org.schabi.newpipe.R;
 import org.schabi.newpipe.extractor.InfoItem;
+import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -77,6 +79,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
         }
     }
 
+    public void addInfoItem(InfoItem data) {
+        if (data != null) {
+            infoItemList.add( data );
+            notifyDataSetChanged();
+        }
+    }
+
     public void clearStreamItemList() {
         if(infoItemList.isEmpty()) {
             return;
@@ -118,7 +127,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
         if(footer != null && position == infoItemList.size() && showFooter) {
             return 1;
         }
-        switch(infoItemList.get(position).infoType()) {
+        InfoItem item = infoItemList.get(position);
+        switch(item.infoType()) {
             case STREAM:
                 return 2;
             case CHANNEL:
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index e93cd9c08..6473741b4 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -18,6 +18,7 @@ import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.stream_info.AudioStream;
 import org.schabi.newpipe.extractor.stream_info.StreamInfo;
+import org.schabi.newpipe.fragments.FeedFragment;
 import org.schabi.newpipe.fragments.MainFragment;
 import org.schabi.newpipe.fragments.channel.ChannelFragment;
 import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
@@ -139,6 +140,14 @@ public class NavigationHelper {
                 .commit();
     }
 
+    public static void openWhatsNewFragment(FragmentManager fragmentManager) {
+        fragmentManager.beginTransaction()
+                .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
+                .replace(R.id.fragment_holder, new FeedFragment())
+                .addToBackStack(null)
+                .commit();
+    }
+
     /*//////////////////////////////////////////////////////////////////////////
     // Through Intents
     //////////////////////////////////////////////////////////////////////////*/
diff --git a/app/src/main/res/layout-land/channel_header.xml b/app/src/main/res/layout-land/channel_header.xml
index 653379832..1ccab7693 100644
--- a/app/src/main/res/layout-land/channel_header.xml
+++ b/app/src/main/res/layout-land/channel_header.xml
@@ -68,7 +68,7 @@
         tools:visibility="visible"/>
 
     <Button
-        android:id="@+id/channel_rss_button"
+        android:id="@+id/channel_subscribe_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
@@ -76,8 +76,7 @@
         android:layout_gravity="center_vertical|right"
         android:layout_marginRight="4dp"
         android:layout_marginTop="2dp"
-        android:drawableLeft="@drawable/ic_rss_feed_white_24dp"
-        android:text="@string/rss_button_title"
+        android:text="@string/subscribe_button_title"
         android:textSize="@dimen/channel_rss_title_size"
         android:theme="@style/RedButton"
         android:visibility="gone"
diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml
index 3aa4e0634..e950142d9 100644
--- a/app/src/main/res/layout/channel_header.xml
+++ b/app/src/main/res/layout/channel_header.xml
@@ -49,7 +49,7 @@
         android:layout_below="@id/channel_banner_image"
         android:layout_marginLeft="8dp"
         android:layout_marginTop="6dp"
-        android:layout_toLeftOf="@+id/channel_rss_button"
+        android:layout_toLeftOf="@+id/channel_subscribe_button"
         android:layout_toRightOf="@+id/channel_avatar_layout"
         android:textAppearance="?android:attr/textAppearanceLarge"
         android:textSize="@dimen/video_item_detail_title_text_size"
@@ -70,15 +70,14 @@
         tools:visibility="visible"/>
 
     <Button
-        android:id="@+id/channel_rss_button"
+        android:id="@+id/channel_subscribe_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
         android:layout_below="@+id/channel_banner_image"
         android:layout_gravity="center_vertical|right"
         android:layout_marginRight="2dp"
-        android:drawableLeft="@drawable/ic_rss_feed_white_24dp"
-        android:text="@string/rss_button_title"
+        android:text="@string/subscribe_button_title"
         android:textSize="@dimen/channel_rss_title_size"
         android:theme="@style/RedButton"
         android:visibility="gone"
diff --git a/app/src/main/res/layout/empty_view_panel.xml b/app/src/main/res/layout/empty_view_panel.xml
new file mode 100644
index 000000000..47913e19a
--- /dev/null
+++ b/app/src/main/res/layout/empty_view_panel.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:gravity="center">
+    <TextView
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:text="¯\\_(ツ)_/¯"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+    <TextView
+        android:text="Nothing Here But Crickets"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_blank.xml b/app/src/main/res/layout/fragment_blank.xml
new file mode 100644
index 000000000..ad91b1d1d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_blank.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <include layout="@layout/main_bg" />
+
+    <include
+        android:id="@+id/error_panel"
+        layout="@layout/error_retry"
+        tools:visibility="visible"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="50dp"
+        android:visibility="gone" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml
index 4393ca190..70fffbc8f 100644
--- a/app/src/main/res/layout/fragment_channel.xml
+++ b/app/src/main/res/layout/fragment_channel.xml
@@ -32,4 +32,14 @@
         android:visibility="gone"
         tools:visibility="visible" />
 
+    <include
+        android:id="@+id/empty_panel"
+        layout="@layout/empty_view_panel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:layout_marginTop="50dp"
+        android:visibility="gone"
+        tools:visibility="visible"/>
+
 </RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index cd5846b56..ec0ac1066 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -1,8 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="match_parent"
-    android:layout_width="match_parent">
+    android:layout_width="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
-    <include layout="@layout/main_bg" />
+
+    <android.support.design.widget.TabLayout
+        android:id="@+id/main_tab_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        app:tabGravity="fill"/>
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/pager"
+        android:layout_width="match_parent"
+        android:layout_height="fill_parent"
+        android:layout_above="@id/main_tab_layout"/>
 
 </RelativeLayout>
diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml
new file mode 100644
index 000000000..125b1e4b2
--- /dev/null
+++ b/app/src/main/res/layout/fragment_subscription.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/result_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="vertical"
+        app:layoutManager="LinearLayoutManager"
+        tools:listitem="@layout/channel_item"/>
+
+    <ProgressBar
+        android:id="@+id/loading_progress_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:indeterminate="true"
+        android:visibility="gone"
+        tools:visibility="visible"/>
+
+    <!--ERROR PANEL-->
+    <include
+        android:id="@+id/error_panel"
+        layout="@layout/error_retry"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="50dp"
+        android:visibility="gone"
+        tools:visibility="visible"/>
+
+    <include
+        android:id="@+id/empty_panel"
+        layout="@layout/empty_view_panel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:layout_marginTop="50dp"
+        android:visibility="gone"
+        tools:visibility="visible"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/load_item_footer.xml b/app/src/main/res/layout/load_item_footer.xml
new file mode 100644
index 000000000..a802b107d
--- /dev/null
+++ b/app/src/main/res/layout/load_item_footer.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/itemRoot"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/video_item_search_height"
+    android:background="?attr/selectableItemBackground"
+    android:clickable="false"
+    android:padding="@dimen/video_item_search_padding">
+
+    <TextView
+        android:id="@+id/load_more_text"
+        android:text="Load More"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textSize="@dimen/header_footer_text_size"
+        android:visibility="gone"/>
+
+    <ProgressBar
+        android:id="@+id/paginate_progress_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:visibility="gone"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/subscription_header.xml b/app/src/main/res/layout/subscription_header.xml
new file mode 100644
index 000000000..6f21bf7f6
--- /dev/null
+++ b/app/src/main/res/layout/subscription_header.xml
@@ -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:id="@+id/channel_header_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginBottom="12dp">
+
+    <TextView
+        android:id="@+id/whatsNew"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        android:drawableLeft="?attr/rss"
+        android:drawablePadding="5dp"
+        android:text="@string/fragment_whats_new"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textSize="@dimen/header_footer_text_size"
+        android:gravity="left|center"
+        android:clickable="true"
+        tools:ignore="RtlHardcoded"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:layout_marginLeft="8dp"
+        android:layout_marginRight="8dp"
+        android:layout_below="@id/whatsNew"
+        android:background="?attr/colorAccent" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_channel.xml b/app/src/main/res/menu/menu_channel.xml
index c371699be..2b9fc6555 100644
--- a/app/src/main/res/menu/menu_channel.xml
+++ b/app/src/main/res/menu/menu_channel.xml
@@ -7,6 +7,11 @@
         app:showAsAction="never"
         android:title="@string/open_in_browser" />
 
+    <item android:id="@+id/menu_item_rss"
+        app:showAsAction="ifRoom"
+        android:title="@string/rss_button_title"
+        android:icon="?attr/rss"/>
+
     <item android:id="@+id/menu_item_share"
         android:title="@string/share"
         app:showAsAction="ifRoom"
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
index b82dddcb8..fa95c30e0 100644
--- a/app/src/main/res/values-land/dimens.xml
+++ b/app/src/main/res/values-land/dimens.xml
@@ -8,6 +8,7 @@
     <dimen name="video_item_search_duration_text_size">12sp</dimen>
     <dimen name="video_item_search_uploader_text_size">14sp</dimen>
     <dimen name="video_item_search_upload_date_text_size">14sp</dimen>
+    <dimen name="header_footer_text_size">18sp</dimen>
     <!-- Elements Size -->
     <!-- 16 / 9 ratio-->
     <dimen name="video_item_search_thumbnail_image_width">142dp</dimen>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index f8ccd708a..ae78279da 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -7,6 +7,7 @@
     <dimen name="video_item_search_duration_text_size">11sp</dimen>
     <dimen name="video_item_search_uploader_text_size">11sp</dimen>
     <dimen name="video_item_search_upload_date_text_size">12sp</dimen>
+    <dimen name="header_footer_text_size">16sp</dimen>
     <!-- Elements Size -->
     <!-- 16 / 9 ratio-->
     <dimen name="video_item_search_thumbnail_image_width">124dp</dimen>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f253167e2..a2449e02a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,6 +23,16 @@
     <string name="use_external_audio_player_title">Use external audio player</string>
     <string name="popup_mode_share_menu_title">NewPipe Popup mode</string>
     <string name="rss_button_title" translatable="false">RSS</string>
+    <string name="subscribe_button_title">Subscribe</string>
+    <string name="subscribed_button_title">Subscribed</string>
+    <string name="channel_unsubscribed">Channel unsubscribed</string>
+    <string name="subscription_change_failed">Unable to change subscription</string>
+    <string name="subscription_update_failed">Unable to update subscription</string>
+
+    <string name="tab_main">Main</string>
+    <string name="tab_subscriptions">Subscriptions</string>
+
+    <string name="fragment_whats_new">What\'s New</string>
 
     <string name="controls_background_title">Background</string>
     <string name="controls_popup_title">Popup</string>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5800a3ac6..aeae8d655 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -44,7 +44,7 @@
         <item name="download">@drawable/ic_file_download_white_24dp</item>
         <item name="share">@drawable/ic_share_white_24dp</item>
         <item name="cast">@drawable/ic_cast_white_24dp</item>
-        <item name="rss">@drawable/ic_rss_feed_black_24dp</item>
+        <item name="rss">@drawable/ic_rss_feed_white_24dp</item>
         <item name="search">@drawable/ic_search_white_24dp</item>
         <item name="close">@drawable/ic_close_white_24dp</item>
         <item name="filter">@drawable/ic_filter_list_white_24dp</item>

From becc90409fcf2d1d81003b2623da13615e8f3f8c Mon Sep 17 00:00:00 2001
From: Tonelico <karyogamy@users.noreply.github.com>
Date: Mon, 7 Aug 2017 06:04:36 -0700
Subject: [PATCH 2/2] Added option to resume on audio focus regain. (#624)

---
 .../java/org/schabi/newpipe/player/BasePlayer.java  | 13 +++++++++++++
 app/src/main/res/values/settings_keys.xml           |  2 +-
 app/src/main/res/values/strings.xml                 |  2 ++
 app/src/main/res/xml/settings.xml                   |  6 ++++++
 4 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index 10f4feb48..8c2e5c6b8 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -47,6 +47,8 @@ import com.google.android.exoplayer2.util.Util;
 import com.nostra13.universalimageloader.core.ImageLoader;
 import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
 
+import org.schabi.newpipe.R;
+
 import java.io.File;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
@@ -321,10 +323,21 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
         }
     }
 
+    private boolean isResumeAfterAudioFocusGain() {
+        if (this.sharedPreferences == null || this.context == null) return false;
+
+        return this.sharedPreferences.getBoolean(
+                this.context.getString(R.string.resume_on_audio_focus_gain_key),
+                false
+        );
+    }
+
     protected void onAudioFocusGain() {
         if (DEBUG) Log.d(TAG, "onAudioFocusGain() called");
         if (simpleExoPlayer != null) simpleExoPlayer.setVolume(DUCK_AUDIO_TO);
         animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION);
+
+        if (isResumeAfterAudioFocusGain()) simpleExoPlayer.setPlayWhenReady(true);
     }
 
     protected void onAudioFocusLoss() {
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index 071592b73..2a1398b09 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -14,7 +14,7 @@
     <string name="autoplay_through_intent_key" translatable="false">autoplay_through_intent</string>
     <string name="use_old_player_key" translatable="false">use_oldplayer</string>
     <string name="player_gesture_controls_key" translatable="false">player_gesture_controls</string>
-
+    <string name="resume_on_audio_focus_gain_key" translatable="false">resume_on_audio_focus_gain</string>
     <string name="default_resolution_key" translatable="false">default_resolution_preference</string>
     <string name="default_resolution_value" translatable="false">360p</string>
 
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a2449e02a..91dc973ef 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -71,6 +71,8 @@
     <string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string>
     <string name="show_search_suggestions_title">Search suggestions</string>
     <string name="show_search_suggestions_summary">Show suggestions when searching</string>
+    <string name="resume_on_audio_focus_gain_title">Resume on focus gain</string>
+    <string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phone calls)</string>
 
     <string name="download_dialog_title">Download</string>
 
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index 282aba918..a0c358c4d 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -64,6 +64,12 @@
             android:summary="@string/player_gesture_controls_summary"
             android:title="@string/player_gesture_controls_title"/>
 
+        <CheckBoxPreference
+            android:defaultValue="false"
+            android:key="@string/resume_on_audio_focus_gain_key"
+            android:summary="@string/resume_on_audio_focus_gain_summary"
+            android:title="@string/resume_on_audio_focus_gain_title"/>
+
     </PreferenceCategory>
 
     <PreferenceCategory