Move from 'vendor/google_experimental' repo under 'users/akersten'
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..710b76f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*~
+\#*#
+/local.properties
+/captures
diff --git a/apps/Pump/AndroidManifest.xml b/apps/Pump/AndroidManifest.xml
new file mode 100644
index 0000000..b920ac8
--- /dev/null
+++ b/apps/Pump/AndroidManifest.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.pump">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <application
+        android:name=".app.PumpApplication"
+        android:label="Personal Universal MediaPlayer"
+        android:theme="@style/PumpTheme">
+
+        <activity android:name=".activity.PumpActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".activity.ArtistDetailsActivity"/>
+
+        <activity android:name=".activity.AlbumDetailsActivity"/>
+
+        <activity android:name=".activity.GenreDetailsActivity"/>
+
+        <activity android:name=".activity.PlaylistDetailsActivity"/>
+
+        <activity android:name=".activity.MovieDetailsActivity"/>
+
+        <activity android:name=".activity.SeriesDetailsActivity"/>
+
+        <activity android:name=".activity.OtherDetailsActivity"/>
+
+        <activity android:name=".activity.AudioPlayerActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="audio/*"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".activity.VideoPlayerActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="video/*"/>
+            </intent-filter>
+        </activity>
+
+        <!-- TODO Handle music app intents;
+                  https://developer.android.com/reference/android/content/Intent.html#CATEGORY_APP_MUSIC
+                  https://developer.android.com/reference/android/provider/MediaStore.html#INTENT_ACTION_MUSIC_PLAYER -->
+
+        <!-- TODO Handle play from search;
+                  https://developer.android.com/guide/components/intents-common#PlaySearch
+                  https://developer.android.com/reference/android/provider/MediaStore.html#INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
+                  https://developer.android.com/reference/android/provider/MediaStore#INTENT_ACTION_MEDIA_SEARCH
+                  https://developer.android.com/reference/android/provider/MediaStore#INTENT_ACTION_TEXT_OPEN_FROM_SEARCH
+                  https://developer.android.com/reference/android/provider/MediaStore#INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH -->
+
+    </application>
+
+</manifest>
diff --git a/apps/Pump/build.gradle b/apps/Pump/build.gradle
new file mode 100644
index 0000000..75ef26b
--- /dev/null
+++ b/apps/Pump/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 28
+    buildToolsVersion '28.0.2'
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode 1
+        versionName '1.0'
+        vectorDrawables.useSupportLibrary = true
+    }
+    buildTypes {
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+    sourceSets {
+        main.manifest.srcFile 'AndroidManifest.xml'
+        main.java.srcDirs = ['java']
+        main.res.srcDirs = ['res']
+    }
+    compileOptions {
+        targetCompatibility 8
+        sourceCompatibility 8
+    }
+}
+
+dependencies {
+    implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
+    implementation 'androidx.mediarouter:mediarouter:1.0.0-beta01'
+    implementation 'com.google.android.material:material:1.0.0-rc02'
+}
diff --git a/apps/Pump/java/com/android/pump/activity/AlbumDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/AlbumDetailsActivity.java
new file mode 100644
index 0000000..05ae341
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/AlbumDetailsActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.pump.R;
+import com.android.pump.db.Album;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class AlbumDetailsActivity extends AppCompatActivity {
+    private Album mAlbum;
+
+    public static void start(@NonNull Context context, @NonNull Album album) {
+        Intent intent = new Intent(context, AlbumDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", album.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_album_details);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            MediaDb mediaDb = Globals.getMediaDb(this);
+            mAlbum = mediaDb.getAlbumById(id);
+        } else {
+            mAlbum = null;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/ArtistDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/ArtistDetailsActivity.java
new file mode 100644
index 0000000..0ce4320
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/ArtistDetailsActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.pump.R;
+import com.android.pump.db.Artist;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class ArtistDetailsActivity extends AppCompatActivity {
+    private Artist mArtist;
+
+    public static void start(@NonNull Context context, @NonNull Artist artist) {
+        Intent intent = new Intent(context, ArtistDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", artist.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_artist_details);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            MediaDb mediaDb = Globals.getMediaDb(this);
+            mArtist = mediaDb.getArtistById(id);
+        } else {
+            mArtist = null;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/AudioPlayerActivity.java b/apps/Pump/java/com/android/pump/activity/AudioPlayerActivity.java
new file mode 100644
index 0000000..a3fbe59
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/AudioPlayerActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.android.pump.db.Audio;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class AudioPlayerActivity extends AppCompatActivity {
+    public static void start(@NonNull Context context, @NonNull Audio audio) {
+        // TODO Find a better URI (audio.getUri()?)
+        Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                audio.getId());
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        intent.setDataAndTypeAndNormalize(uri, audio.getMimeType());
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/GenreDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/GenreDetailsActivity.java
new file mode 100644
index 0000000..8587495
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/GenreDetailsActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.pump.R;
+import com.android.pump.db.Genre;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class GenreDetailsActivity extends AppCompatActivity {
+    private Genre mGenre;
+
+    public static void start(@NonNull Context context, @NonNull Genre genre) {
+        Intent intent = new Intent(context, GenreDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", genre.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_genre_details);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            MediaDb mediaDb = Globals.getMediaDb(this);
+            mGenre = mediaDb.getGenreById(id);
+        } else {
+            mGenre = null;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/MovieDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/MovieDetailsActivity.java
new file mode 100644
index 0000000..60a1b6f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/MovieDetailsActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Movie;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class MovieDetailsActivity extends AppCompatActivity implements MediaDb.UpdateCallback {
+    private MediaDb mMediaDb;
+    private Movie mMovie;
+
+    public static void start(@NonNull Context context, @NonNull Movie movie) {
+        Intent intent = new Intent(context, MovieDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", movie.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_movie_details);
+
+        setSupportActionBar(findViewById(R.id.activity_movie_details_toolbar));
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayShowTitleEnabled(false);
+            actionBar.setDisplayShowHomeEnabled(true);
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        mMediaDb = Globals.getMediaDb(this);
+        mMediaDb.addMovieUpdateCallback(this);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mMediaDb.removeMovieUpdateCallback(this);
+
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+        getMenuInflater().inflate(R.menu.activity_pump, menu); // TODO activity_movie_details ?
+        return true;
+    }
+
+    @Override
+    public boolean onSupportNavigateUp() {
+        // TODO It should not be necessary to override this method
+        onBackPressed();
+        return true;
+    }
+
+    @Override
+    public void onItemsInserted(int index, int count) { }
+
+    @Override
+    public void onItemsUpdated(int index, int count) {
+        for (int i = index; i < index + count; ++i) {
+            Movie movie = mMediaDb.getMovies().get(i);
+            if (movie.equals(mMovie)) {
+                updateViews();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onItemsRemoved(int index, int count) { }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            mMovie = mMediaDb.getMovieById(id);
+        } else {
+            mMovie = null;
+            // TODO This shouldn't happen -- throw exception?
+        }
+
+        mMediaDb.loadData(mMovie);
+        updateViews();
+    }
+
+    private void updateViews() {
+        ImageView imageView = findViewById(R.id.activity_movie_details_image);
+        ImageView posterView = findViewById(R.id.activity_movie_details_poster);
+        TextView titleView = findViewById(R.id.activity_movie_details_title);
+        TextView attributesView = findViewById(R.id.activity_movie_details_attributes);
+        TextView synopsisView = findViewById(R.id.activity_movie_details_synopsis);
+
+        imageView.setImageURI(mMovie.getThumbnailUri());
+        posterView.setImageURI(mMovie.getPosterUri());
+        titleView.setText(mMovie.getTitle());
+        attributesView.setText("1h 20m"); // TODO Implement
+        synopsisView.setText(mMovie.getSynopsis());
+
+        ImageView playView = findViewById(R.id.activity_movie_details_play);
+        playView.setOnClickListener((view) ->
+                VideoPlayerActivity.start(view.getContext(), mMovie));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/OtherDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/OtherDetailsActivity.java
new file mode 100644
index 0000000..5f2a8dc
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/OtherDetailsActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.format.DateFormat;
+import android.view.Menu;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Other;
+import com.android.pump.util.Globals;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class OtherDetailsActivity extends AppCompatActivity implements MediaDb.UpdateCallback {
+    private MediaDb mMediaDb;
+    private Other mOther;
+
+    public static void start(@NonNull Context context, @NonNull Other other) {
+        Intent intent = new Intent(context, OtherDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", other.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_other_details);
+
+        setSupportActionBar(findViewById(R.id.activity_other_details_toolbar));
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayShowTitleEnabled(false);
+            actionBar.setDisplayShowHomeEnabled(true);
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        mMediaDb = Globals.getMediaDb(this);
+        mMediaDb.addOtherUpdateCallback(this);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mMediaDb.removeOtherUpdateCallback(this);
+
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+        getMenuInflater().inflate(R.menu.activity_pump, menu); // TODO activity_other_details ?
+        return true;
+    }
+
+    @Override
+    public boolean onSupportNavigateUp() {
+        // TODO It should not be necessary to override this method
+        onBackPressed();
+        return true;
+    }
+
+    @Override
+    public void onItemsInserted(int index, int count) { }
+
+    @Override
+    public void onItemsUpdated(int index, int count) {
+        for (int i = index; i < index + count; ++i) {
+            Other other = mMediaDb.getOthers().get(i);
+            if (other.equals(mOther)) {
+                updateViews();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onItemsRemoved(int index, int count) { }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            mOther = mMediaDb.getOtherById(id);
+        } else {
+            // TODO This shouldn't happen -- throw exception?
+            mOther = null;
+        }
+
+        mMediaDb.loadData(mOther);
+        updateViews();
+    }
+
+    private void updateViews() {
+        ImageView imageView = findViewById(R.id.activity_other_details_image);
+        TextView titleView = findViewById(R.id.activity_other_details_title);
+        TextView attributesView = findViewById(R.id.activity_other_details_attributes);
+
+        imageView.setImageURI(mOther.getThumbnailUri());
+        titleView.setText(mOther.getTitle());
+
+        StringBuilder attributes = new StringBuilder();
+        if (mOther.hasDuration()) {
+            long dur = mOther.getDuration();
+            // TODO Move to string resource
+            String duration = String.format("%dm %ds",
+                    TimeUnit.MILLISECONDS.toMinutes(dur),
+                    TimeUnit.MILLISECONDS.toSeconds(dur) -
+                            TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(dur)));
+            attributes.append(duration);
+            attributes.append('\n');
+        }
+        if (mOther.hasDateTaken()) {
+            // TODO Better formatting
+            String date = DateFormat.getLongDateFormat(this).format(new Date(mOther.getDateTaken()));
+            attributes.append(date);
+            attributes.append('\n');
+        }
+        if (mOther.hasLatLong()) {
+            // TODO Use Geocoder; https://developer.android.com/training/location/display-address
+            double latitude = mOther.getLatitude();
+            double longitude = mOther.getLongitude();
+            String latlong = String.format("%f %f", latitude, longitude);
+            attributes.append(latlong);
+            attributes.append('\n');
+        }
+        attributesView.setText(attributes);
+
+        ImageView playView = findViewById(R.id.activity_other_details_play);
+        playView.setOnClickListener((view) ->
+                VideoPlayerActivity.start(view.getContext(), mOther));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/PlaylistDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/PlaylistDetailsActivity.java
new file mode 100644
index 0000000..2b6d12a
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/PlaylistDetailsActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.pump.R;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Playlist;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class PlaylistDetailsActivity extends AppCompatActivity {
+    private Playlist mPlaylist;
+
+    public static void start(@NonNull Context context, @NonNull Playlist playlist) {
+        Intent intent = new Intent(context, PlaylistDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("id", playlist.getId()); // TODO Add constant key
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_playlist_details);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            long id = extras.getLong("id");
+
+            MediaDb mediaDb = Globals.getMediaDb(this);
+            mPlaylist = mediaDb.getPlaylistById(id);
+        } else {
+            mPlaylist = null;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/PumpActivity.java b/apps/Pump/java/com/android/pump/activity/PumpActivity.java
new file mode 100644
index 0000000..f750a52
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/PumpActivity.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.android.pump.R;
+import com.android.pump.fragment.AlbumFragment;
+import com.android.pump.fragment.ArtistFragment;
+import com.android.pump.fragment.AudioFragment;
+import com.android.pump.fragment.GenreFragment;
+import com.android.pump.fragment.HomeFragment;
+import com.android.pump.fragment.MovieFragment;
+import com.android.pump.fragment.OtherFragment;
+import com.android.pump.fragment.PlaylistFragment;
+import com.android.pump.fragment.SeriesFragment;
+import com.android.pump.util.Globals;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.bottomnavigation.BottomNavigationView.OnNavigationItemSelectedListener;
+import com.google.android.material.tabs.TabLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+@UiThread
+public class PumpActivity extends AppCompatActivity implements OnNavigationItemSelectedListener {
+    private static final int REQUIRED_PERMISSIONS_REQUEST_CODE = 42;
+    private static final String[] REQUIRED_PERMISSIONS = {
+        android.Manifest.permission.INTERNET,
+        android.Manifest.permission.READ_EXTERNAL_STORAGE,
+        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+    };
+
+    private static final Page HOME_PAGES[] = {
+        new Page(HomeFragment::newInstance, "Home")
+    };
+    private static final Page VIDEO_PAGES[] = {
+        new Page(MovieFragment::newInstance, "Movies"),
+        new Page(SeriesFragment::newInstance, "TV Shows"),
+        new Page(OtherFragment::newInstance, "Personal"),
+        new Page(HomeFragment::newInstance, "All videos")
+    };
+    private static final Page AUDIO_PAGES[] = {
+        new Page(AudioFragment::newInstance, "All audios"),
+        new Page(PlaylistFragment::newInstance, "Playlists"),
+        new Page(AlbumFragment::newInstance, "Albums"),
+        new Page(GenreFragment::newInstance, "Genres"),
+        new Page(ArtistFragment::newInstance, "Artists")
+    };
+    private static final Page FAVORITE_PAGES[] = {
+        new Page(HomeFragment::newInstance, "Videos"),
+        new Page(HomeFragment::newInstance, "Audios")
+    };
+
+    private ActivityPagerAdapter mActivityPagerAdapter;
+
+    private DrawerLayout mDrawerLayout;
+    private ViewPager mViewPager;
+    private TabLayout mTabLayout;
+    private BottomNavigationView mBottomNavigationView;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // TODO Remove hack
+        // HACK This suppresses bogus strict mode violations generated by setContentView(...)
+        final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+        try {
+            findViewById(View.NO_ID);
+        } finally {
+            StrictMode.setThreadPolicy(oldPolicy);
+        }
+        // END HACK
+        setContentView(R.layout.activity_pump);
+
+        setSupportActionBar(findViewById(R.id.activity_pump_toolbar));
+
+        mActivityPagerAdapter = new ActivityPagerAdapter(getSupportFragmentManager());
+
+        mDrawerLayout = findViewById(R.id.activity_pump_drawer_layout);
+        mViewPager = findViewById(R.id.activity_pump_view_pager);
+        mTabLayout = findViewById(R.id.activity_pump_tab_layout);
+        mBottomNavigationView = findViewById(R.id.activity_pump_bottom_navigation_view);
+
+        mBottomNavigationView.setOnNavigationItemSelectedListener(this);
+        mBottomNavigationView.setSelectedItemId(R.id.menu_home);
+        mViewPager.setAdapter(mActivityPagerAdapter);
+        mTabLayout.setupWithViewPager(mViewPager);
+
+        if (!requestMissingPermissions()) {
+            initialize();
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+        getMenuInflater().inflate(R.menu.activity_pump, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            mDrawerLayout.openDrawer(GravityCompat.START);
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_home:
+                selectPages(item.getTitle(), HOME_PAGES);
+                return true;
+            case R.id.menu_video:
+                selectPages(item.getTitle(), VIDEO_PAGES);
+                return true;
+            case R.id.menu_audio:
+                selectPages(item.getTitle(), AUDIO_PAGES);
+                return true;
+            case R.id.menu_favorite:
+                selectPages(item.getTitle(), FAVORITE_PAGES);
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
+            @NonNull int[] grantResults) {
+        if (requestCode == REQUIRED_PERMISSIONS_REQUEST_CODE) {
+            boolean granted = true;
+            if (grantResults.length == 0) {
+                granted = false;
+            } else {
+                for (int grantResult : grantResults) {
+                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
+                        granted = false;
+                    }
+                }
+            }
+            if (!granted) {
+                finish();
+            } else {
+                initialize();
+            }
+        } else {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    private void initialize() {
+        Globals.getMediaDb(this).load();
+    }
+
+    private boolean requestMissingPermissions() {
+        if (isMissingPermissions()) {
+            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS,
+                    REQUIRED_PERMISSIONS_REQUEST_CODE);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isMissingPermissions() {
+        for (String permission : REQUIRED_PERMISSIONS) {
+            if (ContextCompat.checkSelfPermission(this, permission)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void selectPages(@NonNull CharSequence title, @NonNull Page pages[]) {
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setTitle(title);
+        }
+
+        mTabLayout.setVisibility(pages.length <= 1 ? View.GONE : View.VISIBLE);
+        mTabLayout.setTabMode(pages.length <= 4 ? TabLayout.MODE_FIXED : TabLayout.MODE_SCROLLABLE);
+        mActivityPagerAdapter.setPages(pages);
+    }
+
+    private static class ActivityPagerAdapter extends FragmentPagerAdapter {
+        private Page mPages[];
+
+        private ActivityPagerAdapter(@NonNull FragmentManager fm) {
+            super(fm);
+        }
+
+        private void setPages(@NonNull Page pages[]) {
+            mPages = pages;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mPages.length;
+        }
+
+        @Override
+        public @NonNull Fragment getItem(int position) {
+            return mPages[position].pageCreator.newInstance();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return mPages[position].id;
+        }
+
+        @Override
+        public int getItemPosition(@NonNull Object object) {
+            return POSITION_NONE;
+        }
+
+        @Override
+        public @NonNull CharSequence getPageTitle(int position) {
+            return mPages[position].title;
+        }
+    }
+
+    private static class Page {
+        private static int sid;
+        private Page(@NonNull PageCreator pageCreator, @NonNull String title) {
+            this.id = sid++;
+            this.pageCreator = pageCreator;
+            this.title = title;
+        }
+
+        private final int id;
+        private final PageCreator pageCreator;
+        private final String title;
+    }
+
+    @FunctionalInterface
+    private interface PageCreator {
+        @NonNull Fragment newInstance();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/SeriesDetailsActivity.java b/apps/Pump/java/com/android/pump/activity/SeriesDetailsActivity.java
new file mode 100644
index 0000000..81effe6
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/SeriesDetailsActivity.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.pump.R;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Series;
+import com.android.pump.util.Globals;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class SeriesDetailsActivity extends AppCompatActivity {
+    private Series mSeries;
+
+    public static void start(@NonNull Context context, @NonNull Series series) {
+        Intent intent = new Intent(context, SeriesDetailsActivity.class);
+        // TODO Pass URI instead
+        intent.putExtra("title", series.getTitle()); // TODO Add constant key
+        if (series.hasYear()) {
+            intent.putExtra("year", series.getYear()); // TODO Add constant key
+        }
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_series_details);
+
+        handleIntent();
+    }
+
+    @Override
+    protected void onNewIntent(@Nullable Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+
+        handleIntent();
+    }
+
+    private void handleIntent() {
+        Intent intent = getIntent();
+        Bundle extras = intent != null ? intent.getExtras() : null;
+        if (extras != null) {
+            String title = extras.getString("title");
+            int year = extras.getInt("year", Integer.MIN_VALUE);
+
+            MediaDb mediaDb = Globals.getMediaDb(this);
+            if (year > 0) {
+                mSeries = mediaDb.getSeriesById(title, year);
+            } else {
+                mSeries = mediaDb.getSeriesById(title);
+            }
+        } else {
+            mSeries = null;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/activity/VideoPlayerActivity.java b/apps/Pump/java/com/android/pump/activity/VideoPlayerActivity.java
new file mode 100644
index 0000000..5884dda
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/activity/VideoPlayerActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.activity;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.android.pump.db.Video;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.AppCompatActivity;
+
+@UiThread
+public class VideoPlayerActivity extends AppCompatActivity {
+    public static void start(@NonNull Context context, @NonNull Video video) {
+        // TODO Find a better URI (video.getUri()?)
+        Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                video.getId());
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        intent.setDataAndTypeAndNormalize(uri, video.getMimeType());
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/app/GlobalsApplication.java b/apps/Pump/java/com/android/pump/app/GlobalsApplication.java
new file mode 100644
index 0000000..865b274
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/app/GlobalsApplication.java
@@ -0,0 +1,69 @@
+package com.android.pump.app;
+
+import android.app.Application;
+
+import com.android.pump.concurrent.Executors;
+import com.android.pump.db.DataProvider;
+import com.android.pump.db.MediaDb;
+import com.android.pump.provider.OmdbApi;
+import com.android.pump.ui.CustomRecycledViewPool;
+import com.android.pump.util.Globals;
+import com.android.pump.util.ImageLoader;
+
+import java.util.concurrent.Executor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
+
+@UiThread
+public abstract class GlobalsApplication extends Application implements Globals.Provider {
+    private Executor mExecutor;
+    private ImageLoader mImageLoader;
+    private RecycledViewPool mRecycledViewPool;
+    private MediaDb mMediaDb;
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        // TODO implement
+    }
+
+    @Override
+    public @NonNull ImageLoader getImageLoader() {
+        if (mImageLoader == null) {
+            mImageLoader = new ImageLoader(getExecutor());
+        }
+        return mImageLoader;
+    }
+
+    @Override
+    public @NonNull RecycledViewPool getRecycledViewPool() {
+        if (mRecycledViewPool == null) {
+            mRecycledViewPool = new CustomRecycledViewPool();
+        }
+        return mRecycledViewPool;
+    }
+
+    @Override
+    public @NonNull MediaDb getMediaDb() {
+        if (mMediaDb == null) {
+            mMediaDb = new MediaDb(getContentResolver(), getDataProvider(), getExecutor());
+            // TODO When can we release mMediaDb?
+        }
+        return mMediaDb;
+    }
+
+    private @NonNull Executor getExecutor() {
+        if (mExecutor == null) {
+            // TODO Adjust pool size
+            mExecutor = Executors.newFixedUniqueThreadPool(
+                    Runtime.getRuntime().availableProcessors() * 2 + 1);
+        }
+        return mExecutor;
+    }
+
+    private @NonNull DataProvider getDataProvider() {
+        return OmdbApi.getInstance();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/app/PumpApplication.java b/apps/Pump/java/com/android/pump/app/PumpApplication.java
new file mode 100644
index 0000000..e36eaa8
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/app/PumpApplication.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.app;
+
+import android.os.Handler;
+import android.os.StrictMode;
+
+import com.android.pump.BuildConfig;
+import com.android.pump.util.Clog;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+
+@MainThread
+public class PumpApplication extends GlobalsApplication implements Thread.UncaughtExceptionHandler {
+    private static final String TAG = Clog.tag(PumpApplication.class);
+
+    private final Thread.UncaughtExceptionHandler mSystemUncaughtExceptionHandler =
+            Thread.getDefaultUncaughtExceptionHandler();
+
+    public PumpApplication() {
+        Thread.setDefaultUncaughtExceptionHandler(this);
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        if (BuildConfig.DEBUG) {
+            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                    .detectAll()
+                    .penaltyLog()
+                    .penaltyFlashScreen()
+                    .penaltyDialog()
+                    .penaltyDeath()
+                    .build());
+            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+                    .detectAll()
+                    .penaltyLog()
+                    .penaltyDeath()
+                    .build());
+        }
+    }
+
+    @Override
+    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
+        if (getMainLooper().getThread() != t) {
+            Clog.e(TAG, "Uncaught exception in background thread " + t, e);
+            new Handler(getMainLooper()).post(() ->
+                    mSystemUncaughtExceptionHandler.uncaughtException(t, e));
+        } else {
+            mSystemUncaughtExceptionHandler.uncaughtException(t, e);
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/DirectExecutor.java b/apps/Pump/java/com/android/pump/concurrent/DirectExecutor.java
new file mode 100644
index 0000000..089960c
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/DirectExecutor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import java.util.concurrent.Executor;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class DirectExecutor {
+    private static final Executor EXECUTOR = Runnable::run;
+
+    private DirectExecutor() { }
+
+    public static @NonNull Executor get() {
+        return EXECUTOR;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/Executors.java b/apps/Pump/java/com/android/pump/concurrent/Executors.java
new file mode 100644
index 0000000..db57ce2
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/Executors.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class Executors {
+    private Executors() { }
+
+    private static final Executor DIRECT_EXECUTOR = Runnable::run;
+    private static final Executor MAIN_THREAD_EXECUTOR = new Executor() {
+        private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+        @Override
+        public void execute(@NonNull Runnable command) {
+            if (mHandler.getLooper() != Looper.myLooper()) {
+                mHandler.post(command);
+            } else {
+                command.run();
+            }
+        }
+    };
+    private static final Executor UI_THREAD_EXECUTOR = MAIN_THREAD_EXECUTOR;
+
+    public static @NonNull Executor directExecutor() {
+        return DIRECT_EXECUTOR;
+    }
+
+    public static @NonNull Executor mainThreadExecutor() {
+        return MAIN_THREAD_EXECUTOR;
+    }
+
+    public static @NonNull Executor uiThreadExecutor() {
+        return UI_THREAD_EXECUTOR;
+    }
+
+    public static @NonNull ExecutorService newFixedUniqueThreadPool(int nThreads) {
+        return new UniqueExecutor(nThreads, nThreads, 0, TimeUnit.MILLISECONDS,
+                new LinkedBlockingQueue<>());
+    }
+
+    public static @NonNull ExecutorService newFixedUniqueThreadPool(int nThreads,
+            @NonNull ThreadFactory threadFactory) {
+        return new UniqueExecutor(nThreads, nThreads, 0, TimeUnit.MILLISECONDS,
+                new LinkedBlockingQueue<>(), threadFactory);
+    }
+
+    public static @NonNull ExecutorService newCachedUniqueThreadPool() {
+        return new UniqueExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>());
+    }
+
+    public static @NonNull ExecutorService newCachedUniqueThreadPool(
+            @NonNull ThreadFactory threadFactory) {
+        return new UniqueExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(), threadFactory);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/MainThreadExecutor.java b/apps/Pump/java/com/android/pump/concurrent/MainThreadExecutor.java
new file mode 100644
index 0000000..eb87b87
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/MainThreadExecutor.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class MainThreadExecutor {
+    private static final Executor EXECUTOR = new Executor() {
+        private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+        @Override
+        public void execute(@NonNull Runnable command) {
+            if (mHandler.getLooper() != Looper.myLooper()) {
+                mHandler.post(command);
+            } else {
+                command.run();
+            }
+        }
+    };
+
+    private MainThreadExecutor() { }
+
+    public static @NonNull Executor get() {
+        return EXECUTOR;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/Task.java b/apps/Pump/java/com/android/pump/concurrent/Task.java
new file mode 100644
index 0000000..34e725d
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/Task.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public abstract class Task implements Runnable {
+    private boolean mCancelled;
+
+    public abstract void execute();
+
+    public abstract void merge(@NonNull Task task);
+
+    public abstract void finish();
+
+    @Override
+    public final void run() {
+        if (!mCancelled) {
+            execute();
+        }
+    }
+
+    @Override
+    public abstract boolean equals(@Nullable Object obj);
+
+    @Override
+    public abstract int hashCode();
+
+    void cancel() {
+        mCancelled = true;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/TaskExecutor.java b/apps/Pump/java/com/android/pump/concurrent/TaskExecutor.java
new file mode 100644
index 0000000..458e42e
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/TaskExecutor.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class TaskExecutor extends ThreadPoolExecutor {
+    public static @NonNull ExecutorService newFixedThreadPool(int nThreads) {
+        return new TaskExecutor(nThreads, nThreads, 0, TimeUnit.MILLISECONDS, new TaskQueue());
+    }
+
+    public static @NonNull ExecutorService newFixedThreadPool(int nThreads,
+                @NonNull ThreadFactory threadFactory) {
+        return new TaskExecutor(nThreads, nThreads, 0, TimeUnit.MILLISECONDS, new TaskQueue(),
+                threadFactory);
+    }
+
+    public static @NonNull ExecutorService newCachedThreadPool() {
+        return new TaskExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new TaskQueue());
+    }
+
+    public static @NonNull ExecutorService newCachedThreadPool(
+            @NonNull ThreadFactory threadFactory) {
+        return new TaskExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new TaskQueue(),
+                threadFactory);
+    }
+
+    public TaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull TaskQueue workQueue) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
+    }
+
+    public TaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull TaskQueue workQueue,
+            @NonNull ThreadFactory threadFactory) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
+    }
+
+    public TaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull TaskQueue workQueue,
+            @NonNull RejectedExecutionHandler handler) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
+    }
+
+    public TaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull TaskQueue workQueue,
+            @NonNull ThreadFactory threadFactory, @NonNull RejectedExecutionHandler handler) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
+                handler);
+    }
+
+    @Override
+    public void execute(@NonNull Runnable command) {
+        if (!(command instanceof Task)) {
+            throw new IllegalArgumentException("The Runnable must be a Task");
+        }
+        super.execute(command);
+    }
+
+    @Override
+    protected void beforeExecute(@NonNull Thread t, @NonNull Runnable r) {
+        getQueue().prepare((Task) r);
+        super.beforeExecute(t, r);
+    }
+
+    @Override
+    protected void afterExecute(@NonNull Runnable r, @Nullable Throwable t) {
+        super.afterExecute(r, t);
+        getQueue().finish((Task) r);
+    }
+
+    @Override
+    public @NonNull TaskQueue getQueue() {
+        return (TaskQueue) super.getQueue();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/TaskQueue.java b/apps/Pump/java/com/android/pump/concurrent/TaskQueue.java
new file mode 100644
index 0000000..dc0d93f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/TaskQueue.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import com.android.pump.util.Clog;
+
+import java.util.AbstractQueue;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class TaskQueue extends AbstractQueue<Runnable> implements BlockingQueue<Runnable> {
+    private static final String TAG = Clog.tag(TaskQueue.class);
+
+    private static final int NANOS_PER_MILLI = 1000000;
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final List mQueue = new List();
+    @GuardedBy("mLock")
+    private final List mRunning = new List();
+
+    @Override
+    public @NonNull Iterator<Runnable> iterator() {
+        Clog.i(TAG, "iterator()");
+        return new Iterator<Runnable>() {
+            private final Iterator<Runnable> mIterator = mQueue.iterator();
+
+            @Override
+            public boolean hasNext() {
+                synchronized (mLock) {
+                    return mIterator.hasNext();
+                }
+            }
+
+            @Override
+            public @Nullable Runnable next() {
+                synchronized (mLock) {
+                    return mIterator.next();
+                }
+            }
+
+            @Override
+            public void remove() {
+                synchronized (mLock) {
+                    mIterator.remove();
+                }
+            }
+        };
+    }
+
+    @Override
+    public int size() {
+        Clog.i(TAG, "size()");
+        return mQueue.size();
+    }
+
+    @Override
+    public void put(@NonNull Runnable runnable) throws InterruptedException {
+        Clog.i(TAG, "put(" + runnable + ")");
+        offer(runnable);
+    }
+
+    @Override
+    public boolean offer(@NonNull Runnable runnable, long timeout, @NonNull TimeUnit unit)
+            throws InterruptedException {
+        Clog.i(TAG, "offer(" + runnable + ", " + timeout + ", " + unit + ")");
+        return offer(runnable);
+    }
+
+    @Override
+    public @NonNull Runnable take() throws InterruptedException {
+        Clog.i(TAG, "take()");
+        synchronized (mLock) {
+            while (mQueue.size() == 0) {
+                mLock.wait();
+            }
+            Task task = dequeue();
+            if (mQueue.size() > 0) {
+                mLock.notifyAll();
+            }
+            return task;
+        }
+    }
+
+    @Override
+    public @Nullable Runnable poll(long timeout, @NonNull TimeUnit unit)
+            throws InterruptedException {
+        Clog.i(TAG, "poll(" + timeout + ", " + unit + ")");
+        synchronized (mLock) {
+            if (mQueue.size() == 0) {
+                wait(timeout, unit);
+                if (mQueue.size() == 0) {
+                    return null;
+                }
+            }
+            Task task = dequeue();
+            if (mQueue.size() > 0) {
+                mLock.notifyAll();
+            }
+            return task;
+        }
+    }
+
+    @Override
+    public int remainingCapacity() {
+        Clog.i(TAG, "remainingCapacity()");
+        return Integer.MAX_VALUE;
+    }
+
+    @Override
+    public int drainTo(@NonNull Collection<? super Runnable> c) {
+        Clog.i(TAG, "drainTo(" + c + ")");
+        return drainTo(c, Integer.MAX_VALUE);
+    }
+
+    @Override
+    public int drainTo(@NonNull Collection<? super Runnable> c, int maxElements) {
+        Clog.i(TAG, "drainTo(" + c + ", " + maxElements + ")");
+        if (c == this) {
+            throw new IllegalArgumentException("Queue can't drain itself");
+        }
+        if (maxElements <= 0) {
+            return 0;
+        }
+        synchronized (mLock) {
+            /*
+            int n = Math.min(mCount, maxElements);
+            Node node = mHead;
+            for (int i = 0; i < n; ++i) {
+                c.add(node.task);
+                node = node.next;
+            }
+            mHead = node;
+            if (mHead == null) {
+                mTail = null;
+            }
+            mCount -= n;
+            mModificationId++;
+            return n;
+            */
+            throw new IllegalStateException("Not yet implemented");
+        }
+    }
+
+    @Override
+    public boolean offer(@NonNull Runnable runnable) {
+        Clog.i(TAG, "offer(" + runnable + ")");
+        if (!(runnable instanceof Task)) {
+            throw new IllegalArgumentException("The Runnable must be a Task");
+        }
+        synchronized (mLock) {
+            boolean notify = mQueue.size() == 0;
+            enqueue((Task) runnable);
+            if (notify) {
+                mLock.notifyAll();
+            }
+            return true;
+        }
+    }
+
+    @Override
+    public @Nullable Runnable poll() {
+        Clog.i(TAG, "poll()");
+        synchronized (mLock) {
+            if (mQueue.size() == 0) {
+                return null;
+            }
+            Task task = dequeue();
+            if (mQueue.size() > 0) {
+                mLock.notifyAll();
+            }
+            return task;
+        }
+    }
+
+    @Override
+    public @Nullable Runnable peek() {
+        Clog.i(TAG, "peek()");
+        synchronized (mLock) {
+            return mQueue.peek();
+        }
+    }
+
+    @Override
+    public boolean remove(@Nullable Object o) {
+        Clog.i(TAG, "remove(" + o + ")");
+        if (!(o instanceof Task)) {
+            return false;
+        }
+        synchronized (mLock) {
+            return mQueue.remove((Task) o) != null;
+        }
+    }
+
+    @Override
+    public boolean contains(@Nullable Object o) {
+        Clog.i(TAG, "contains(" + o + ")");
+        if (!(o instanceof Task)) {
+            return false;
+        }
+        synchronized (mLock) {
+            return mQueue.find((Task) o) != null;
+        }
+    }
+
+    @Override
+    public @NonNull Object[] toArray() {
+        Clog.i(TAG, "toArray()");
+        synchronized (mLock) {
+            /*
+            Object[] a = new Object[mCount];
+            int i = 0;
+            for (Node node = mHead; node != null; node = node.next) {
+                a[i++] = node.task;
+            }
+            return a;
+            */
+            throw new IllegalStateException("Not yet implemented");
+        }
+    }
+
+    @Override
+    public @NonNull <T> T[] toArray(@NonNull T[] a) {
+        Clog.i(TAG, "toArray(" + a + ")");
+        synchronized (mLock) {
+            /*
+            if (a.length < mCount) {
+                a = (T[]) Array.newInstance(a.getClass().getComponentType(), mCount);
+            }
+            int i = 0;
+            for (Node node = mHead; node != null; node = node.next) {
+                a[i++] = (T) node.task;
+            }
+            if (a.length > i) {
+                a[i] = null;
+            }
+            return a;
+            */
+            throw new IllegalStateException("Not yet implemented");
+        }
+    }
+
+    @Override
+    public void clear() {
+        Clog.i(TAG, "clear()");
+        synchronized (mLock) {
+            mQueue.clear();
+        }
+    }
+
+    void prepare(@NonNull Task task) {
+        Clog.i(TAG, "prepare(" + task + ")");
+        synchronized (mLock) {
+            Task runningTask = mRunning.find(task);
+            if (runningTask != null) {
+                if (runningTask != task) {
+                    runningTask.merge(task); // TODO find another solution
+                    task.cancel();
+                }
+            } else {
+                Task queuedTask = mQueue.remove(task);
+                if (queuedTask != null) {
+                    task.merge(queuedTask); // TODO find another solution
+                }
+                mRunning.put(task);
+            }
+        }
+    }
+
+    void finish(@NonNull Task task) {
+        Clog.i(TAG, "finished(" + task + ")");
+        synchronized (mLock) {
+            Task removed = mRunning.remove(task);
+            if (removed != task) {
+                throw new IllegalStateException("Failed to find running task " + task +
+                        " found " + removed);
+            }
+        }
+        task.finish();
+    }
+
+    @GuardedBy("mLock")
+    private void enqueue(@NonNull Task task) {
+        Clog.i(TAG, "enqueue(" + task + ")");
+        Task runningTask = mRunning.find(task);
+        if (runningTask != null) {
+            runningTask.merge(task); // TODO find another solution
+            return;
+        }
+        Task queuedTask = mQueue.find(task);
+        if (queuedTask != null) {
+            queuedTask.merge(task); // TODO find another solution
+            return;
+        }
+        mQueue.put(task);
+    }
+
+    @GuardedBy("mLock")
+    private @NonNull Task dequeue() {
+        Clog.i(TAG, "dequeue()");
+        // TODO Reuse the node just dequeued
+        Task task = mQueue.get();
+        mRunning.put(task);
+        return task;
+    }
+
+    @GuardedBy("mLock")
+    private void wait(long timeout, @NonNull TimeUnit unit) throws InterruptedException {
+        long duration = unit.toNanos(timeout);
+        long start = System.nanoTime();
+        for (;;) {
+            mLock.wait(duration / NANOS_PER_MILLI, (int) (duration % NANOS_PER_MILLI));
+            long now = System.nanoTime();
+            long elapsed = now - start;
+            if (elapsed >= duration) {
+                break;
+            }
+            duration -= elapsed;
+            start = now;
+        }
+    }
+
+    private static class List {
+        private int mModificationId;
+
+        private int mSize;
+        private Node mHead;
+
+        private int size() {
+            return mSize;
+        }
+
+        private void clear() {
+            mModificationId++;
+            mSize = 0;
+            mHead = null;
+        }
+
+        private @Nullable Task peek() {
+            return mHead == null ? null : mHead.task;
+        }
+
+        private void put(@NonNull Task task) {
+            mModificationId++;
+            mSize++;
+            mHead = new Node(task, mHead);
+        }
+
+        private @NonNull Task get() {
+            mModificationId++;
+            mSize--;
+            Node node = mHead;
+            mHead = node.next;
+            return node.task;
+        }
+
+        private @Nullable Task find(@NonNull Task task) {
+            for (Node node = mHead; node != null; node = node.next) {
+                if (task.equals(node.task)) {
+                    return node.task;
+                }
+            }
+            return null;
+        }
+
+        private @Nullable Task remove(@NonNull Task task) {
+            for (Node node = mHead, prev = null; node != null; node = (prev = node).next) {
+                if (task.equals(node.task)) {
+                    mModificationId++;
+                    mSize--;
+                    if (prev == null) {
+                        mHead = node.next;
+                    } else {
+                        prev.next = node.next;
+                    }
+                    return node.task;
+                }
+            }
+            return null;
+        }
+
+        private @NonNull Iterator<Runnable> iterator() {
+            return new Iterator<Runnable>() {
+                private int mExpectedModificationId;
+
+                private Node mPrev;
+                private Node mCurrent;
+                private Node mNext = mHead;
+
+                @Override
+                public boolean hasNext() {
+                    return mNext != null;
+                }
+
+                @Override
+                public @NonNull Runnable next() {
+                    if (mModificationId != mExpectedModificationId) {
+                        throw new ConcurrentModificationException();
+                    }
+                    if (mNext == null) {
+                        throw new NoSuchElementException();
+                    }
+                    mPrev = mCurrent;
+                    mCurrent = mNext;
+                    mNext = mNext.next;
+                    return mCurrent.task;
+                }
+
+                @Override
+                public void remove() {
+                    if (mModificationId != mExpectedModificationId) {
+                        throw new ConcurrentModificationException();
+                    }
+                    if (mCurrent == mPrev) {
+                        throw new IllegalStateException();
+                    }
+                    mModificationId++;
+                    mSize--;
+                    if (mPrev == null) {
+                        mHead = mNext;
+                    } else {
+                        mPrev.next = mNext;
+                    }
+                    mCurrent = mPrev;
+                    mExpectedModificationId = mModificationId;
+                }
+            };
+        }
+
+        private static class Node {
+            private final Task task;
+            private Node next;
+
+            private Node(@NonNull Task task, @Nullable Node next) {
+                this.task = task;
+                this.next = next;
+            }
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/UiThreadExecutor.java b/apps/Pump/java/com/android/pump/concurrent/UiThreadExecutor.java
new file mode 100644
index 0000000..2b7e320
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/UiThreadExecutor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import java.util.concurrent.Executor;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class UiThreadExecutor {
+    private UiThreadExecutor() { }
+
+    public static @NonNull Executor get() {
+        return MainThreadExecutor.get();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/UniqueExecutor.java b/apps/Pump/java/com/android/pump/concurrent/UniqueExecutor.java
new file mode 100644
index 0000000..eed3143
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/UniqueExecutor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class UniqueExecutor extends ThreadPoolExecutor {
+    private final Collection<Runnable> mRunning = Collections.synchronizedSet(new HashSet<>());
+
+    public UniqueExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull BlockingQueue<Runnable> workQueue) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
+    }
+
+    public UniqueExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull BlockingQueue<Runnable> workQueue,
+            @NonNull ThreadFactory threadFactory) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
+    }
+
+    public UniqueExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull BlockingQueue<Runnable> workQueue,
+            @NonNull RejectedExecutionHandler handler) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
+    }
+
+    public UniqueExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
+            @NonNull TimeUnit unit, @NonNull BlockingQueue<Runnable> workQueue,
+            @NonNull ThreadFactory threadFactory, @NonNull RejectedExecutionHandler handler) {
+        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
+                handler);
+    }
+
+    @Override
+    public void execute(@NonNull Runnable command) {
+        if (!mRunning.add(command)) {
+            return;
+        }
+        super.execute(command);
+    }
+
+    @Override
+    protected void afterExecute(@NonNull Runnable r, @Nullable Throwable t) {
+        super.afterExecute(r, t);
+        mRunning.remove(r);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/concurrent/UriTask.java b/apps/Pump/java/com/android/pump/concurrent/UriTask.java
new file mode 100644
index 0000000..495d19b
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/concurrent/UriTask.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.concurrent;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public abstract class UriTask extends Task {
+    public abstract @NonNull Uri getUri();
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof UriTask && getUri().equals(((UriTask) obj).getUri());
+    }
+
+    @Override
+    public final int hashCode() {
+        return getUri().hashCode();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Album.java b/apps/Pump/java/com/android/pump/db/Album.java
new file mode 100644
index 0000000..1a0797f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Album.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Album {
+    private final long mId;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mTitle;
+    private Uri mAlbumArtUri;
+    private Artist mArtist;
+    private boolean mLoaded;
+
+    Album(long id) {
+        mId = id;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @Nullable String getTitle() {
+        return mTitle;
+    }
+
+    public @Nullable Uri getAlbumArtUri() {
+        return mAlbumArtUri;
+    }
+
+    public @Nullable Artist getArtist() {
+        return mArtist;
+    }
+
+    boolean setTitle(@NonNull String title) {
+        if (title.equals(mTitle)) {
+            return false;
+        }
+        mTitle = title;
+        return true;
+    }
+
+    boolean setAlbumArtUri(@NonNull Uri albumArtUri) {
+        if (albumArtUri.equals(mAlbumArtUri)) {
+            return false;
+        }
+        mAlbumArtUri = albumArtUri;
+        return true;
+    }
+
+    boolean setArtist(@NonNull Artist artist) {
+        if (artist.equals(mArtist)) {
+            return false;
+        }
+        mArtist = artist;
+        return true;
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Album && mId == ((Album) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Artist.java b/apps/Pump/java/com/android/pump/db/Artist.java
new file mode 100644
index 0000000..3cac63f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Artist.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Artist {
+    private final long mId;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mName;
+    private final List<Album> mAlbums = new ArrayList<>();
+    private boolean mLoaded;
+
+    Artist(long id) {
+        mId = id;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @Nullable String getName() {
+        return mName;
+    }
+
+    public @NonNull List<Album> getAlbums() {
+        return Collections.unmodifiableList(mAlbums);
+    }
+
+    boolean setName(@NonNull String name) {
+        if (name.equals(mName)) {
+            return false;
+        }
+        mName = name;
+        return true;
+    }
+
+    boolean addAlbum(@NonNull Album album) {
+        if (mAlbums.contains(album)) {
+            return false;
+        }
+        return mAlbums.add(album);
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Artist && mId == ((Artist) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Audio.java b/apps/Pump/java/com/android/pump/db/Audio.java
new file mode 100644
index 0000000..e2ee08c
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Audio.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Audio {
+    private final long mId;
+    private final Uri mUri;
+    private final String mMimeType;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mTitle;
+    private Artist mArtist;
+    private Album mAlbum;
+    private boolean mLoaded;
+
+    Audio(long id, @NonNull Uri uri, @NonNull String mimeType) {
+        mId = id;
+        mUri = uri;
+        mMimeType = mimeType;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @NonNull Uri getUri() {
+        return mUri;
+    }
+
+    public @NonNull String getMimeType() {
+        return mMimeType;
+    }
+
+    public @NonNull String getTitle() {
+        if (mTitle != null) {
+            return mTitle;
+        }
+        return mUri.getLastPathSegment();
+    }
+
+    public @Nullable Artist getArtist() {
+        return mArtist;
+    }
+
+    public @Nullable Album getAlbum() {
+        return mAlbum;
+    }
+
+    boolean setTitle(@NonNull String title) {
+        if (title.equals(mTitle)) {
+            return false;
+        }
+        mTitle = title;
+        return true;
+    }
+
+    boolean setArtist(@NonNull Artist artist) {
+        if (artist.equals(mArtist)) {
+            return false;
+        }
+        mArtist = artist;
+        return true;
+    }
+
+    boolean setAlbum(@NonNull Album album) {
+        if (album.equals(mAlbum)) {
+            return false;
+        }
+        mAlbum = album;
+        return true;
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Audio && mId == ((Audio) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/AudioStore.java b/apps/Pump/java/com/android/pump/db/AudioStore.java
new file mode 100644
index 0000000..e867eeb
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/AudioStore.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.pump.util.Clog;
+import com.android.pump.util.Collections;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+class AudioStore extends ContentObserver {
+    private static final String TAG = Clog.tag(AudioStore.class);
+
+    private final ContentResolver mContentResolver;
+    private final ChangeListener mChangeListener;
+    private final MediaProvider mMediaProvider;
+
+    interface ChangeListener {
+        void onAudiosAdded(@NonNull Collection<Audio> audios);
+        void onArtistsAdded(@NonNull Collection<Artist> artists);
+        void onAlbumsAdded(@NonNull Collection<Album> albums);
+        void onGenresAdded(@NonNull Collection<Genre> genres);
+        void onPlaylistsAdded(@NonNull Collection<Playlist> playlists);
+    }
+
+    @AnyThread
+    AudioStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener,
+            @NonNull MediaProvider mediaProvider) {
+        super(null);
+
+        Clog.i(TAG, "AudioStore(" + contentResolver + ", " + changeListener
+                + ", " + mediaProvider + ")");
+        mContentResolver = contentResolver;
+        mChangeListener = changeListener;
+        mMediaProvider = mediaProvider;
+
+        // TODO Do we need content observer for other content uris? (E.g. album, artist)
+        mContentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                true, this);
+
+        // TODO When to call unregisterContentObserver?
+        // mContentResolver.unregisterContentObserver(this);
+    }
+
+    void load() {
+        Clog.i(TAG, "load()");
+        ArrayList<Artist> artists = new ArrayList<>();
+        ArrayList<Album> albums = new ArrayList<>();
+        ArrayList<Audio> audios = new ArrayList<>();
+        ArrayList<Playlist> playlists = new ArrayList<>();
+        ArrayList<Genre> genres = new ArrayList<>();
+
+        // #1 Load artists
+        {
+            Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Audio.Artists._ID
+            };
+            String sortOrder = MediaStore.Audio.Artists._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID);
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+
+                        Artist artist = new Artist(id);
+                        artists.add(artist);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        // #2 Load albums and connect each to artist
+        {
+            Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Audio.Albums._ID,
+                MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID
+            };
+            String sortOrder = MediaStore.Audio.Albums._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID);
+                    int artistIdColumn = cursor.getColumnIndexOrThrow(
+                            MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+
+                        Album album = new Album(id);
+                        albums.add(album);
+
+                        if (!cursor.isNull(artistIdColumn)) {
+                            long artistId = cursor.getLong(artistIdColumn);
+
+                            Artist artist = Collections.find(artists, artistId, Artist::getId);
+                            album.setArtist(artist);
+                        }
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        // #3 Load songs and connect each to album and artist
+        {
+            Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Audio.Media._ID,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.MIME_TYPE,
+                MediaStore.Audio.Media.ARTIST_ID,
+                MediaStore.Audio.Media.ALBUM_ID
+            };
+            String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
+            String sortOrder = MediaStore.Audio.Media._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, selection, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+                    int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
+                    int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
+                    int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID);
+                    int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID);
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+                        String data = cursor.getString(dataColumn);
+                        String mimeType = cursor.getString(mimeTypeColumn);
+
+                        Uri uri = Uri.fromFile(new File(data));
+                        Audio audio = new Audio(id, uri, mimeType);
+                        audios.add(audio);
+
+                        if (!cursor.isNull(artistIdColumn)) {
+                            long artistId = cursor.getLong(artistIdColumn);
+
+                            Artist artist = Collections.find(artists, artistId, Artist::getId);
+                            audio.setArtist(artist);
+                        }
+                        if (!cursor.isNull(albumIdColumn)) {
+                            long albumId = cursor.getLong(albumIdColumn);
+
+                            Album album = Collections.find(albums, albumId, Album::getId);
+                            audio.setAlbum(album);
+                        }
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        // #4 Load playlists (optional?)
+        {
+            Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Audio.Playlists._ID
+            };
+            String sortOrder = MediaStore.Audio.Playlists._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID);
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+
+                        Playlist playlist = new Playlist(id);
+                        playlists.add(playlist);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        // #5 Load genres (optional?)
+        {
+            Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Audio.Genres._ID
+            };
+            String sortOrder = MediaStore.Audio.Genres._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID);
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+
+                        Genre genre = new Genre(id);
+                        genres.add(genre);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        mChangeListener.onAudiosAdded(audios);
+        mChangeListener.onArtistsAdded(artists);
+        mChangeListener.onAlbumsAdded(albums);
+        mChangeListener.onGenresAdded(genres);
+        mChangeListener.onPlaylistsAdded(playlists);
+    }
+
+    boolean loadData(@NonNull Audio audio) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+        String[] projection = {
+            MediaStore.Audio.Media.TITLE,
+            MediaStore.Audio.Media.ARTIST_ID,
+            MediaStore.Audio.Media.ALBUM_ID
+        };
+        String selection = MediaStore.Audio.Media._ID + " = ?";
+        String[] selectionArgs = { Long.toString(audio.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
+                int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID);
+                int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID);
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(titleColumn)) {
+                        String title = cursor.getString(titleColumn);
+                        updated |= audio.setTitle(title);
+                    }
+                    if (!cursor.isNull(artistIdColumn)) {
+                        long artistId = cursor.getLong(artistIdColumn);
+                        Artist artist = mMediaProvider.getArtistById(artistId);
+                        updated |= audio.setArtist(artist);
+                        updated |= loadData(artist); // TODO Load separate from audio
+                    }
+                    if (!cursor.isNull(albumIdColumn)) {
+                        long albumId = cursor.getLong(albumIdColumn);
+                        Album album = mMediaProvider.getAlbumById(albumId);
+                        updated |= audio.setAlbum(album);
+                        updated |= loadData(album); // TODO Load separate from audio
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return updated;
+    }
+
+    boolean loadData(@NonNull Artist artist) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
+        String[] projection = { MediaStore.Audio.Artists.ARTIST };
+        String selection = MediaStore.Audio.Artists._ID + " = ?";
+        String[] selectionArgs = { Long.toString(artist.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST);
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(artistColumn)) {
+                        String name = cursor.getString(artistColumn);
+                        updated |= artist.setName(name);
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        updated |= loadAlbums(artist); // TODO Load separate from artist
+
+        return updated;
+    }
+
+    boolean loadData(@NonNull Album album) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
+        String[] projection = {
+            MediaStore.Audio.Albums.ALBUM_ART,
+            MediaStore.Audio.Albums.ALBUM,
+            MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID
+        };
+        String selection = MediaStore.Audio.Albums._ID + " = ?";
+        String[] selectionArgs = { Long.toString(album.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int albumArtColumn = cursor.getColumnIndexOrThrow(
+                        MediaStore.Audio.Albums.ALBUM_ART);
+                int albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
+                int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(albumColumn)) {
+                        String albumTitle = cursor.getString(albumColumn);
+                        updated |= album.setTitle(albumTitle);
+                    }
+                    if (!cursor.isNull(albumArtColumn)) {
+                        Uri albumArtUri = Uri.fromFile(new File(cursor.getString(albumArtColumn)));
+                        updated |= album.setAlbumArtUri(albumArtUri);
+                    }
+                    if (!cursor.isNull(artistIdColumn)) {
+                        long artistId = cursor.getLong(artistIdColumn);
+                        Artist artist = mMediaProvider.getArtistById(artistId);
+                        updated |= album.setArtist(artist);
+                        updated |= loadData(artist); // TODO Load separate from album
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return updated;
+    }
+
+    boolean loadData(@NonNull Genre genre) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
+        String[] projection = { MediaStore.Audio.Genres.NAME };
+        String selection = MediaStore.Audio.Genres._ID + " = ?";
+        String[] selectionArgs = { Long.toString(genre.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME);
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(nameColumn)) {
+                        String name = cursor.getString(nameColumn);
+                        updated |= genre.setName(name);
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        updated |= loadAudios(genre); // TODO Load separate from genre
+
+        return updated;
+    }
+
+    boolean loadData(@NonNull Playlist playlist) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
+        String[] projection = { MediaStore.Audio.Playlists.NAME };
+        String selection = MediaStore.Audio.Playlists._ID + " = ?";
+        String[] selectionArgs = { Long.toString(playlist.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME);
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(nameColumn)) {
+                        String name = cursor.getString(nameColumn);
+                        updated |= playlist.setName(name);
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        updated |= loadAudios(playlist); // TODO Load separate from playlist
+
+        return updated;
+    }
+
+    boolean loadAlbums(@NonNull Artist artist) {
+        boolean updated = false;
+
+        // TODO Remove hardcoded value
+        Uri contentUri = MediaStore.Audio.Artists.Albums.getContentUri("external", artist.getId());
+        String[] projection = {
+            MediaStore.Audio.Media._ID // TODO MediaStore.Audio.Artists.Albums.ALBUM_ID
+        };
+        Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
+        if (cursor != null) {
+            try {
+                int albumIdColumn = cursor.getColumnIndexOrThrow(
+                        MediaStore.Audio.Media._ID); // TODO MediaStore.Audio.Artists.Albums.ALBUM_ID
+
+                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                    long albumId = cursor.getLong(albumIdColumn);
+                    Album album = mMediaProvider.getAlbumById(albumId);
+                    updated |= artist.addAlbum(album);
+                    //updated |= loadData(album); // TODO Load separate from artist
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return updated;
+    }
+
+    boolean loadAudios(@NonNull Genre genre) {
+        boolean updated = false;
+
+        // TODO Remove hardcoded value
+        Uri contentUri = MediaStore.Audio.Genres.Members.getContentUri("external", genre.getId());
+        String[] projection = { MediaStore.Audio.Genres.Members.AUDIO_ID };
+        Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
+        if (cursor != null) {
+            try {
+                int audioIdColumn = cursor.getColumnIndexOrThrow(
+                        MediaStore.Audio.Genres.Members.AUDIO_ID);
+
+                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                    long audioId = cursor.getLong(audioIdColumn);
+                    Audio audio = mMediaProvider.getAudioById(audioId);
+                    updated |= genre.addAudio(audio);
+                    updated |= loadData(audio); // TODO Load separate from genre
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return updated;
+    }
+
+    boolean loadAudios(@NonNull Playlist playlist) {
+        boolean updated = false;
+
+        // TODO Remove hardcoded value
+        Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
+                "external", playlist.getId());
+        String[] projection = { MediaStore.Audio.Playlists.Members.AUDIO_ID };
+        Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
+        if (cursor != null) {
+            try {
+                int audioIdColumn = cursor.getColumnIndexOrThrow(
+                        MediaStore.Audio.Playlists.Members.AUDIO_ID);
+
+                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                    long audioId = cursor.getLong(audioIdColumn);
+                    Audio audio = mMediaProvider.getAudioById(audioId);
+                    updated |= playlist.addAudio(audio);
+                    updated |= loadData(audio); // TODO Load separate from playlist
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return updated;
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        Clog.i(TAG, "onChange(" + selfChange + ")");
+        onChange(selfChange, null);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, @Nullable Uri uri) {
+        Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")");
+        // TODO Figure out what changed
+        // onChange(false, content://media)
+        // onChange(false, content://media/external)
+        // onChange(false, content://media/external/audio/media/444)
+        // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0)
+
+        // TODO Notify listener about changes
+        // mChangeListener.xxx();
+    }
+
+    // TODO Remove unused methods
+    private long createPlaylist(@NonNull String name) {
+        Clog.i(TAG, "createPlaylist(" + name + ")");
+        Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
+        ContentValues contentValues = new ContentValues(1);
+        contentValues.put(MediaStore.Audio.Playlists.NAME, name);
+        Uri uri = mContentResolver.insert(contentUri, contentValues);
+        return Long.parseLong(uri.getLastPathSegment());
+    }
+
+    private void addToPlaylist(@NonNull Playlist playlist, @NonNull Audio audio) {
+        Clog.i(TAG, "addToPlaylist(" + playlist + ", " + audio + ")");
+        long base = getLastPlayOrder(playlist);
+
+        // TODO Remove hardcoded value
+        Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
+                "external", playlist.getId());
+        ContentValues contentValues = new ContentValues(2);
+        contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audio.getId());
+        contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + 1);
+        mContentResolver.insert(contentUri, contentValues);
+    }
+
+    private long getLastPlayOrder(@NonNull Playlist playlist) {
+        Clog.i(TAG, "getLastPlayOrder(" + playlist + ")");
+
+        long playOrder = -1;
+
+        // TODO Remove hardcoded value
+        Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
+                "external", playlist.getId());
+        String[] projection = { MediaStore.Audio.Playlists.Members.PLAY_ORDER };
+        String sortOrder = MediaStore.Audio.Playlists.Members.PLAY_ORDER + " DESC LIMIT 1";
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, null, null, sortOrder);
+        if (cursor != null) {
+            try {
+                int playOrderColumn = cursor.getColumnIndexOrThrow(
+                        MediaStore.Audio.Playlists.Members.PLAY_ORDER);
+
+                if (cursor.moveToFirst()) {
+                    playOrder = cursor.getLong(playOrderColumn);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return playOrder;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/DataProvider.java b/apps/Pump/java/com/android/pump/db/DataProvider.java
new file mode 100644
index 0000000..f1fb204
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/DataProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+
+public interface DataProvider {
+    boolean populateMovie(@NonNull Movie movie) throws IOException;
+    boolean populateSeries(@NonNull Series series) throws IOException;
+    boolean populateEpisode(@NonNull Episode episode) throws IOException;
+}
diff --git a/apps/Pump/java/com/android/pump/db/Episode.java b/apps/Pump/java/com/android/pump/db/Episode.java
new file mode 100644
index 0000000..ce96dbf
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Episode.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Episode extends Video {
+    private final Series mSeries;
+    private final int mSeason;
+    private final int mEpisode;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private Uri mThumbnailUri;
+    private Uri mPosterUri;
+    private boolean mLoaded;
+
+    Episode(long id, @NonNull Uri uri, @NonNull String mimeType, @NonNull Series series,
+            int season, int episode) {
+        super(id, uri, mimeType);
+
+        mSeries = series;
+        if (season <= 0 || episode <= 0) {
+            throw new IllegalArgumentException();
+        }
+        mSeason = season;
+        mEpisode = episode;
+    }
+
+    public @NonNull Series getSeries() {
+        return mSeries;
+    }
+
+    public int getSeason() {
+        return mSeason;
+    }
+
+    public int getEpisode() {
+        return mEpisode;
+    }
+
+    public @Nullable Uri getThumbnailUri() {
+        return mThumbnailUri;
+    }
+
+    public boolean setThumbnailUri(@NonNull Uri thumbnailUri) {
+        if (thumbnailUri.equals(mThumbnailUri)) {
+            return false;
+        }
+        mThumbnailUri = thumbnailUri;
+        return true;
+    }
+
+    public @Nullable Uri getPosterUri() {
+        return mPosterUri;
+    }
+
+    public boolean setPosterUri(@NonNull Uri posterUri) {
+        if (posterUri.equals(mPosterUri)) {
+            return false;
+        }
+        mPosterUri = posterUri;
+        return true;
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Genre.java b/apps/Pump/java/com/android/pump/db/Genre.java
new file mode 100644
index 0000000..e75bfa8
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Genre.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Genre {
+    private final long mId;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mName;
+    private final List<Audio> mAudios = new ArrayList<>();
+    private boolean mLoaded;
+
+    Genre(long id) {
+        mId = id;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @Nullable String getName() {
+        return mName;
+    }
+
+    public @NonNull List<Audio> getAudios() {
+        return Collections.unmodifiableList(mAudios);
+    }
+
+    boolean setName(@NonNull String name) {
+        if (name.equals(mName)) {
+            return false;
+        }
+        mName = name;
+        return true;
+    }
+
+    boolean addAudio(@NonNull Audio audio) {
+        if (mAudios.contains(audio)) {
+            return false;
+        }
+        return mAudios.add(audio);
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Genre && mId == ((Genre) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/MediaDb.java b/apps/Pump/java/com/android/pump/db/MediaDb.java
new file mode 100644
index 0000000..848c09b
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/MediaDb.java
@@ -0,0 +1,716 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.Manifest;
+import android.content.ContentResolver;
+
+import com.android.pump.concurrent.Executors;
+import com.android.pump.util.Clog;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.UiThread;
+import androidx.collection.ArraySet;
+
+@UiThread
+public class MediaDb implements MediaProvider {
+    private static final String TAG = Clog.tag(MediaDb.class);
+
+    private final AtomicBoolean mLoaded = new AtomicBoolean();
+
+    private final Executor mExecutor;
+
+    private final AudioStore mAudioStore;
+    private final VideoStore mVideoStore;
+    private final DataProvider mDataProvider;
+
+    private final List<Audio> mAudios = new ArrayList<>();
+    private final List<Artist> mArtists = new ArrayList<>();
+    private final List<Album> mAlbums = new ArrayList<>();
+    private final List<Genre> mGenres = new ArrayList<>();
+    private final List<Playlist> mPlaylists = new ArrayList<>();
+
+    private final List<Movie> mMovies = new ArrayList<>();
+    private final List<Series> mSeries = new ArrayList<>();
+    private final List<Episode> mEpisodes = new ArrayList<>();
+    private final List<Other> mOthers = new ArrayList<>();
+
+    private final Set<UpdateCallback> mAudioUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mArtistUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mAlbumUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mGenreUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mPlaylistUpdateCallbacks = new ArraySet<>();
+
+    private final Set<UpdateCallback> mMovieUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mSeriesUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mEpisodeUpdateCallbacks = new ArraySet<>();
+    private final Set<UpdateCallback> mOtherUpdateCallbacks = new ArraySet<>();
+
+    public interface UpdateCallback {
+        void onItemsInserted(int index, int count);
+        void onItemsUpdated(int index, int count);
+        void onItemsRemoved(int index, int count);
+    }
+
+    public MediaDb(@NonNull ContentResolver contentResolver, @NonNull DataProvider dataProvider,
+            @NonNull Executor executor) {
+        Clog.i(TAG, "MediaDb(" + contentResolver + ", " + dataProvider + ", " + executor + ")");
+        mDataProvider = dataProvider;
+        mExecutor = executor;
+
+        mAudioStore = new AudioStore(contentResolver, new AudioStore.ChangeListener() {
+            @Override
+            public void onAudiosAdded(@NonNull Collection<Audio> audios) {
+                Executors.uiThreadExecutor().execute(() -> addAudios(audios));
+            }
+
+            @Override
+            public void onArtistsAdded(@NonNull Collection<Artist> artists) {
+                Executors.uiThreadExecutor().execute(() -> addArtists(artists));
+            }
+
+            @Override
+            public void onAlbumsAdded(@NonNull Collection<Album> albums) {
+                Executors.uiThreadExecutor().execute(() -> addAlbums(albums));
+            }
+
+            @Override
+            public void onGenresAdded(@NonNull Collection<Genre> genres) {
+                Executors.uiThreadExecutor().execute(() -> addGenres(genres));
+            }
+
+            @Override
+            public void onPlaylistsAdded(@NonNull Collection<Playlist> playlists) {
+                Executors.uiThreadExecutor().execute(() -> addPlaylists(playlists));
+            }
+        }, this);
+
+        mVideoStore = new VideoStore(contentResolver, new VideoStore.ChangeListener() {
+            @Override
+            public void onMoviesAdded(@NonNull Collection<Movie> movies) {
+                Executors.uiThreadExecutor().execute(() -> addMovies(movies));
+            }
+
+            @Override
+            public void onSeriesAdded(@NonNull Collection<Series> series) {
+                Executors.uiThreadExecutor().execute(() -> addSeries(series));
+            }
+
+            @Override
+            public void onEpisodesAdded(@NonNull Collection<Episode> episodes) {
+                Executors.uiThreadExecutor().execute(() -> addEpisodes(episodes));
+            }
+
+            @Override
+            public void onOthersAdded(@NonNull Collection<Other> others) {
+                Executors.uiThreadExecutor().execute(() -> addOthers(others));
+            }
+        }, this);
+    }
+
+    public void addAudioUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mAudioUpdateCallbacks, callback);
+    }
+
+    public void removeAudioUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mAudioUpdateCallbacks, callback);
+    }
+
+    public void addArtistUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mArtistUpdateCallbacks, callback);
+    }
+
+    public void removeArtistUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mArtistUpdateCallbacks, callback);
+    }
+
+    public void addAlbumUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mAlbumUpdateCallbacks, callback);
+    }
+
+    public void removeAlbumUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mAlbumUpdateCallbacks, callback);
+    }
+
+    public void addGenreUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mGenreUpdateCallbacks, callback);
+    }
+
+    public void removeGenreUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mGenreUpdateCallbacks, callback);
+    }
+
+    public void addPlaylistUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mPlaylistUpdateCallbacks, callback);
+    }
+
+    public void removePlaylistUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mPlaylistUpdateCallbacks, callback);
+    }
+
+    public void addMovieUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mMovieUpdateCallbacks, callback);
+    }
+
+    public void removeMovieUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mMovieUpdateCallbacks, callback);
+    }
+
+    public void addSeriesUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mSeriesUpdateCallbacks, callback);
+    }
+
+    public void removeSeriesUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mSeriesUpdateCallbacks, callback);
+    }
+
+    public void addEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mEpisodeUpdateCallbacks, callback);
+    }
+
+    public void removeEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mEpisodeUpdateCallbacks, callback);
+    }
+
+    public void addOtherUpdateCallback(@NonNull UpdateCallback callback) {
+        addUpdateCallback(mOtherUpdateCallbacks, callback);
+    }
+
+    public void removeOtherUpdateCallback(@NonNull UpdateCallback callback) {
+        removeUpdateCallback(mOtherUpdateCallbacks, callback);
+    }
+
+    @RequiresPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+    public void load() {
+        Clog.i(TAG, "load()");
+        if (mLoaded.getAndSet(true)) {
+            return;
+        }
+
+        mExecutor.execute(mAudioStore::load);
+        mExecutor.execute(mVideoStore::load);
+    }
+
+    public @NonNull List<Audio> getAudios() {
+        return Collections.unmodifiableList(mAudios);
+    }
+    public @NonNull List<Artist> getArtists() {
+        return Collections.unmodifiableList(mArtists);
+    }
+    public @NonNull List<Album> getAlbums() {
+        return Collections.unmodifiableList(mAlbums);
+    }
+    public @NonNull List<Genre> getGenres() {
+        return Collections.unmodifiableList(mGenres);
+    }
+    public @NonNull List<Playlist> getPlaylists() {
+        return Collections.unmodifiableList(mPlaylists);
+    }
+
+    public @NonNull List<Movie> getMovies() {
+        return Collections.unmodifiableList(mMovies);
+    }
+    public @NonNull List<Series> getSeries() {
+        return Collections.unmodifiableList(mSeries);
+    }
+    public @NonNull List<Episode> getEpisodes() {
+        return Collections.unmodifiableList(mEpisodes);
+    }
+    public @NonNull List<Other> getOthers() {
+        return Collections.unmodifiableList(mOthers);
+    }
+
+    public void loadData(@NonNull Audio audio) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (audio.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mAudioStore.loadData(audio);
+
+            audio.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updateAudio(audio));
+            }
+        });
+    }
+
+    public void loadData(@NonNull Artist artist) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (artist.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mAudioStore.loadData(artist);
+
+            artist.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updateArtist(artist));
+            }
+        });
+    }
+
+    public void loadData(@NonNull Album album) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (album.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mAudioStore.loadData(album);
+
+            album.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updateAlbum(album));
+            }
+        });
+    }
+
+    public void loadData(@NonNull Genre genre) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (genre.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mAudioStore.loadData(genre);
+
+            genre.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updateGenre(genre));
+            }
+        });
+    }
+
+    public void loadData(@NonNull Playlist playlist) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (playlist.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mAudioStore.loadData(playlist);
+
+            playlist.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updatePlaylist(playlist));
+            }
+        });
+    }
+
+    // TODO Merge with loadData(episode)/loadData(other)
+    public void loadData(@NonNull Movie movie) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (movie.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            try {
+                boolean updated = mDataProvider.populateMovie(movie);
+
+                updated |= mVideoStore.loadData(movie);
+
+                movie.setLoaded();
+                if (updated) {
+                    Executors.uiThreadExecutor().execute(() -> updateMovie(movie));
+                }
+            } catch (IOException e) {
+                Clog.e(TAG, "Search for " + movie + " failed", e);
+            }
+        });
+    }
+
+    public void loadData(@NonNull Series series) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (series.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            try {
+                boolean updated = mDataProvider.populateSeries(series);
+
+                updated |= mVideoStore.loadData(series);
+
+                series.setLoaded();
+                if (updated) {
+                    Executors.uiThreadExecutor().execute(() -> updateSeries(series));
+                }
+            } catch (IOException e) {
+                Clog.e(TAG, "Search for " + series + " failed", e);
+            }
+        });
+    }
+
+    // TODO Merge with loadData(movie)/loadData(other)
+    public void loadData(@NonNull Episode episode) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (episode.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            try {
+                boolean updated = mDataProvider.populateEpisode(episode);
+
+                updated |= mVideoStore.loadData(episode);
+
+                episode.setLoaded();
+                if (updated) {
+                    Executors.uiThreadExecutor().execute(() -> updateEpisode(episode));
+                }
+            } catch (IOException e) {
+                Clog.e(TAG, "Search for " + episode + " failed", e);
+            }
+        });
+    }
+
+    // TODO Merge with loadData(movie)/loadData(episode)
+    public void loadData(@NonNull Other other) {
+        // TODO ensure no concurrent runs for the same item !!
+        if (other.isLoaded()) return;
+
+        mExecutor.execute(() -> {
+            boolean updated = mVideoStore.loadData(other);
+
+            other.setLoaded();
+            if (updated) {
+                Executors.uiThreadExecutor().execute(() -> updateOther(other));
+            }
+        });
+    }
+
+    @Override
+    public @NonNull Audio getAudioById(long id) {
+        for (Audio audio : mAudios) {
+            if (audio.getId() == id) {
+                return audio;
+            }
+        }
+        throw new IllegalArgumentException("Audio with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Artist getArtistById(long id) {
+        for (Artist artist : mArtists) {
+            if (artist.getId() == id) {
+                return artist;
+            }
+        }
+        throw new IllegalArgumentException("Artist with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Album getAlbumById(long id) {
+        for (Album album : mAlbums) {
+            if (album.getId() == id) {
+                return album;
+            }
+        }
+        throw new IllegalArgumentException("Album with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Genre getGenreById(long id) {
+        for (Genre genre : mGenres) {
+            if (genre.getId() == id) {
+                return genre;
+            }
+        }
+        throw new IllegalArgumentException("Genre with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Playlist getPlaylistById(long id) {
+        for (Playlist playlist : mPlaylists) {
+            if (playlist.getId() == id) {
+                return playlist;
+            }
+        }
+        throw new IllegalArgumentException("Playlist with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Movie getMovieById(long id) {
+        for (Movie movie : mMovies) {
+            if (movie.getId() == id) {
+                return movie;
+            }
+        }
+        throw new IllegalArgumentException("Movie with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Series getSeriesById(@NonNull String title) {
+        for (Series series : mSeries) {
+            if (!series.hasYear() && series.getTitle().equals(title)) {
+                return series;
+            }
+        }
+        throw new IllegalArgumentException("Series '" + title + "' was not found");
+    }
+
+    @Override
+    public @NonNull Series getSeriesById(@NonNull String title, int year) {
+        for (Series series : mSeries) {
+            if (series.hasYear() && series.getTitle().equals(title) && series.getYear() == year) {
+                return series;
+            }
+        }
+        throw new IllegalArgumentException("Series '" + title + "' (" + year + ") was not found");
+    }
+
+    @Override
+    public @NonNull Episode getEpisodeById(long id) {
+        for (Episode episode : mEpisodes) {
+            if (episode.getId() == id) {
+                return episode;
+            }
+        }
+        throw new IllegalArgumentException("Episode with id " + id + " was not found");
+    }
+
+    @Override
+    public @NonNull Other getOtherById(long id) {
+        for (Other other : mOthers) {
+            if (other.getId() == id) {
+                return other;
+            }
+        }
+        throw new IllegalArgumentException("Other with id " + id + " was not found");
+    }
+
+    private void addUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
+            @NonNull UpdateCallback callback) {
+        if (!callbacks.add(callback)) {
+            throw new IllegalArgumentException("Callback " + callback + " already added in " +
+                    callbacks);
+        }
+    }
+
+    private void removeUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
+            @NonNull UpdateCallback callback) {
+        if (!callbacks.remove(callback)) {
+            throw new IllegalArgumentException("Callback " + callback + " not found in " +
+                    callbacks);
+        }
+    }
+
+    private void addAudios(@NonNull Collection<Audio> audios) {
+        int audiosIndex = mAudios.size();
+        int audiosCount = 0;
+
+        mAudios.addAll(audios);
+        audiosCount += audios.size();
+
+        if (audiosCount > 0) {
+            for (UpdateCallback callback : mAudioUpdateCallbacks) {
+                callback.onItemsInserted(audiosIndex, audiosCount);
+            }
+        }
+    }
+
+    private void addArtists(@NonNull Collection<Artist> artists) {
+        int artistsIndex = mArtists.size();
+        int artistsCount = 0;
+
+        mArtists.addAll(artists);
+        artistsCount += artists.size();
+
+        if (artistsCount > 0) {
+            for (UpdateCallback callback : mArtistUpdateCallbacks) {
+                callback.onItemsInserted(artistsIndex, artistsCount);
+            }
+        }
+    }
+
+    private void addAlbums(@NonNull Collection<Album> albums) {
+        int albumsIndex = mAlbums.size();
+        int albumsCount = 0;
+
+        mAlbums.addAll(albums);
+        albumsCount += albums.size();
+
+        if (albumsCount > 0) {
+            for (UpdateCallback callback : mAlbumUpdateCallbacks) {
+                callback.onItemsInserted(albumsIndex, albumsCount);
+            }
+        }
+    }
+
+    private void addGenres(@NonNull Collection<Genre> genres) {
+        int genresIndex = mGenres.size();
+        int genresCount = 0;
+
+        mGenres.addAll(genres);
+        genresCount += genres.size();
+
+        if (genresCount > 0) {
+            for (UpdateCallback callback : mGenreUpdateCallbacks) {
+                callback.onItemsInserted(genresIndex, genresCount);
+            }
+        }
+    }
+
+    private void addPlaylists(@NonNull Collection<Playlist> playlists) {
+        int playlistsIndex = mPlaylists.size();
+        int playlistsCount = 0;
+
+        mPlaylists.addAll(playlists);
+        playlistsCount += playlists.size();
+
+        if (playlistsCount > 0) {
+            for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
+                callback.onItemsInserted(playlistsIndex, playlistsCount);
+            }
+        }
+    }
+
+    private void addMovies(@NonNull Collection<Movie> movies) {
+        int moviesIndex = mMovies.size();
+        int moviesCount = 0;
+
+        mMovies.addAll(movies);
+        moviesCount += movies.size();
+
+        if (moviesCount > 0) {
+            for (UpdateCallback callback : mMovieUpdateCallbacks) {
+                callback.onItemsInserted(moviesIndex, moviesCount);
+            }
+        }
+    }
+
+    private void addSeries(@NonNull Collection<Series> series) {
+        int seriesIndex = mSeries.size();
+        int seriesCount = 0;
+
+        mSeries.addAll(series);
+        seriesCount += series.size();
+
+        if (seriesCount > 0) {
+            for (UpdateCallback callback : mSeriesUpdateCallbacks) {
+                callback.onItemsInserted(seriesIndex, seriesCount);
+            }
+        }
+    }
+
+    private void addEpisodes(@NonNull Collection<Episode> episodes) {
+        int episodesIndex = mEpisodes.size();
+        int episodesCount = 0;
+
+        mEpisodes.addAll(episodes);
+        episodesCount += episodes.size();
+
+        if (episodesCount > 0) {
+            for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
+                callback.onItemsInserted(episodesIndex, episodesCount);
+            }
+        }
+    }
+
+    private void addOthers(@NonNull Collection<Other> others) {
+        int othersIndex = mOthers.size();
+        int othersCount = 0;
+
+        mOthers.addAll(others);
+        othersCount += others.size();
+
+        if (othersCount > 0) {
+            for (UpdateCallback callback : mOtherUpdateCallbacks) {
+                callback.onItemsInserted(othersIndex, othersCount);
+            }
+        }
+    }
+
+    private void updateAudio(@NonNull Audio audio) {
+        int index = mAudios.indexOf(audio);
+        if (index != -1) {
+            for (UpdateCallback callback : mAudioUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateArtist(@NonNull Artist artist) {
+        int index = mArtists.indexOf(artist);
+        if (index != -1) {
+            for (UpdateCallback callback : mArtistUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateAlbum(@NonNull Album album) {
+        int index = mAlbums.indexOf(album);
+        if (index != -1) {
+            for (UpdateCallback callback : mAlbumUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateGenre(@NonNull Genre genre) {
+        int index = mGenres.indexOf(genre);
+        if (index != -1) {
+            for (UpdateCallback callback : mGenreUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updatePlaylist(@NonNull Playlist playlist) {
+        int index = mPlaylists.indexOf(playlist);
+        if (index != -1) {
+            for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateMovie(@NonNull Movie movie) {
+        int index = mMovies.indexOf(movie);
+        if (index != -1) {
+            for (UpdateCallback callback : mMovieUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateSeries(@NonNull Series series) {
+        int index = mSeries.indexOf(series);
+        if (index != -1) {
+            for (UpdateCallback callback : mSeriesUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateEpisode(@NonNull Episode episode) {
+        int index = mEpisodes.indexOf(episode);
+        if (index != -1) {
+            for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+
+    private void updateOther(@NonNull Other other) {
+        int index = mOthers.indexOf(other);
+        if (index != -1) {
+            for (UpdateCallback callback : mOtherUpdateCallbacks) {
+                callback.onItemsUpdated(index, 1);
+            }
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/MediaProvider.java b/apps/Pump/java/com/android/pump/db/MediaProvider.java
new file mode 100644
index 0000000..d6e8e37
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/MediaProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import androidx.annotation.NonNull;
+
+interface MediaProvider {
+    @NonNull Audio getAudioById(long id);
+    @NonNull Artist getArtistById(long id);
+    @NonNull Album getAlbumById(long id);
+    @NonNull Genre getGenreById(long id);
+    @NonNull Playlist getPlaylistById(long id);
+
+    @NonNull Movie getMovieById(long id);
+    @NonNull Series getSeriesById(@NonNull String title);
+    @NonNull Series getSeriesById(@NonNull String title, int year);
+    @NonNull Episode getEpisodeById(long id);
+    @NonNull Other getOtherById(long id);
+}
diff --git a/apps/Pump/java/com/android/pump/db/Movie.java b/apps/Pump/java/com/android/pump/db/Movie.java
new file mode 100644
index 0000000..31b4ae7
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Movie.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Movie extends Video {
+    private final String mTitle;
+    private final int mYear;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private Uri mThumbnailUri;
+    private Uri mPosterUri;
+    private String mSynopsis;
+    private boolean mLoaded;
+
+    Movie(long id, @NonNull Uri uri, @NonNull String mimeType, @NonNull String title) {
+        super(id, uri, mimeType);
+
+        mTitle = title;
+        mYear = Integer.MIN_VALUE;
+    }
+
+    Movie(long id, @NonNull Uri uri, @NonNull String mimeType, @NonNull String title, int year) {
+        super(id, uri, mimeType);
+
+        mTitle = title;
+        if (year <= 0) {
+            throw new IllegalArgumentException();
+        }
+        mYear = year;
+    }
+
+    public @NonNull String getTitle() {
+        return mTitle;
+    }
+
+    public boolean hasYear() {
+        return mYear > 0;
+    }
+
+    public int getYear() {
+        if (!hasYear()) {
+            throw new IllegalStateException();
+        }
+        return mYear;
+    }
+
+    public @Nullable Uri getThumbnailUri() {
+        return mThumbnailUri;
+    }
+
+    public boolean setThumbnailUri(@NonNull Uri thumbnailUri) {
+        if (thumbnailUri.equals(mThumbnailUri)) {
+            return false;
+        }
+        mThumbnailUri = thumbnailUri;
+        return true;
+    }
+
+    public @Nullable Uri getPosterUri() {
+        return mPosterUri;
+    }
+
+    public boolean setPosterUri(@NonNull Uri posterUri) {
+        if (posterUri.equals(mPosterUri)) {
+            return false;
+        }
+        mPosterUri = posterUri;
+        return true;
+    }
+
+    public @Nullable String getSynopsis() {
+        return mSynopsis;
+    }
+
+    public boolean setSynopsis(@NonNull String synopsis) {
+        if (synopsis.equals(mSynopsis)) {
+            return false;
+        }
+        mSynopsis = synopsis;
+        return true;
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Other.java b/apps/Pump/java/com/android/pump/db/Other.java
new file mode 100644
index 0000000..8b46d55
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Other.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Other extends Video {
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mTitle;
+    private long mDuration;
+    private long mDateTaken;
+    private double mLatitude;
+    private double mLongitude;
+    private Uri mThumbnailUri;
+    private boolean mLoaded;
+
+    Other(long id, @NonNull Uri uri, @NonNull String mimeType, @NonNull String title) {
+        super(id, uri, mimeType);
+
+        mTitle = title;
+        mDuration = Long.MIN_VALUE;
+        mDateTaken = Long.MIN_VALUE;
+        mLatitude = Double.NaN;
+        mLongitude = Double.NaN;
+    }
+
+    public @NonNull String getTitle() {
+        return mTitle;
+    }
+
+    public boolean setTitle(@NonNull String title) {
+        if (title.equals(mTitle)) {
+            return false;
+        }
+        mTitle = title;
+        return true;
+    }
+
+    public boolean hasDuration() {
+        return mDuration >= 0;
+    }
+
+    public long getDuration() {
+        if (!hasDuration()) {
+            throw new IllegalStateException();
+        }
+        return mDuration;
+    }
+
+    public boolean setDuration(long duration) {
+        if (duration == mDuration) {
+            return false;
+        }
+        mDuration = duration;
+        return true;
+    }
+
+    public boolean hasDateTaken() {
+        return mDateTaken >= 0;
+    }
+
+    public long getDateTaken() {
+        if (!hasDateTaken()) {
+            throw new IllegalStateException();
+        }
+        return mDateTaken;
+    }
+
+    public boolean setDateTaken(long dateTaken) {
+        if (dateTaken == mDateTaken) {
+            return false;
+        }
+        mDateTaken = dateTaken;
+        return true;
+    }
+
+    public boolean hasLatLong() {
+        return !Double.isNaN(mLatitude) && !Double.isNaN(mLongitude);
+    }
+
+    public boolean setLatLong(double latitude, double longitude) {
+        if (latitude == mLatitude || longitude == mLongitude) {
+            return false;
+        }
+        mLatitude = latitude;
+        mLongitude = longitude;
+        return true;
+    }
+
+    public double getLatitude() {
+        if (!hasLatLong()) {
+            throw new IllegalStateException();
+        }
+        return mLatitude;
+    }
+
+    public double getLongitude() {
+        if (!hasLatLong()) {
+            throw new IllegalStateException();
+        }
+        return mLongitude;
+    }
+
+    public @Nullable Uri getThumbnailUri() {
+        return mThumbnailUri;
+    }
+
+    public boolean setThumbnailUri(@NonNull Uri thumbnailUri) {
+        if (thumbnailUri.equals(mThumbnailUri)) {
+            return false;
+        }
+        mThumbnailUri = thumbnailUri;
+        return true;
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Playlist.java b/apps/Pump/java/com/android/pump/db/Playlist.java
new file mode 100644
index 0000000..8419797
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Playlist.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Playlist {
+    private final long mId;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private String mName;
+    private final List<Audio> mAudios = new ArrayList<>();
+    private boolean mLoaded;
+
+    Playlist(long id) {
+        mId = id;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @Nullable String getName() {
+        return mName;
+    }
+
+    public @NonNull List<Audio> getAudios() {
+        return Collections.unmodifiableList(mAudios);
+    }
+
+    boolean setName(@NonNull String name) {
+        if (name.equals(mName)) {
+            return false;
+        }
+        mName = name;
+        return true;
+    }
+
+    boolean addAudio(@NonNull Audio audio) {
+        if (mAudios.contains(audio)) {
+            return false;
+        }
+        return mAudios.add(audio);
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Playlist && mId == ((Playlist) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Series.java b/apps/Pump/java/com/android/pump/db/Series.java
new file mode 100644
index 0000000..398122c
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Series.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public class Series {
+    private final String mTitle;
+    private final int mYear;
+
+    // TODO Lock mutable fields to ensure consistent updates
+    private Uri mPosterUri;
+    private final List<Episode> mEpisodes = new ArrayList<>();
+    private boolean mLoaded;
+
+    Series(@NonNull String title) {
+        mTitle = title;
+        mYear = Integer.MIN_VALUE;
+    }
+
+    Series(@NonNull String title, int year) {
+        mTitle = title;
+        if (year <= 0) {
+            throw new IllegalArgumentException();
+        }
+        mYear = year;
+    }
+
+    public @NonNull String getTitle() {
+        return mTitle;
+    }
+
+    public boolean hasYear() {
+        return mYear > 0;
+    }
+
+    public int getYear() {
+        if (!hasYear()) {
+            throw new IllegalStateException();
+        }
+        return mYear;
+    }
+
+    public @Nullable Uri getPosterUri() {
+        return mPosterUri;
+    }
+
+    public boolean setPosterUri(@NonNull Uri posterUri) {
+        if (posterUri.equals(mPosterUri)) {
+            return false;
+        }
+        mPosterUri = posterUri;
+        return true;
+    }
+
+    public @NonNull List<Episode> getEpisodes() {
+        return Collections.unmodifiableList(mEpisodes);
+    }
+
+    boolean addEpisode(@NonNull Episode episode) {
+        if (mEpisodes.contains(episode)) {
+            return false;
+        }
+        return mEpisodes.add(episode);
+    }
+
+    boolean isLoaded() {
+        return mLoaded;
+    }
+
+    void setLoaded() {
+        mLoaded = true;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Series && mTitle.equals(((Series) obj).mTitle)
+                && mYear == ((Series) obj).mYear;
+    }
+
+    @Override
+    public final int hashCode() {
+        return mTitle.hashCode() ^ mYear;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/Video.java b/apps/Pump/java/com/android/pump/db/Video.java
new file mode 100644
index 0000000..80bddb8
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/Video.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public abstract class Video {
+    private final long mId;
+    private final Uri mUri;
+    private final String mMimeType;
+
+    Video(long id, @NonNull Uri uri, @NonNull String mimeType) {
+        mId = id;
+        mUri = uri;
+        mMimeType = mimeType;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public @NonNull Uri getUri() {
+        return mUri;
+    }
+
+    public @NonNull String getMimeType() {
+        return mMimeType;
+    }
+
+    @Override
+    public final boolean equals(@Nullable Object obj) {
+        return obj instanceof Video && mId == ((Video) obj).mId;
+    }
+
+    @Override
+    public final int hashCode() {
+        return (int) (mId ^ (mId >>> 32));
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/db/VideoStore.java b/apps/Pump/java/com/android/pump/db/VideoStore.java
new file mode 100644
index 0000000..53b5ad5
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/db/VideoStore.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.db;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.pump.provider.Query;
+import com.android.pump.util.Clog;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+class VideoStore extends ContentObserver {
+    private static final String TAG = Clog.tag(VideoStore.class);
+
+    private final ContentResolver mContentResolver;
+    private final ChangeListener mChangeListener;
+    private final MediaProvider mMediaProvider;
+
+    interface ChangeListener {
+        void onMoviesAdded(@NonNull Collection<Movie> movies);
+        void onSeriesAdded(@NonNull Collection<Series> series);
+        void onEpisodesAdded(@NonNull Collection<Episode> episodes);
+        void onOthersAdded(@NonNull Collection<Other> others);
+    }
+
+    @AnyThread
+    VideoStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener,
+            @NonNull MediaProvider mediaProvider) {
+        super(null);
+
+        Clog.i(TAG, "VideoStore(" + contentResolver + ", " + changeListener
+                + ", " + mediaProvider + ")");
+        mContentResolver = contentResolver;
+        mChangeListener = changeListener;
+        mMediaProvider = mediaProvider;
+
+        // TODO Do we need content observer for other content uris? (E.g. thumbnail)
+        mContentResolver.registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                true, this);
+
+        // TODO When to call unregisterContentObserver?
+        // mContentResolver.unregisterContentObserver(this);
+    }
+
+    void load() {
+        Clog.i(TAG, "load()");
+        Collection<Movie> movies = new ArrayList<>();
+        Collection<Series> series = new ArrayList<>();
+        Collection<Episode> episodes = new ArrayList<>();
+        Collection<Other> others = new ArrayList<>();
+
+        /* TODO get via count instead?
+                Cursor countCursor = mContentResolver.query(CONTENT_URI,
+                new String[] { "count(*) AS count" },
+                null,
+                null,
+                null);
+        countCursor.moveToFirst();
+        int count = countCursor.getInt(0);
+        Clog.i(TAG, "count = " + count);
+        countCursor.close();
+        */
+
+        {
+            Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+            String[] projection = {
+                MediaStore.Video.Media._ID,
+                MediaStore.Video.Media.DATA,
+                MediaStore.Video.Media.MIME_TYPE
+            };
+            String sortOrder = MediaStore.Video.Media._ID;
+            Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
+            if (cursor != null) {
+                try {
+                    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
+                    int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
+                    int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE);
+
+                    for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                        long id = cursor.getLong(idColumn);
+                        String data = cursor.getString(dataColumn);
+                        String mimeType = cursor.getString(mimeTypeColumn);
+
+                        Uri uri = Uri.fromFile(new File(data));
+                        Query query = Query.parse(uri);
+                        if (query.isMovie()) {
+                            Movie movie;
+                            if (query.hasYear()) {
+                                movie = new Movie(id, uri, mimeType, query.getName(), query.getYear());
+                            } else {
+                                movie = new Movie(id, uri, mimeType, query.getName());
+                            }
+                            movies.add(movie);
+                        } else if (query.isEpisode()) {
+                            Series serie = null;
+                            for (Series s : series) {
+                                if (s.getTitle().equals(query.getName())
+                                        && s.hasYear() == query.hasYear()
+                                        && (!s.hasYear() || s.getYear() == query.getYear())) {
+                                    serie = s;
+                                    break;
+                                }
+                            }
+                            if (serie == null) {
+                                if (query.hasYear()) {
+                                    serie = new Series(query.getName(), query.getYear());
+                                } else {
+                                    serie = new Series(query.getName());
+                                }
+                                series.add(serie);
+                            }
+
+                            Episode episode = new Episode(id, uri, mimeType, serie,
+                                    query.getSeason(), query.getEpisode());
+                            episodes.add(episode);
+
+                            serie.addEpisode(episode);
+                        } else {
+                            Other other = new Other(id, uri, mimeType, query.getName());
+                            others.add(other);
+                        }
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        mChangeListener.onMoviesAdded(movies);
+        mChangeListener.onSeriesAdded(series);
+        mChangeListener.onEpisodesAdded(episodes);
+        mChangeListener.onOthersAdded(others);
+    }
+
+    boolean loadData(@NonNull Movie movie) {
+        Uri thumbnailUri = getThumbnailUri(movie.getId());
+        if (thumbnailUri != null) {
+            return movie.setThumbnailUri(thumbnailUri);
+        }
+        return false;
+    }
+
+    boolean loadData(@NonNull Series series) {
+        return false;
+    }
+
+    boolean loadData(@NonNull Episode episode) {
+        Uri thumbnailUri = getThumbnailUri(episode.getId());
+        if (thumbnailUri != null) {
+            return episode.setThumbnailUri(thumbnailUri);
+        }
+        return false;
+    }
+
+    boolean loadData(@NonNull Other other) {
+        boolean updated = false;
+
+        Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+        String[] projection = {
+            MediaStore.Video.Media.TITLE,
+            MediaStore.Video.Media.DURATION,
+            MediaStore.Video.Media.DATE_TAKEN,
+            MediaStore.Video.Media.LATITUDE,
+            MediaStore.Video.Media.LONGITUDE
+        };
+        String selection = MediaStore.Video.Media._ID + " = ?";
+        String[] selectionArgs = { Long.toString(other.getId()) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE);
+                int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
+                int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN);
+                int latitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LATITUDE);
+                int longitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LONGITUDE);
+
+                if (cursor.moveToFirst()) {
+                    if (!cursor.isNull(titleColumn)) {
+                        String title = cursor.getString(titleColumn);
+                        updated |= other.setTitle(title);
+                    }
+                    if (!cursor.isNull(durationColumn)) {
+                        long duration = cursor.getLong(durationColumn);
+                        updated |= other.setDuration(duration);
+                    }
+                    if (!cursor.isNull(dateTakenColumn)) {
+                        long dateTaken = cursor.getLong(dateTakenColumn);
+                        updated |= other.setDateTaken(dateTaken);
+                    }
+                    if (!cursor.isNull(latitudeColumn) && !cursor.isNull(longitudeColumn)) {
+                        double latitude = cursor.getDouble(latitudeColumn);
+                        double longitude = cursor.getDouble(longitudeColumn);
+                        updated |= other.setLatLong(latitude, longitude);
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        Uri thumbnailUri = getThumbnailUri(other.getId());
+        if (thumbnailUri != null) {
+            updated |= other.setThumbnailUri(thumbnailUri);
+        }
+
+        return updated;
+    }
+
+    private @Nullable Uri getThumbnailUri(long id) {
+        int thumbKind = MediaStore.Video.Thumbnails.MINI_KIND;
+
+        // TODO The following line is required to generate thumbnails -- is there a better way?
+        MediaStore.Video.Thumbnails.getThumbnail(mContentResolver, id, thumbKind, null);
+
+        Uri thumbnailUri = null;
+        Uri contentUri = MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI;
+        String[] projection = {
+            MediaStore.Video.Thumbnails.DATA
+        };
+        String selection = MediaStore.Video.Thumbnails.KIND + " = " + thumbKind + " AND " +
+                MediaStore.Video.Thumbnails.VIDEO_ID + " = ?";
+        String[] selectionArgs = { Long.toString(id) };
+        Cursor cursor = mContentResolver.query(
+                contentUri, projection, selection, selectionArgs, null);
+        if (cursor != null) {
+            try {
+                int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Thumbnails.DATA);
+
+                if (cursor.moveToFirst()) {
+                    String data = cursor.getString(dataColumn);
+
+                    thumbnailUri = Uri.fromFile(new File(data));
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+        return thumbnailUri;
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        Clog.i(TAG, "onChange(" + selfChange + ")");
+        onChange(selfChange, null);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, @Nullable Uri uri) {
+        Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")");
+        // TODO Figure out what changed
+        // onChange(false, content://media)
+        // onChange(false, content://media/external)
+        // onChange(false, content://media/external/audio/media/444)
+        // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0)
+
+        // TODO Notify listener about changes
+        // mChangeListener.xxx();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/AlbumFragment.java b/apps/Pump/java/com/android/pump/fragment/AlbumFragment.java
new file mode 100644
index 0000000..07e5bc8
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/AlbumFragment.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.AlbumDetailsActivity;
+import com.android.pump.db.Album;
+import com.android.pump.db.Artist;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class AlbumFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new AlbumFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_album, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_album_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new AlbumAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class AlbumAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Album> mAlbums; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private AlbumAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mAlbums = mMediaDb.getAlbums();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addAlbumUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeAlbumUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new AlbumViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Album album = mAlbums.get(position - 1);
+                mMediaDb.loadData(album); // TODO Where should we call this? In bind()?
+                ((AlbumViewHolder) holder).bind(album);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mAlbums.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mAlbums.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.album;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class AlbumViewHolder extends RecyclerView.ViewHolder {
+        private AlbumViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Album album) {
+            ImageView imageView = itemView.findViewById(R.id.album_image);
+            TextView titleView = itemView.findViewById(R.id.album_title);
+            TextView artistView = itemView.findViewById(R.id.album_artist);
+
+            imageView.setImageURI(album.getAlbumArtUri());
+            titleView.setText(album.getTitle());
+            Artist artist = album.getArtist();
+            artistView.setText(artist == null ? null : artist.getName());
+
+            itemView.setOnClickListener((view) ->
+                    AlbumDetailsActivity.start(view.getContext(), album));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/ArtistFragment.java b/apps/Pump/java/com/android/pump/fragment/ArtistFragment.java
new file mode 100644
index 0000000..81f908d
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/ArtistFragment.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.ArtistDetailsActivity;
+import com.android.pump.db.Album;
+import com.android.pump.db.Artist;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class ArtistFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new ArtistFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_artist, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_artist_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new ArtistAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class ArtistAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Artist> mArtists; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private ArtistAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mArtists = mMediaDb.getArtists();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addArtistUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeArtistUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new ArtistViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Artist artist = mArtists.get(position - 1);
+                mMediaDb.loadData(artist); // TODO Where should we call this? In bind()?
+                ((ArtistViewHolder) holder).bind(artist);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mArtists.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mArtists.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.artist;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class ArtistViewHolder extends RecyclerView.ViewHolder {
+        private ArtistViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Artist artist) {
+            ImageView imageView = itemView.findViewById(R.id.artist_image);
+            TextView nameView = itemView.findViewById(R.id.artist_name);
+
+            // TODO This should be artist head shot rather than album art
+            Uri albumArtUri = null;
+            List<Album> albums = artist.getAlbums();
+            for (Album album : albums) {
+                if (album.getAlbumArtUri() != null) {
+                    albumArtUri = album.getAlbumArtUri();
+                    break;
+                }
+            }
+            imageView.setImageURI(albumArtUri);
+            nameView.setText(artist.getName());
+
+            itemView.setOnClickListener((view) ->
+                    ArtistDetailsActivity.start(view.getContext(), artist));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/AudioFragment.java b/apps/Pump/java/com/android/pump/fragment/AudioFragment.java
new file mode 100644
index 0000000..11e102b
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/AudioFragment.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.AudioPlayerActivity;
+import com.android.pump.db.Album;
+import com.android.pump.db.Artist;
+import com.android.pump.db.Audio;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class AudioFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new AudioFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_audio, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_audio_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new AudioAdapter(requireContext()));
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class AudioAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Audio> mAudios; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private AudioAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mAudios = mMediaDb.getAudios();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addAudioUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeAudioUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new AudioViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Audio audio = mAudios.get(position - 1);
+                mMediaDb.loadData(audio); // TODO Where should we call this? In bind()?
+                ((AudioViewHolder) holder).bind(audio);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mAudios.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mAudios.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.audio;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class AudioViewHolder extends RecyclerView.ViewHolder {
+        private AudioViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Audio audio) {
+            ImageView imageView = itemView.findViewById(R.id.audio_image);
+            TextView titleView = itemView.findViewById(R.id.audio_title);
+            TextView artistView = itemView.findViewById(R.id.audio_artist);
+
+            Album album = audio.getAlbum();
+            imageView.setImageURI(album == null ? null : album.getAlbumArtUri());
+            titleView.setText(audio.getTitle());
+            Artist artist = audio.getArtist();
+            artistView.setText(artist == null ? null : artist.getName());
+
+            itemView.setOnClickListener((view) ->
+                    AudioPlayerActivity.start(view.getContext(), audio));
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/GenreFragment.java b/apps/Pump/java/com/android/pump/fragment/GenreFragment.java
new file mode 100644
index 0000000..227d01f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/GenreFragment.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.GenreDetailsActivity;
+import com.android.pump.db.Genre;
+import com.android.pump.db.MediaDb;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class GenreFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new GenreFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_genre, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_genre_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new GenreAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class GenreAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Genre> mGenres; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private GenreAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mGenres = mMediaDb.getGenres();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addGenreUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeGenreUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new GenreViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Genre genre = mGenres.get(position - 1);
+                mMediaDb.loadData(genre); // TODO Where should we call this? In bind()?
+                ((GenreViewHolder) holder).bind(genre);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mGenres.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mGenres.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.genre;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class GenreViewHolder extends RecyclerView.ViewHolder {
+        private GenreViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Genre genre) {
+            ImageView imageView = itemView.findViewById(R.id.genre_image);
+            TextView textView = itemView.findViewById(R.id.genre_text);
+
+            // TODO imageView.setImageURI(xxx);
+            textView.setText(genre.getName());
+
+            itemView.setOnClickListener((view) ->
+                    GenreDetailsActivity.start(view.getContext(), genre));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/HomeFragment.java b/apps/Pump/java/com/android/pump/fragment/HomeFragment.java
new file mode 100644
index 0000000..7af7efc
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/HomeFragment.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.pump.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.Fragment;
+
+@UiThread
+public class HomeFragment extends Fragment {
+    public static @NonNull Fragment newInstance() {
+        return new HomeFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_home, container, false);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/MovieFragment.java b/apps/Pump/java/com/android/pump/fragment/MovieFragment.java
new file mode 100644
index 0000000..a6a6e8a
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/MovieFragment.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.MovieDetailsActivity;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Movie;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+@UiThread
+public class MovieFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new MovieFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_movie, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_movie_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new MovieAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class MovieAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Movie> mMovies; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private MovieAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mMovies = mMediaDb.getMovies();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addMovieUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeMovieUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new MovieViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Movie movie = mMovies.get(position - 1);
+                mMediaDb.loadData(movie); // TODO Where should we call this? In bind()?
+                ((MovieViewHolder) holder).bind(movie);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mMovies.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mMovies.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.movie;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class MovieViewHolder extends RecyclerView.ViewHolder {
+        private MovieViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Movie movie) {
+            ImageView imageView = itemView.findViewById(R.id.movie_image);
+            TextView textView = itemView.findViewById(R.id.movie_text);
+
+            Uri posterUri = movie.getPosterUri();
+            if (posterUri == null) {
+                posterUri = movie.getThumbnailUri();
+            }
+            imageView.setImageURI(posterUri);
+            textView.setText(movie.getTitle());
+
+            itemView.setOnClickListener((view) ->
+                    MovieDetailsActivity.start(view.getContext(), movie));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/OtherFragment.java b/apps/Pump/java/com/android/pump/fragment/OtherFragment.java
new file mode 100644
index 0000000..19c4c2b
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/OtherFragment.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.pump.R;
+import com.android.pump.activity.OtherDetailsActivity;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Other;
+import com.android.pump.util.Globals;
+import com.android.pump.util.ImageLoader;
+import com.android.pump.util.Orientation;
+import com.android.pump.widget.UriImageView;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+@UiThread
+public class OtherFragment extends Fragment {
+    private static final int SPAN_COUNT = 6;
+
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new OtherFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_other, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_other_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+
+        OtherAdapter otherAdapter = new OtherAdapter(requireContext());
+        mRecyclerView.setAdapter(otherAdapter);
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(otherAdapter.getSpanSizeLookup());
+        if (gridLayoutManager.getSpanCount() != SPAN_COUNT) {
+            throw new IllegalArgumentException("Expected a span count of " + SPAN_COUNT +
+                    ", found a span count of " + gridLayoutManager.getSpanCount() + ".");
+        }
+
+        mRecyclerView.setItemAnimator(null); // TODO Re-enable add/remove animations
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class OtherAdapter extends Adapter<ViewHolder>
+            implements MediaDb.UpdateCallback, ImageLoader.Callback {
+        private final ImageLoader mImageLoader;
+        private final MediaDb mMediaDb;
+        private final List<Other> mOthers; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+        private final SparseIntArray mSpanSize = new SparseIntArray();
+
+        private OtherAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+
+            mImageLoader = Globals.getImageLoader(context);
+            mMediaDb = Globals.getMediaDb(context);
+            mOthers = mMediaDb.getOthers();
+            recalculateSpans();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addOtherUpdateCallback(this);
+            mImageLoader.addCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeOtherUpdateCallback(this);
+            mImageLoader.removeCallback(this);
+        }
+
+        @Override
+        public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new OtherViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Other other = mOthers.get(position - 1);
+                mMediaDb.loadData(other); // TODO Where should we call this? In bind()?
+                ((OtherViewHolder) holder).bind(other);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mOthers.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mOthers.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.other;
+        }
+
+        @Override
+        public void onImageLoaded(@NonNull Uri uri, @Nullable Bitmap bitmap) {
+            // TODO Optimize this (only update necessary parts -- not the whole world)
+            recalculateSpans();
+            notifyItemRangeChanged(1, mOthers.size());
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+
+        private void recalculateSpans() {
+            // TODO Recalculate when an image is loaded
+            // TODO Recalculate when notifyXxx is called
+            // TODO Optimize
+            mSpanSize.clear();
+            int current = 0;
+            while (current < mOthers.size()) {
+                int orientation = getOrientation(current);
+                if (orientation == Orientation.LANDSCAPE) {
+                    orientation = getOrientation(current + 1);
+                    if (orientation == Orientation.LANDSCAPE) {
+                        // L L
+                        mSpanSize.append(current++, SPAN_COUNT / 2);
+                        mSpanSize.append(current++, SPAN_COUNT / 2);
+                    } else if (orientation == Orientation.PORTRAIT) {
+                        // L P
+                        mSpanSize.append(current++, SPAN_COUNT * 2 / 3);
+                        mSpanSize.append(current++, SPAN_COUNT * 1 / 3);
+                    } else {
+                        // L
+                        mSpanSize.append(current++, SPAN_COUNT);
+                    }
+                } else if (orientation == Orientation.PORTRAIT) {
+                    orientation = getOrientation(current + 1);
+                    if (orientation == Orientation.LANDSCAPE) {
+                        // P L
+                        mSpanSize.append(current++, SPAN_COUNT * 1 / 3);
+                        mSpanSize.append(current++, SPAN_COUNT * 2 / 3);
+                    } else if (orientation == Orientation.PORTRAIT &&
+                            getOrientation(current + 2) == Orientation.PORTRAIT) {
+                        // P P P
+                        mSpanSize.append(current++, SPAN_COUNT / 3);
+                        mSpanSize.append(current++, SPAN_COUNT / 3);
+                        mSpanSize.append(current++, SPAN_COUNT / 3);
+                    } else {
+                        // P
+                        mSpanSize.append(current++, SPAN_COUNT);
+                    }
+                } else {
+                    // unknown
+                    mSpanSize.append(current++, SPAN_COUNT);
+                }
+            }
+        }
+
+        private @Orientation int getOrientation(int index) {
+            Uri thumbUri = index >= mOthers.size() ? null : mOthers.get(index).getThumbnailUri();
+            if (thumbUri == null) {
+                return Orientation.UNKNOWN;
+            }
+            return mImageLoader.getOrientation(thumbUri);
+        }
+
+        private @NonNull SpanSizeLookup getSpanSizeLookup() {
+            return new SpanSizeLookup() {
+                @Override
+                public int getSpanSize(int position) {
+                    return position == 0 ? SPAN_COUNT : mSpanSize.get(position - 1);
+                }
+
+                @Override
+                public int getSpanIndex(int position, int spanCount) {
+                    // TODO Optimize
+                    return super.getSpanIndex(position, spanCount);
+                }
+
+                @Override
+                public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+                    // TODO Optimize
+                    return super.getSpanGroupIndex(adapterPosition, spanCount);
+                }
+            };
+        }
+    }
+
+    private static class OtherViewHolder extends ViewHolder {
+        private OtherViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Other other) {
+            UriImageView imageView = itemView.findViewById(R.id.other_image);
+
+            imageView.setImageURI(other.getThumbnailUri());
+
+            itemView.setOnClickListener((view) ->
+                    OtherDetailsActivity.start(view.getContext(), other));
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/PlaylistFragment.java b/apps/Pump/java/com/android/pump/fragment/PlaylistFragment.java
new file mode 100644
index 0000000..7862521
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/PlaylistFragment.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.PlaylistDetailsActivity;
+import com.android.pump.db.Album;
+import com.android.pump.db.Artist;
+import com.android.pump.db.Audio;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Playlist;
+import com.android.pump.util.Globals;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class PlaylistFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new PlaylistFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_playlist, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_playlist_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new PlaylistAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class PlaylistAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Playlist> mPlaylists; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private PlaylistAdapter(@NonNull Context context) {
+            setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mPlaylists = mMediaDb.getPlaylists();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addPlaylistUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removePlaylistUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new PlaylistViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Playlist playlist = mPlaylists.get(position - 1);
+                mMediaDb.loadData(playlist); // TODO Where should we call this? In bind()?
+                ((PlaylistViewHolder) holder).bind(playlist);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mPlaylists.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position == 0 ? -1 : mPlaylists.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.playlist;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class PlaylistViewHolder extends RecyclerView.ViewHolder {
+        private PlaylistViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Playlist playlist) {
+            ImageView image0View = itemView.findViewById(R.id.playlist_image_0);
+            ImageView image1View = itemView.findViewById(R.id.playlist_image_1);
+            ImageView image2View = itemView.findViewById(R.id.playlist_image_2);
+            ImageView image3View = itemView.findViewById(R.id.playlist_image_3);
+            TextView titleView = itemView.findViewById(R.id.playlist_title);
+            TextView artistsView = itemView.findViewById(R.id.playlist_artists);
+
+            // TODO Find a better way to handle 2x2 art
+            Set<Uri> albumArtUris = new HashSet<>();
+            Set<String> artistNames = new HashSet<>();
+            List<Audio> audios = playlist.getAudios();
+            for (Audio audio : audios) {
+                Album album = audio.getAlbum();
+                if (album != null && album.getAlbumArtUri() != null) {
+                    albumArtUris.add(album.getAlbumArtUri());
+                }
+
+                Artist artist = audio.getArtist();
+                if (artist != null && artist.getName() != null) {
+                    artistNames.add(artist.getName());
+                }
+            }
+
+            int numAlbumArt = albumArtUris.size();
+            if (numAlbumArt == 0) {
+                image0View.setImageURI(null);
+                image1View.setImageURI(null);
+                image2View.setImageURI(null);
+                image3View.setImageURI(null);
+                image0View.setVisibility(View.VISIBLE);
+                image1View.setVisibility(View.GONE);
+                image2View.setVisibility(View.GONE);
+                image3View.setVisibility(View.GONE);
+            } else if (numAlbumArt < 4) {
+                Iterator<Uri> iterator = albumArtUris.iterator();
+                image0View.setImageURI(iterator.next());
+                image1View.setImageURI(null);
+                image2View.setImageURI(null);
+                image3View.setImageURI(null);
+                image0View.setVisibility(View.VISIBLE);
+                image1View.setVisibility(View.GONE);
+                image2View.setVisibility(View.GONE);
+                image3View.setVisibility(View.GONE);
+            } else {
+                Iterator<Uri> iterator = albumArtUris.iterator();
+                image0View.setImageURI(iterator.next());
+                image1View.setImageURI(iterator.next());
+                image2View.setImageURI(iterator.next());
+                image3View.setImageURI(iterator.next());
+                image0View.setVisibility(View.VISIBLE);
+                image1View.setVisibility(View.VISIBLE);
+                image2View.setVisibility(View.VISIBLE);
+                image3View.setVisibility(View.VISIBLE);
+            }
+            titleView.setText(playlist.getName());
+            // TODO Fix comma separation for i18n/l11n
+            artistsView.setText(artistNames.isEmpty() ? null : TextUtils.join(", ", artistNames));
+
+            itemView.setOnClickListener((view) ->
+                    PlaylistDetailsActivity.start(view.getContext(), playlist));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/fragment/SeriesFragment.java b/apps/Pump/java/com/android/pump/fragment/SeriesFragment.java
new file mode 100644
index 0000000..dbe930f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/fragment/SeriesFragment.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.fragment;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.pump.R;
+import com.android.pump.activity.SeriesDetailsActivity;
+import com.android.pump.db.MediaDb;
+import com.android.pump.db.Series;
+import com.android.pump.util.Globals;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+@UiThread
+public class SeriesFragment extends Fragment {
+    private RecyclerView mRecyclerView;
+
+    public static @NonNull Fragment newInstance() {
+        return new SeriesFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_series, container, false);
+        mRecyclerView = view.findViewById(R.id.fragment_series_recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(new SeriesAdapter(requireContext()));
+        mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
+
+        GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+        gridLayoutManager.setSpanSizeLookup(
+                new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
+
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
+
+        // TODO Enable view caching
+        //mRecyclerView.setItemViewCacheSize(0);
+        //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
+        return view;
+    }
+
+    private static class SeriesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MediaDb.UpdateCallback {
+        private final MediaDb mMediaDb;
+        private final List<Series> mSeries; // TODO Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
+
+        private SeriesAdapter(@NonNull Context context) {
+            // TODO setHasStableIds(true);
+            mMediaDb = Globals.getMediaDb(context);
+            mSeries = mMediaDb.getSeries();
+        }
+
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.addSeriesUpdateCallback(this);
+        }
+
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mMediaDb.removeSeriesUpdateCallback(this);
+        }
+
+        @Override
+        public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent, int viewType) {
+            if (viewType == R.layout.header) {
+                return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false)) { };
+            } else {
+                return new SeriesViewHolder(LayoutInflater.from(parent.getContext())
+                        .inflate(viewType, parent, false));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+            if (position == 0) {
+                // TODO Handle header view
+            } else {
+                Series series = mSeries.get(position - 1);
+                mMediaDb.loadData(series); // TODO Where should we call this? In bind()?
+                ((SeriesViewHolder) holder).bind(series);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mSeries.size() + 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return 0; // TODO return position == 0 ? -1 : mSeries.get(position - 1).getId();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == 0 ? R.layout.header : R.layout.series;
+        }
+
+        @Override
+        public void onItemsInserted(int index, int count) {
+            notifyItemRangeInserted(index + 1, count);
+        }
+
+        @Override
+        public void onItemsUpdated(int index, int count) {
+            notifyItemRangeChanged(index + 1, count);
+        }
+
+        @Override
+        public void onItemsRemoved(int index, int count) {
+            notifyItemRangeRemoved(index + 1, count);
+        }
+    }
+
+    private static class SeriesViewHolder extends RecyclerView.ViewHolder {
+        private SeriesViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        private void bind(@NonNull Series series) {
+            ImageView imageView = itemView.findViewById(R.id.series_image);
+            TextView textView = itemView.findViewById(R.id.series_text);
+
+            imageView.setImageURI(series.getPosterUri());
+            textView.setText(series.getTitle());
+
+            itemView.setOnClickListener((view) ->
+                    SeriesDetailsActivity.start(view.getContext(), series));
+        }
+    }
+
+    private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
+        private final int mXOffset;
+        private final int mYOffset;
+
+        private SpaceItemDecoration(int xOffset, int yOffset) {
+            mXOffset = xOffset;
+            mYOffset = yOffset;
+        }
+
+        @Override
+        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
+            outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
+            if (parent.getChildAdapterPosition(view) > 0) {
+                outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
+            }
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends SpanSizeLookup {
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(int spanCount) {
+            mSpanCount = spanCount;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            return position == 0 ? mSpanCount : 1;
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            return position == 0 ? 0 : (position - 1) % spanCount;
+        }
+
+        @Override
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/provider/ApiKeys.java b/apps/Pump/java/com/android/pump/provider/ApiKeys.java
new file mode 100644
index 0000000..d832b41
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/provider/ApiKeys.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.provider;
+
+final class ApiKeys {
+    // STOPSHIP: Create proper API keys (and move to AndroidManifest.xml <meta-data/>?)
+    static final String OMDB_API = "";
+
+    private ApiKeys() { }
+}
diff --git a/apps/Pump/java/com/android/pump/provider/KnowledgeGraph.java b/apps/Pump/java/com/android/pump/provider/KnowledgeGraph.java
new file mode 100644
index 0000000..522ebb8
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/provider/KnowledgeGraph.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.provider;
+
+import com.android.pump.util.Clog;
+
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+public final class KnowledgeGraph {
+    private static final String TAG = Clog.tag(KnowledgeGraph.class);
+
+    private KnowledgeGraph() { }
+
+    public static void search(@NonNull Query query) throws IOException {
+        search(query, 1);
+    }
+
+    public static void search(@NonNull Query query, int maxResults) throws IOException {
+        Clog.i(TAG, "search(" + query + ", " + maxResults + ")");
+    }
+}
+
+/*
+https://kgsearch.googleapis.com/v1/entities:search?key=AIzaSyCV2--pLOigY36buwE7bnmyPLj7-z8DOd0&indent=true&ids=/m/0524b41
+https://kgsearch.googleapis.com/v1/entities:search?key=AIzaSyCV2--pLOigY36buwE7bnmyPLj7-z8DOd0&limit=1&indent=true&query=game+of+thrones&types=Movie&types=MovieSeries&types=TVSeries&types=TVEpisode
+https://kgsearch.googleapis.com/v1/entities:search?key=AIzaSyCV2--pLOigY36buwE7bnmyPLj7-z8DOd0&limit=1&query=game+of+thrones&types=Movie&types=MovieSeries&types=TVSeries&types=TVEpisode&alt=json&pp=false
+https://www.googleapis.com/kgraph/v1/search?key=AIzaSyDD7SzYetjesv1XXLvDHOrab2B_97FVUnI&limit=1&lang=en&output=(name)&query=/m/0524b41
+https://www.googleapis.com/kgraph/v1/search?key=AIzaSyDD7SzYetjesv1XXLvDHOrab2B_97FVUnI&limit=1&lang=en&query=/m/0524b41&output=/common/topic/description+/common/topic/image+/type/object/name
+https://www.googleapis.com/kgraph/v1kpanels/search?query=babar&indent=true&key=AIzaSyDD7SzYetjesv1XXLvDHOrab2B_97FVUnI
+
+package com.google.tv.annotation.util;
+
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.JsonReader;
+import android.util.LruCache;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.regex.Pattern;
+
+public class Freebase extends LruCache<String, Freebase.FreebaseResult> {
+    private static final boolean USE_KNOWLEDGE_GRAPH = true;
+    private static final Pattern MID_REGEX = Pattern.compile("/m/[\\d\\w_]+");
+    private static final int CACHE_SIZE = 256;
+    private static final Freebase sFreebase = new Freebase();
+
+    public interface FreebaseListener {
+        void onDataLoaded(String mid, FreebaseResult result);
+    }
+
+    public static class FreebaseResult {
+        protected FreebaseResult(String title, String description, String imageUri) {
+            this.title = title;
+            this.description = description;
+            this.imageUri = imageUri;
+        }
+
+        public String title;
+        public String description;
+        public String imageUri;
+    }
+
+    private Freebase() {
+        super(CACHE_SIZE);
+    }
+
+    public static void loadData(String mid, FreebaseListener listener) {
+        sFreebase.internalLoadData(mid, listener);
+    }
+
+    private void internalLoadData(String mid, FreebaseListener listener) {
+        FreebaseResult result = get(mid);
+        if (result != null) {
+            listener.onDataLoaded(mid, result);
+        } else {
+            try {
+                new FreebaseLoader(mid, listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            } catch (RejectedExecutionException e) {
+            }
+        }
+    }
+
+    private String getTopicUri(String mid) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("www.googleapis.com");
+        if (USE_KNOWLEDGE_GRAPH) {
+            ub.appendPath("kgraph");
+        } else {
+            ub.appendPath("freebase");
+        }
+        ub.appendPath("v1");
+        ub.appendPath("topic");
+        ub.appendPath("m");
+        ub.appendPath(getMidId(mid));
+        ub.appendQueryParameter("filter", "/common/topic/description");
+        ub.appendQueryParameter("filter", "/common/topic/image");
+        ub.appendQueryParameter("filter", "/type/object/name");
+        ub.appendQueryParameter("limit", "1");
+        ub.appendQueryParameter("lang", Locale.getDefault().getLanguage());
+        if (USE_KNOWLEDGE_GRAPH) {
+            ub.appendQueryParameter("key", "AIzaSyDD7SzYetjesv1XXLvDHOrab2B_97FVUnI");
+        } else {
+            ub.appendQueryParameter("key", "AIzaSyCfYZxsM9VR99tFLIGyrxpMhJvyrqdCFnw");
+        }
+        return ub.build().toString();
+    }
+
+    private String getImageUri(String mid) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("usercontent.googleapis.com");
+        ub.appendPath("freebase");
+        ub.appendPath("v1");
+        ub.appendPath("image");
+        ub.appendPath("m");
+        ub.appendPath(getMidId(mid));
+        return ub.build().toString();
+    }
+
+    private String getMidId(String mid) {
+        if (!isValidMid(mid)) {
+            throw new IllegalArgumentException("Invalid Freebase MID");
+        }
+        return mid.substring(3);
+    }
+
+    private boolean isValidMid(String mid) {
+        return MID_REGEX.matcher(mid).matches();
+    }
+
+    class FreebaseLoader extends AsyncTask<Void, Void, FreebaseResult> {
+        private String mMid;
+        private FreebaseListener mListener;
+        private String mTitle;
+        private String mDescription;
+        private String mImageMid;
+
+        public FreebaseLoader(String mid, FreebaseListener listener) {
+            mMid = mid;
+            mListener = listener;
+        }
+
+        @Override
+        protected FreebaseResult doInBackground(Void... params) {
+            InputStream inputStream;
+            try {
+                inputStream = new URL(getTopicUri(mMid)).openStream();
+                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
+                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
+                parseFreebaseResult(new JsonReader(bufferedReader));
+                String imageUri = mImageMid == null ? null : getImageUri(mImageMid);
+                return new FreebaseResult(mTitle, mDescription, imageUri);
+            } catch (MalformedURLException e) {
+            } catch (IOException e) {
+            } catch (IllegalArgumentException e) {
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(FreebaseResult result) {
+            if (result != null) {
+                put(mMid, result);
+                mListener.onDataLoaded(mMid, result);
+            }
+        }
+
+        void parseFreebaseResult(JsonReader jsonReader) throws IOException {
+            String name;
+            jsonReader.beginObject();
+            while (jsonReader.hasNext()) {
+                name = jsonReader.nextName();
+                if (name.equals("property")) {
+                    jsonReader.beginObject();
+                    while (jsonReader.hasNext()) {
+                        name = jsonReader.nextName();
+                        if (name.equals("/common/topic/description")) {
+                            jsonReader.beginObject();
+                            while (jsonReader.hasNext()) {
+                                name = jsonReader.nextName();
+                                if (name.equals("values")) {
+                                    jsonReader.beginArray();
+                                    while (jsonReader.hasNext()) {
+                                        jsonReader.beginObject();
+                                        while (jsonReader.hasNext()) {
+                                            name = jsonReader.nextName();
+                                            if (name.equals("text")) {
+                                                mDescription = jsonReader.nextString();
+                                            } else {
+                                                jsonReader.skipValue();
+                                            }
+                                        }
+                                        jsonReader.endObject();
+                                    }
+                                    jsonReader.endArray();
+                                } else {
+                                    jsonReader.skipValue();
+                                }
+                            }
+                            jsonReader.endObject();
+                        } else if (name.equals("/common/topic/image")) {
+                            jsonReader.beginObject();
+                            while (jsonReader.hasNext()) {
+                                name = jsonReader.nextName();
+                                if (name.equals("values")) {
+                                    jsonReader.beginArray();
+                                    while (jsonReader.hasNext()) {
+                                        jsonReader.beginObject();
+                                        while (jsonReader.hasNext()) {
+                                            name = jsonReader.nextName();
+                                            if (name.equals("id")) {
+                                                mImageMid = jsonReader.nextString();
+                                            } else {
+                                                jsonReader.skipValue();
+                                            }
+                                        }
+                                        jsonReader.endObject();
+                                    }
+                                    jsonReader.endArray();
+                                } else {
+                                    jsonReader.skipValue();
+                                }
+                            }
+                            jsonReader.endObject();
+                        } else if (name.equals("/type/object/name")) {
+                            jsonReader.beginObject();
+                            while (jsonReader.hasNext()) {
+                                name = jsonReader.nextName();
+                                if (name.equals("values")) {
+                                    jsonReader.beginArray();
+                                    while (jsonReader.hasNext()) {
+                                        jsonReader.beginObject();
+                                        while (jsonReader.hasNext()) {
+                                            name = jsonReader.nextName();
+                                            if (name.equals("value")) {
+                                                mTitle = jsonReader.nextString();
+                                            } else {
+                                                jsonReader.skipValue();
+                                            }
+                                        }
+                                        jsonReader.endObject();
+                                    }
+                                    jsonReader.endArray();
+                                } else {
+                                    jsonReader.skipValue();
+                                }
+                            }
+                            jsonReader.endObject();
+                        } else {
+                            jsonReader.skipValue();
+                        }
+                    }
+                    jsonReader.endObject();
+                } else {
+                    jsonReader.skipValue();
+                }
+            }
+            jsonReader.endObject();
+        }
+    }
+}
+*/
\ No newline at end of file
diff --git a/apps/Pump/java/com/android/pump/provider/OmdbApi.java b/apps/Pump/java/com/android/pump/provider/OmdbApi.java
new file mode 100644
index 0000000..16e9fb9
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/provider/OmdbApi.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.provider;
+
+import android.net.Uri;
+
+import com.android.pump.db.DataProvider;
+import com.android.pump.db.Episode;
+import com.android.pump.db.Movie;
+import com.android.pump.db.Series;
+import com.android.pump.util.Clog;
+import com.android.pump.util.Http;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.IOException;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+public final class OmdbApi implements DataProvider {
+    private static final String TAG = Clog.tag(OmdbApi.class);
+
+    private static final DataProvider INSTANCE = new OmdbApi();
+
+    private OmdbApi() { }
+
+    @AnyThread
+    public static @NonNull DataProvider getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public boolean populateMovie(@NonNull Movie movie) throws IOException {
+        boolean updated = false;
+        try {
+            JSONObject root = (JSONObject) getContent(getContentUri(movie));
+            updated |= movie.setPosterUri(getPosterUri(root.getString("imdbID")));
+            updated |= movie.setSynopsis(root.getString("Plot"));
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse search result", e);
+            throw new IOException(e);
+        }
+        return updated;
+    }
+
+    @Override
+    public boolean populateSeries(@NonNull Series series) throws IOException {
+        boolean updated = false;
+        try {
+            JSONObject root = (JSONObject) getContent(getContentUri(series));
+            updated |= series.setPosterUri(getPosterUri(root.getString("imdbID")));
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse search result", e);
+            throw new IOException(e);
+        }
+        return updated;
+    }
+
+    @Override
+    public boolean populateEpisode(@NonNull Episode episode) throws IOException {
+        boolean updated = false;
+        try {
+            JSONObject root = (JSONObject) getContent(getContentUri(episode));
+            updated |= episode.setPosterUri(getPosterUri(root.getString("imdbID")));
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse search result", e);
+            throw new IOException(e);
+        }
+        return updated;
+    }
+
+    private static @NonNull Uri getContentUri(@NonNull Movie movie) {
+        Uri.Builder ub = getContentUri(movie.getTitle());
+        if (movie.hasYear()) {
+            ub.appendQueryParameter("y", Integer.toString(movie.getYear()));
+        }
+        ub.appendQueryParameter("type", "movie");
+        return ub.build();
+    }
+
+    private static @NonNull Uri getContentUri(@NonNull Series series) {
+        Uri.Builder ub = getContentUri(series.getTitle());
+        if (series.hasYear()) {
+            ub.appendQueryParameter("y", Integer.toString(series.getYear()));
+        }
+        ub.appendQueryParameter("type", "series");
+        return ub.build();
+    }
+
+    private static @NonNull Uri getContentUri(@NonNull Episode episode) {
+        Series series = episode.getSeries();
+        Uri.Builder ub = getContentUri(series.getTitle());
+        if (series.hasYear()) {
+            ub.appendQueryParameter("y", Integer.toString(series.getYear()));
+        }
+        ub.appendQueryParameter("Season", Integer.toString(episode.getSeason()));
+        ub.appendQueryParameter("Episode", Integer.toString(episode.getEpisode()));
+        ub.appendQueryParameter("type", "episode");
+        return ub.build();
+    }
+
+    private static @NonNull Uri.Builder getContentUri(@NonNull String title) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("omdbapi.com");
+        ub.appendQueryParameter("apikey", ApiKeys.OMDB_API);
+        ub.appendQueryParameter("t", title);
+        return ub;
+    }
+
+    private static @NonNull Object getContent(@NonNull Uri uri) throws IOException, JSONException {
+        return new JSONTokener(new String(Http.get(uri.toString()), "utf-8")).nextValue();
+    }
+
+    private static @NonNull Uri getPosterUri(@NonNull String imdbId) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("img.omdbapi.com");
+        ub.appendQueryParameter("apikey", ApiKeys.OMDB_API);
+        ub.appendQueryParameter("i", imdbId);
+        return ub.build();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/provider/Query.java b/apps/Pump/java/com/android/pump/provider/Query.java
new file mode 100644
index 0000000..1810c6e
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/provider/Query.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.provider;
+
+import android.net.Uri;
+
+import com.android.pump.util.Clog;
+
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class Query {
+    private static final String TAG = Clog.tag(Query.class);
+
+    private static final Pattern PATTERN_EPISODE = Pattern.compile(
+            "([^\\\\/]+?)(?:\\(((?:19|20)\\d{2})\\))?\\s*" +
+            "[Ss](\\d{1,2})[._x]?[Ee](\\d{1,2})(?!.*[Ss]\\d{1,2}[._x]?[Ee]\\d{1,2})");
+    private static final Pattern PATTERN_MOVIE = Pattern.compile(
+            "([^\\\\/]+)\\(((?:19|20)\\d{2})\\)(?!.*\\((?:19|20)\\d{2}\\))");
+    private static final Pattern PATTERN_CLEANUP = Pattern.compile("\\s+");
+
+    private final String mName;
+    private final int mYear;
+    private final int mSeason;
+    private final int mEpisode;
+
+    private Query(String name) {
+        //Clog.i(TAG, "Query(" + name + ")");
+        mName = name;
+        mYear = -1;
+        mSeason = -1;
+        mEpisode = -1;
+    }
+
+    private Query(String name, int year) {
+        //Clog.i(TAG, "Query(" + name + ", " + year + ")");
+        mName = name;
+        mYear = year;
+        mSeason = -1;
+        mEpisode = -1;
+    }
+
+    private Query(String name, int season, int episode) {
+        //Clog.i(TAG, "Query(" + name + ", " + season + ", " + episode + ")");
+        mName = name;
+        mYear = -1;
+        mSeason = season;
+        mEpisode = episode;
+    }
+
+    private Query(String name, int year, int season, int episode) {
+        //Clog.i(TAG, "Query(" + name + ", " + year + ", " + season + ", " + episode + ")");
+        mName = name;
+        mYear = year;
+        mSeason = season;
+        mEpisode = episode;
+    }
+
+    public boolean isMovie() {
+        return hasYear() && !isEpisode();
+    }
+
+    public boolean isEpisode() {
+        return mSeason >= 0 && mEpisode >= 0;
+    }
+
+    public @NonNull String getName() {
+        return mName;
+    }
+
+    public boolean hasYear() {
+        return mYear >= 0;
+    }
+
+    public int getYear() {
+        return mYear;
+    }
+
+    public int getSeason() {
+        return mSeason;
+    }
+
+    public int getEpisode() {
+        return mEpisode;
+    }
+
+    public static @NonNull Query parse(@NonNull Uri uri) {
+        //Clog.i(TAG, "parse(" + uri + ")");
+        String filePath = uri.getPath();
+        Query query;
+        query = parseEpisode(filePath);
+        if (query == null) {
+            query = parseMovie(filePath);
+        }
+        if (query == null) {
+            query = new Query(uri.getLastPathSegment());
+        }
+        return query;
+    }
+
+    private static Query parseEpisode(String filePath) {
+        //Clog.i(TAG, "parseEpisode(" + filePath + ")");
+        Matcher matcher = PATTERN_EPISODE.matcher(filePath);
+        if (matcher.find()) {
+            MatchResult matchResult = matcher.toMatchResult();
+            if (matchResult.groupCount() == 4) {
+                String name = cleanup(matchResult.group(1));
+                int year = matchResult.group(2) == null ? 0 : Integer.valueOf(matchResult.group(2));
+                int season = Integer.valueOf(matchResult.group(3));
+                int episode = Integer.valueOf(matchResult.group(4));
+                //Clog.i(TAG, "name = " + name);
+                //if (year > 0) {
+                //    Clog.i(TAG, "year = " + year);
+                //}
+                //Clog.i(TAG, "season = " + season);
+                //Clog.i(TAG, "episode = " + episode);
+                if (year > 0) {
+                    return new Query(name, year, season, episode);
+                } else {
+                    return new Query(name, season, episode);
+                }
+            }
+        }
+        return null;
+    }
+
+    private static Query parseMovie(String filePath) {
+        //Clog.i(TAG, "parseMovie(" + filePath + ")");
+        Matcher matcher = PATTERN_MOVIE.matcher(filePath);
+        if (matcher.find()) {
+            MatchResult matchResult = matcher.toMatchResult();
+            if (matchResult.groupCount() == 2) {
+                String name = cleanup(matchResult.group(1));
+                int year = Integer.valueOf(matchResult.group(2));
+                //Clog.i(TAG, "name = " + name);
+                //Clog.i(TAG, "year = " + year);
+                return new Query(name, year);
+            }
+        }
+        return null;
+    }
+
+    private static String cleanup(String string) {
+        return PATTERN_CLEANUP.matcher(string).replaceAll(" ").trim();
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/provider/Wikidata.java b/apps/Pump/java/com/android/pump/provider/Wikidata.java
new file mode 100644
index 0000000..85b89a9
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/provider/Wikidata.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.provider;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.pump.util.Clog;
+import com.android.pump.util.Http;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+public final class Wikidata {
+    private static final String TAG = Clog.tag(Wikidata.class);
+
+    private Wikidata() { }
+
+    public static void search(@NonNull Query query) throws IOException {
+        search(query, 1);
+    }
+
+    public static void search(@NonNull Query query, int maxResults) throws IOException {
+        Clog.i(TAG, "search(" + query + ", " + maxResults + ")");
+        getContentForResults(getSearchResults(getQueryString(query), maxResults));
+    }
+
+    private static String getQueryString(Query query) {
+        StringBuilder builder = new StringBuilder();
+        builder.append(query.getName());
+        if (query.hasYear()) {
+            builder.append(' ');
+            builder.append(query.getYear());
+        }
+        if (query.isEpisode()) {
+            builder.append(' ');
+            builder.append('s');
+            builder.append(query.getSeason());
+
+            builder.append(' ');
+            builder.append('e');
+            builder.append(query.getEpisode());
+        }
+        return  builder.toString();
+    }
+
+    private static List<String> getSearchResults(String search, int maxResults) throws IOException {
+        String uri = getSearchUri(search, maxResults);
+        Clog.i(TAG, uri);
+        String result = new String(Http.get(uri), "utf-8");
+        Clog.i(TAG, result);
+        try {
+            Object root = new JSONTokener(result).nextValue();
+            dumpJson(root);
+            JSONObject resultRoot = (JSONObject) root;
+            JSONArray resultArray = resultRoot.getJSONObject("query").getJSONArray("search");
+            List<String> ids = new ArrayList<>(resultArray.length());
+            for (int i = 0; i < resultArray.length(); ++i) {
+                ids.add(resultArray.getJSONObject(i).getString("title"));
+            }
+            return ids;
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse search result", e);
+            throw new IOException(e);
+        }
+    }
+
+    private static void getContentForResults(List<String> ids) throws IOException {
+        /*
+        String uri = getContentUri(ids);
+        Clog.i(TAG, uri);
+        String result = new String(Http.get(uri), "utf-8");
+        //Clog.i(TAG, result);
+        try {
+            Object root = new JSONTokener(result).nextValue();
+            //dumpJson(root);
+            JSONObject resultRoot = (JSONObject) root;
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse data", e);
+            throw new IOException(e);
+        }
+        */
+        getSparqlForResults(ids);
+    }
+
+    private static void getSparqlForResults(List<String> ids) throws IOException {
+        String uri = getSparqlUri(ids);
+        Clog.i(TAG, uri);
+        String result = new String(Http.get(uri), "utf-8");
+        Clog.i(TAG, result);
+        try {
+            Object root = new JSONTokener(result).nextValue();
+            dumpJson(root);
+            JSONObject resultRoot = (JSONObject) root;
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse sparql", e);
+            throw new IOException(e);
+        }
+    }
+
+    private static String getSearchUri(String search, int maxResults) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("www.wikidata.org");
+        ub.appendPath("w");
+        ub.appendPath("api.php");
+        ub.appendQueryParameter("action", "query");
+        ub.appendQueryParameter("list", "search");
+        ub.appendQueryParameter("format", "json");
+        ub.appendQueryParameter("formatversion", "2");
+        ub.appendQueryParameter("srsearch", search);
+        ub.appendQueryParameter("srlimit", Integer.toString(maxResults));
+        ub.appendQueryParameter("srprop", "");
+        ub.appendQueryParameter("srinfo", "");
+        return ub.build().toString();
+    }
+
+    /*
+     */
+
+    // https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service
+
+    /*
+const endpointUrl = 'https://query.wikidata.org/sparql',
+      sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?itemDescription
+WHERE
+{
+  ?item wdt:P31/wdt:P279* wd:Q11424.
+  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" }
+  ?item wdt:P3383 ?itemPoster.
+}`,
+      fullUrl = endpointUrl + '?query=' + encodeURIComponent( sparqlQuery ),
+      headers = { 'Accept': 'application/sparql-results+json' };
+
+fetch( fullUrl, { headers } ).then( body => body.json() ).then( json => {
+    const { head: { vars }, results } = json;
+    for ( const result of results.bindings ) {
+        for ( const variable of vars ) {
+            console.log( '%s: %o', variable, result[variable] );
+        }
+        console.log( '---' );
+    }
+} );
+     */
+
+    // ?action=wbgetentities&ids=Q775450|Q3041294|Q646968|Q434841|Q11920&format=jsonfm&props=labels&languages=en|de|fr
+    private static String getContentUri(List<String> ids) {
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("www.wikidata.org");
+        ub.appendPath("w");
+        ub.appendPath("api.php");
+        ub.appendQueryParameter("action", "wbgetentities");
+        ub.appendQueryParameter("props", "labels|descriptions");
+        ub.appendQueryParameter("format", "json");
+        ub.appendQueryParameter("formatversion", "2");
+        ub.appendQueryParameter("languages", "en");
+        ub.appendQueryParameter("languagefallback", "");
+        ub.appendQueryParameter("ids", TextUtils.join("|", ids));
+        return ub.build().toString();
+    }
+
+    private static String getSparqlUri(List<String> ids) {
+        List<String> dbIds = new ArrayList<>(ids.size());
+        for (String id : ids) {
+            dbIds.add("wd:" + id);
+        }
+        String sparqlQuery = ""
+                + "SELECT * WHERE {"
+                +   "VALUES ?item {"
+                +     TextUtils.join(" ", dbIds)
+                +   "}"
+                +   "FILTER EXISTS {?item wdt:P31/wdt:P279* wd:Q11424.}"
+                +   "OPTIONAL {?item wdt:P3383 ?poster.}"
+                +   "?item rdfs:label ?title FILTER (lang(?title) = 'en')."
+                + "}";
+        Uri.Builder ub = new Uri.Builder();
+        ub.scheme("https");
+        ub.authority("query.wikidata.org");
+        ub.appendPath("sparql");
+        ub.appendQueryParameter("format", "json");
+        ub.appendQueryParameter("query", sparqlQuery);
+        return ub.build().toString();
+    }
+
+    private static void dumpJson(Object root) throws JSONException {
+        dumpJson(null, "", root);
+    }
+
+    private static void dumpJson(String name, String indent, Object object) throws JSONException {
+        name = name != null ? name + ": " : "";
+        if (object == JSONObject.NULL) {
+            Clog.d(TAG, indent + name + "null");
+        } else if (object instanceof JSONObject) {
+            Clog.d(TAG, indent + name + "{");
+            JSONObject jsonObject = (JSONObject) object;
+            Iterator<String> keys = jsonObject.keys();
+            while (keys.hasNext()) {
+                String key = keys.next();
+                dumpJson(key, indent + "  ", jsonObject.get(key));
+            }
+            Clog.d(TAG, indent + "}");
+        } else if (object instanceof JSONArray) {
+            Clog.d(TAG, indent + name + "[");
+            JSONArray jsonArray = (JSONArray) object;
+            for (int i = 0; i < jsonArray.length(); ++i) {
+                dumpJson(null, indent + "  ", jsonArray.get(i));
+            }
+            Clog.d(TAG, indent + "]");
+        } else if (object instanceof String) {
+            String jsonString = (String) object;
+            Clog.d(TAG, indent + name + jsonString + " (string)");
+        } else if (object instanceof Boolean) {
+            Boolean jsonBoolean = (Boolean) object;
+            Clog.d(TAG, indent + name + jsonBoolean + " (boolean)");
+        } else if (object instanceof Integer) {
+            Integer jsonInteger = (Integer) object;
+            Clog.d(TAG, indent + name + jsonInteger + " (int)");
+        } else if (object instanceof Long) {
+            Long jsonLong = (Long) object;
+            Clog.d(TAG, indent + name + jsonLong + " (long)");
+        } else if (object instanceof Double) {
+            Double jsonDouble = (Double) object;
+            Clog.d(TAG, indent + name + jsonDouble + " (double)");
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/ui/CustomRecycledViewPool.java b/apps/Pump/java/com/android/pump/ui/CustomRecycledViewPool.java
new file mode 100644
index 0000000..faa87dc
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/ui/CustomRecycledViewPool.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.ui;
+
+import android.util.SparseBooleanArray;
+
+import com.android.pump.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+@UiThread
+public class CustomRecycledViewPool extends RecycledViewPool {
+    private static final int MAX_VIEWS = 17;
+
+    private final SparseBooleanArray mInitialized = new SparseBooleanArray();
+
+    public CustomRecycledViewPool() {
+        setMaxRecycledViews(R.layout.header, 1);
+        setMaxRecycledViews(R.layout.movie, MAX_VIEWS);
+    }
+
+    @Override
+    public void setMaxRecycledViews(int viewType, int maxViews) {
+        super.setMaxRecycledViews(viewType, maxViews);
+        mInitialized.put(viewType, true);
+    }
+
+    @Override
+    public void putRecycledView(@NonNull ViewHolder scrap) {
+        int viewType = scrap.getItemViewType();
+        if (!mInitialized.get(viewType)) {
+            setMaxRecycledViews(viewType, MAX_VIEWS);
+            throw new IllegalArgumentException("Unknown view type"); // TODO tuning only -- remove this
+        }
+        super.putRecycledView(scrap);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/BitmapCache.java b/apps/Pump/java/com/android/pump/util/BitmapCache.java
new file mode 100644
index 0000000..06c081a
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/BitmapCache.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.LruCache;
+import androidx.core.graphics.BitmapCompat;
+
+@AnyThread
+class BitmapCache {
+    private static final int CACHE_SIZE =
+            (int) Math.min(Runtime.getRuntime().maxMemory() / 8, Integer.MAX_VALUE / 4);
+
+    private final MemoryCache mMemoryCache = new MemoryCache(CACHE_SIZE);
+
+    void put(@NonNull Uri key, @NonNull Bitmap bitmap) {
+        mMemoryCache.put(key, bitmap);
+    }
+
+    @Nullable Bitmap get(@NonNull Uri key) {
+        return mMemoryCache.get(key);
+    }
+
+    void clear() {
+        mMemoryCache.evictAll();
+    }
+
+    private static class MemoryCache extends LruCache<Uri, Bitmap> {
+        private MemoryCache(int maxSize) {
+            super(maxSize);
+        }
+
+        @Override
+        protected int sizeOf(@NonNull Uri key, @NonNull Bitmap bitmap) {
+            return BitmapCompat.getAllocationByteCount(bitmap);
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Clog.java b/apps/Pump/java/com/android/pump/util/Clog.java
new file mode 100644
index 0000000..1b31735
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Clog.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import com.android.pump.BuildConfig;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class Clog {
+    private static final boolean COLORIZE = BuildConfig.DEBUG;
+
+    public static final int VERBOSE = android.util.Log.VERBOSE;
+    public static final int DEBUG = android.util.Log.DEBUG;
+    public static final int INFO = android.util.Log.INFO;
+    public static final int WARN = android.util.Log.WARN;
+    public static final int ERROR = android.util.Log.ERROR;
+    public static final int ASSERT = android.util.Log.ASSERT;
+
+    private static final int COLOR_BLACK = 30;
+    private static final int COLOR_RED = 31;
+    private static final int COLOR_GREEN = 32;
+    private static final int COLOR_YELLOW = 33;
+    private static final int COLOR_BLUE = 34;
+    private static final int COLOR_MAGENTA = 35;
+    private static final int COLOR_CYAN = 36;
+    //private static final int COLOR_WHITE = 37;
+
+    private static final int MAX_TAG_LENGTH = 23;
+    private static final int MAX_LINE_LENGTH = 1024;
+
+    private static final Pattern LINE_BREAKER = Pattern.compile("\\r?\\n");
+
+    private Clog() { }
+
+    public static @NonNull String tag(@NonNull Class<?> clazz) {
+        String tag = clazz.getSimpleName();
+        return tag.substring(0, Math.min(tag.length(), MAX_TAG_LENGTH));
+    }
+
+    public static int v(@NonNull String tag, @NonNull String msg) {
+        return println(VERBOSE, tag, msg);
+    }
+
+    public static int v(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return println(VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
+    }
+
+    public static int d(@NonNull String tag, @NonNull String msg) {
+        return println(DEBUG, tag, msg);
+    }
+
+    public static int d(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return println(DEBUG, tag, msg + '\n' + getStackTraceString(tr));
+    }
+
+    public static int i(@NonNull String tag, @NonNull String msg) {
+        return println(INFO, tag, msg);
+    }
+
+    public static int i(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return println(INFO, tag, msg + '\n' + getStackTraceString(tr));
+    }
+
+    public static int w(@NonNull String tag, @NonNull String msg) {
+        return println(WARN, tag, msg);
+    }
+
+    public static int w(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return println(WARN, tag, msg + '\n' + getStackTraceString(tr));
+    }
+
+    public static boolean isLoggable(@NonNull String tag, int level) {
+        return android.util.Log.isLoggable(tag, level);
+    }
+
+    public static int w(@NonNull String tag, @NonNull Throwable tr) {
+        return println(WARN, tag, getStackTraceString(tr));
+    }
+
+    public static int e(@NonNull String tag, @NonNull String msg) {
+        return println(ERROR, tag, msg);
+    }
+
+    public static int e(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return println(ERROR, tag, msg + '\n' + getStackTraceString(tr));
+    }
+
+    public static int wtf(@NonNull String tag, @NonNull String msg) {
+        return android.util.Log.wtf(tag, msg);
+    }
+
+    public static int wtf(@NonNull String tag, @NonNull Throwable tr) {
+        return android.util.Log.wtf(tag, tr);
+    }
+
+    public static int wtf(@NonNull String tag, @NonNull String msg, @NonNull Throwable tr) {
+        return android.util.Log.wtf(tag, msg, tr);
+    }
+
+    public static @NonNull String getStackTraceString(@NonNull Throwable tr) {
+        return android.util.Log.getStackTraceString(tr);
+    }
+
+    public static int println(int priority, @NonNull String tag, @NonNull String msg) {
+        tag = String.valueOf(tag);
+        msg = String.valueOf(msg);
+
+        int color;
+        switch (priority) {
+            case VERBOSE:
+                color = COLOR_CYAN;
+                break;
+            case DEBUG:
+                color = COLOR_BLUE;
+                break;
+            case INFO:
+                color = COLOR_GREEN;
+                break;
+            case WARN:
+                color = COLOR_YELLOW;
+                break;
+            case ERROR:
+                color = COLOR_RED;
+                break;
+            case ASSERT:
+                color = COLOR_MAGENTA;
+                break;
+            default:
+                color = COLOR_BLACK;
+                break;
+        }
+
+        int result = 0;
+        for (String line : LINE_BREAKER.split(msg)) {
+            int length = line.length();
+            for (int start = 0; start < length; start += MAX_LINE_LENGTH) {
+                String part = line.substring(start, Math.min(start + MAX_LINE_LENGTH, length));
+                result += android.util.Log.println(priority, tag, colorize(part, color));
+            }
+        }
+        return result;
+    }
+
+    private static String colorize(String msg, int color) {
+        if (COLORIZE) {
+            return String.format(Locale.ROOT, "\033[%2$dm%1$s\033[0m", msg, color);
+        }
+        return msg;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Collections.java b/apps/Pump/java/com/android/pump/util/Collections.java
new file mode 100644
index 0000000..a8ba612
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Collections.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@AnyThread
+public final class Collections {
+    private Collections() { }
+
+    @FunctionalInterface
+    public interface LongKeyRetriever<T> {
+        long getKey(@NonNull T value);
+    }
+
+    public static @Nullable <T> T find(@NonNull List<T> list, long key,
+            @NonNull LongKeyRetriever<T> keyRetriever) {
+        int index = binarySearch(list, key, keyRetriever);
+        if (index >= 0) {
+            return list.get(index);
+        }
+        return null;
+    }
+
+    public static <T> int binarySearch(@NonNull List<T> list, long key,
+            @NonNull LongKeyRetriever<T> keyRetriever) {
+        int lo = 0;
+        int hi = list.size() - 1;
+
+        while (lo <= hi) {
+            int mid = lo + (hi - lo) / 2;
+            long midKey = keyRetriever.getKey(list.get(mid));
+
+            if (midKey < key) {
+                lo = mid + 1;
+            } else if (midKey > key) {
+                hi = mid - 1;
+            } else {
+                return mid;
+            }
+        }
+        return ~lo;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Globals.java b/apps/Pump/java/com/android/pump/util/Globals.java
new file mode 100644
index 0000000..50e68e1
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Globals.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+
+import com.android.pump.db.MediaDb;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
+
+@AnyThread
+public final class Globals {
+    private Globals() { }
+
+    public static @NonNull ImageLoader getImageLoader(@NonNull Context context) {
+        return getProvider(context).getImageLoader();
+    }
+
+    public static @NonNull RecycledViewPool getRecycledViewPool(@NonNull Context context) {
+        return getProvider(context).getRecycledViewPool();
+    }
+
+    public static @NonNull MediaDb getMediaDb(@NonNull Context context) {
+        return getProvider(context).getMediaDb();
+    }
+
+    public interface Provider {
+        @NonNull ImageLoader getImageLoader();
+        @NonNull RecycledViewPool getRecycledViewPool();
+        @NonNull MediaDb getMediaDb();
+    }
+
+    private static @NonNull Provider getProvider(@NonNull Context context) {
+        while (!(context instanceof Provider)) {
+            if (context instanceof ContextWrapper) {
+                context = ((ContextWrapper) context).getBaseContext();
+            } else {
+                context = context.getApplicationContext();
+                if (!(context instanceof Provider)) {
+                    throw new IllegalArgumentException("No global provider in context");
+                }
+            }
+        }
+        return (Provider) context;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Http.java b/apps/Pump/java/com/android/pump/util/Http.java
new file mode 100644
index 0000000..aae7ff7
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Http.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.Manifest;
+import android.net.TrafficStats;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+public final class Http {
+    private static final String TAG = Clog.tag(Http.class);
+
+    private static final int TRAFFIC_STATS_TAG = 4711; // TODO Assign a better value
+    private static final byte[] EMPTY_DATA = new byte[0];
+
+    private Http() { }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public static @NonNull byte[] post(@NonNull String uri) throws IOException {
+        return post(uri, Headers.NONE, EMPTY_DATA);
+    }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public static @NonNull byte[] post(@NonNull String uri, @NonNull Headers headers)
+            throws IOException {
+        return post(uri, headers, EMPTY_DATA);
+    }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public static @NonNull byte[] post(@NonNull String uri, @NonNull byte[] data)
+            throws IOException {
+        return post(uri, Headers.NONE, data);
+    }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public @NonNull static byte[] post(@NonNull String uri, @NonNull Headers headers,
+            @NonNull byte[] data) throws IOException {
+        return getOrPost(uri, headers, data);
+    }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public static @NonNull byte[] get(@NonNull String uri) throws IOException {
+        return get(uri, Headers.NONE);
+    }
+
+    @RequiresPermission(Manifest.permission.INTERNET)
+    public static @NonNull byte[] get(@NonNull String uri, @NonNull Headers headers)
+            throws IOException {
+        return getOrPost(uri, headers, null);
+    }
+
+    private static byte[] getOrPost(String uri, Headers headers, byte[] data) throws IOException {
+        final URL url = new URL(uri);
+        int numRetries = 3;
+        for (;;) {
+            long retryDelaySec = 5;
+            try {
+                return getOrPost(url, headers, data);
+            } catch (Http.HttpError e) {
+                int responseCode = e.getResponseCode();
+                if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) {
+                    String retryAfter = e.getHeaders().getField("Retry-After");
+                    if (retryAfter != null) {
+                        retryDelaySec = Math.max(0, Long.valueOf(retryAfter));
+                    }
+                } else if (responseCode != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) {
+                    throw e;
+                }
+                if (numRetries-- <= 0) {
+                    throw e;
+                }
+            } catch (IOException e) {
+                if (numRetries-- <= 0) {
+                    throw e;
+                }
+            }
+
+            if (retryDelaySec > 0) {
+                try {
+                    Thread.sleep(TimeUnit.SECONDS.toMillis(retryDelaySec));
+                } catch (InterruptedException e) {
+                    Clog.w(TAG, "Interrupted waiting for retry", e);
+                    throw new IOException(e);
+                }
+            }
+        }
+    }
+
+    private static byte[] getOrPost(URL url, Headers headers, byte[] data) throws IOException {
+        HttpURLConnection connection = null;
+        OutputStream outputStream = null;
+        InputStream inputStream = null;
+        final int oldTag = TrafficStats.getThreadStatsTag();
+        try {
+            TrafficStats.setThreadStatsTag(TRAFFIC_STATS_TAG);
+            connection = (HttpURLConnection) url.openConnection();
+            headers.apply(connection);
+
+            if (data != null) {
+                connection.setDoOutput(true);
+                connection.setFixedLengthStreamingMode(data.length);
+
+                outputStream = connection.getOutputStream();
+                IoUtils.writeToStream(outputStream, data);
+                checkResponseCode(connection);
+            }
+
+            checkResponseCode(connection);
+            inputStream = connection.getInputStream();
+            return IoUtils.readFromStream(inputStream);
+        } finally {
+            IoUtils.close(inputStream);
+            IoUtils.close(outputStream);
+            disconnect(connection);
+            TrafficStats.setThreadStatsTag(oldTag);
+        }
+    }
+
+    private static void checkResponseCode(HttpURLConnection connection) throws IOException {
+        int responseCode = connection.getResponseCode();
+        if (responseCode == HttpURLConnection.HTTP_OK) return;
+        String responseMessage = connection.getResponseMessage();
+        Headers responseHeaders = new Headers(connection.getHeaderFields());
+
+        InputStream errorStream = null;
+        try {
+            errorStream = connection.getErrorStream();
+            if (errorStream != null) {
+                byte[] responseBody = IoUtils.readFromStream(errorStream);
+                throw new HttpError(responseCode, responseMessage, responseHeaders, responseBody);
+            }
+            throw new HttpError(responseCode, responseMessage, responseHeaders);
+        } finally {
+            IoUtils.close(errorStream);
+        }
+    }
+
+    private static void disconnect(HttpURLConnection connection) {
+        if (connection == null) return;
+        connection.disconnect();
+    }
+
+    public static final class ContentType {
+        private ContentType() { }
+    }
+
+    public static final class Headers {
+        private final Map<String, List<String>> mFields;
+
+        public static final Headers NONE = new Headers.Builder().build();
+
+        private static Headers create(String contentType) {
+            return new Headers.Builder().set("Content-Type", contentType).build();
+        }
+
+        private Headers(Map<String, List<String>> fields) {
+            mFields = fields;
+        }
+
+        public void apply(@NonNull HttpURLConnection connection) {
+            for (Map.Entry<String, List<String>> entry : mFields.entrySet()) {
+                boolean first = true;
+                String key = entry.getKey();
+                for (String value: entry.getValue()) {
+                    if (first) {
+                        first = false;
+                        connection.setRequestProperty(key, value);
+                    } else {
+                        connection.addRequestProperty(key, value);
+                    }
+                }
+            }
+        }
+
+        public @Nullable String getField(@NonNull String key) {
+            List<String> values = getFieldValues(key);
+            return values == null ? null : values.get(0);
+        }
+
+        public @Nullable List<String> getFieldValues(@NonNull String key) {
+            return getFields().get(key);
+        }
+
+        public @NonNull Map<String, List<String>> getFields() {
+            return mFields;
+        }
+
+        public static final class Builder {
+            private static final Comparator<String> FIELD_NAME_COMPARATOR = (a, b) -> {
+                //noinspection StringEquality
+                if (a == b) {
+                    return 0;
+                } else if (a == null) {
+                    return -1;
+                } else if (b == null) {
+                    return 1;
+                } else {
+                    return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+                }
+            };
+            private final List<String> mNamesAndValues = new ArrayList<>();
+
+            public Builder() { }
+
+            public Builder(@NonNull Headers headers) {
+                for (Map.Entry<String, List<String>> entry : headers.mFields.entrySet()) {
+                    for (String value: entry.getValue()) {
+                        mNamesAndValues.add(entry.getKey());
+                        mNamesAndValues.add(value);
+                    }
+                }
+            }
+
+            public @NonNull Builder add(@NonNull String fieldName, @NonNull String value) {
+                mNamesAndValues.add(fieldName);
+                mNamesAndValues.add(value);
+                return this;
+            }
+
+            public @NonNull Builder set(@NonNull String fieldName, @NonNull String value) {
+                return removeAll(fieldName).add(fieldName, value);
+            }
+
+            private Builder removeAll(String fieldName) {
+                for (int i = 0; i < mNamesAndValues.size(); i += 2) {
+                    if (fieldName.equalsIgnoreCase(mNamesAndValues.get(i))) {
+                        mNamesAndValues.remove(i);
+                        mNamesAndValues.remove(i);
+                    }
+                }
+                return this;
+            }
+
+            public @NonNull Headers build() {
+                Map<String, List<String>> headers = new TreeMap<>(FIELD_NAME_COMPARATOR);
+
+                for (int i = 0; i < mNamesAndValues.size(); i += 2) {
+                    String fieldName = mNamesAndValues.get(i);
+                    String value = mNamesAndValues.get(i + 1);
+
+                    List<String> values = new ArrayList<>();
+                    List<String> others = headers.get(fieldName);
+                    if (others != null) {
+                        values.addAll(others);
+                    }
+                    values.add(value);
+                    headers.put(fieldName, Collections.unmodifiableList(values));
+                }
+
+                return new Headers(Collections.unmodifiableMap(headers));
+            }
+        }
+    }
+
+    public static final class HttpError extends IOException {
+        private static final long serialVersionUID = 1L;
+
+        private final int mCode;
+        private final String mMessage;
+        private final Headers mHeaders;
+        private final byte[] mBody;
+
+        private HttpError(int code, String message, Headers headers) {
+            this(code, message, headers, null);
+        }
+
+        private HttpError(int code, String message, Headers headers, byte[] body) {
+            super(code + " " + message);
+            mCode = code;
+            mMessage = message;
+            mHeaders = headers;
+            mBody = body;
+        }
+
+        public int getResponseCode() {
+            return mCode;
+        }
+
+        public @NonNull String getResponseMessage() {
+            return mMessage;
+        }
+
+        public @NonNull Headers getHeaders() {
+            return mHeaders;
+        }
+
+        public @Nullable byte[] getResponseBody() {
+            return mBody;
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/ImageLoader.java b/apps/Pump/java/com/android/pump/util/ImageLoader.java
new file mode 100644
index 0000000..6caab6d
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/ImageLoader.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+
+import com.android.pump.concurrent.Executors;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+@AnyThread
+public class ImageLoader {
+    private static final String TAG = Clog.tag(ImageLoader.class);
+
+    private final BitmapCache mBitmapCache = new BitmapCache();
+    private final OrientationCache mOrientationCache = new OrientationCache();
+    private final Executor mExecutor;
+    private final Set<Map.Entry<Executor, Callback>> mCallbacks = new ArraySet<>();
+    private final Map<Uri, List<Map.Entry<Executor, Callback>>> mLoadCallbacks = new ArrayMap<>();
+
+    @FunctionalInterface
+    public interface Callback {
+        void onImageLoaded(@NonNull Uri uri, @Nullable Bitmap bitmap);
+    }
+
+    public ImageLoader(@NonNull Executor executor) {
+        mExecutor = executor;
+    }
+
+    public void addCallback(@NonNull Callback callback) {
+        addCallback(callback, Executors.uiThreadExecutor());
+    }
+
+    public void addCallback(@NonNull Callback callback, @NonNull Executor executor) {
+        synchronized (this) { // TODO other lock
+            if (!mCallbacks.add(new SimpleEntry<>(executor, callback))) {
+                throw new IllegalArgumentException("Callback " + callback + " already added");
+            }
+        }
+    }
+
+    public void removeCallback(@NonNull Callback callback) {
+        removeCallback(callback, Executors.uiThreadExecutor());
+    }
+
+    public void removeCallback(@NonNull Callback callback, @NonNull Executor executor) {
+        synchronized (this) { // TODO other lock
+            if (!mCallbacks.remove(new SimpleEntry<>(executor, callback))) {
+                throw new IllegalArgumentException("Callback " + callback + " not found");
+            }
+        }
+    }
+
+    public void loadImage(@NonNull Uri uri, @NonNull Callback callback) {
+        loadImage(uri, callback, Executors.uiThreadExecutor());
+    }
+
+    public void loadImage(@NonNull Uri uri, @NonNull Callback callback,
+            @NonNull Executor executor) {
+        Bitmap bitmap;
+        Runnable loader = null;
+        synchronized (this) { // TODO other lock
+            bitmap = mBitmapCache.get(uri);
+            if (bitmap == null) {
+                List<Map.Entry<Executor, Callback>> callbacks = mLoadCallbacks.get(uri);
+                if (callbacks == null) {
+                    callbacks = new LinkedList<>();
+                    mLoadCallbacks.put(uri, callbacks);
+                    loader = new ImageLoaderTask(uri);
+                }
+                callbacks.add(new SimpleEntry<>(executor, callback));
+            }
+        }
+        if (bitmap != null) {
+            executor.execute(() -> callback.onImageLoaded(uri, bitmap));
+        } else if (loader != null) {
+            mExecutor.execute(loader);
+        }
+    }
+
+    public @Orientation int getOrientation(@NonNull Uri uri) {
+        return mOrientationCache.get(uri);
+    }
+
+    private class ImageLoaderTask implements Runnable {
+        private final Uri mUri;
+
+        private ImageLoaderTask(@NonNull Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public void run() {
+            try {
+                byte[] data;
+                if (Scheme.isFile(mUri)) {
+                    data = IoUtils.readFromFile(new File(mUri.getPath()));
+                } else if (Scheme.isHttp(mUri) || Scheme.isHttps(mUri)) {
+                    data = Http.get(mUri.toString());
+                } else {
+                    throw new IllegalArgumentException("Unknown scheme '" + mUri.getScheme() + "'");
+                }
+                Bitmap bitmap = decodeBitmapFromByteArray(data);
+                Set<Map.Entry<Executor, Callback>> callbacks;
+                List<Map.Entry<Executor, Callback>> loadCallbacks;
+                synchronized (ImageLoader.this) { // TODO proper lock
+                    if (bitmap != null) {
+                        mBitmapCache.put(mUri, bitmap);
+                        mOrientationCache.put(mUri, bitmap);
+                    }
+                    callbacks = new ArraySet<>(mCallbacks);
+                    loadCallbacks = mLoadCallbacks.get(mUri);
+                    mLoadCallbacks.remove(mUri);
+                }
+                for (Map.Entry<Executor, Callback> callback : callbacks) {
+                    callback.getKey().execute(() ->
+                            callback.getValue().onImageLoaded(mUri, bitmap));
+                }
+                for (Map.Entry<Executor, Callback> callback : loadCallbacks) {
+                    callback.getKey().execute(() ->
+                            callback.getValue().onImageLoaded(mUri, bitmap));
+                }
+            } catch (IOException | OutOfMemoryError e) {
+                Clog.e(TAG, "Failed to load image " + mUri, e);
+                // TODO remove from mLoadCallbacks
+            }
+        }
+
+        private @Nullable Bitmap decodeBitmapFromByteArray(@NonNull byte[] data) {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+
+            options.inJustDecodeBounds = true;
+            BitmapFactory.decodeByteArray(data, 0, data.length, options);
+
+            options.inJustDecodeBounds = false;
+            options.inSampleSize = 1; // TODO add scaling
+            return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/IoUtils.java b/apps/Pump/java/com/android/pump/util/IoUtils.java
new file mode 100644
index 0000000..27f55b6
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/IoUtils.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+@WorkerThread
+public final class IoUtils {
+    private static final String TAG = Clog.tag(IoUtils.class);
+
+    private IoUtils() { }
+
+    public static @NonNull byte[] readFromFile(@NonNull File file) throws IOException  {
+        InputStream inputStream = new FileInputStream(file);
+        try {
+            return readFromStream(inputStream);
+        } finally {
+            close(inputStream);
+        }
+    }
+
+    public static @NonNull byte[] readFromStream(@NonNull InputStream inputStream)
+            throws IOException {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        try {
+            int num;
+            byte[] buf = new byte[16384];
+            while ((num = inputStream.read(buf, 0, buf.length)) >= 0) {
+                buffer.write(buf, 0, num);
+            }
+            return buffer.toByteArray();
+        } finally {
+            close(buffer);
+        }
+    }
+
+    public static void writeToStream(@NonNull OutputStream outputStream, @NonNull byte[] buffer)
+            throws IOException {
+        outputStream.write(buffer);
+        outputStream.flush();
+    }
+
+    public static void close(@Nullable Closeable closeable) {
+        if (closeable == null) return;
+        try {
+            closeable.close();
+        } catch (IOException e) {
+            Clog.w(TAG, "Failed to close '" + closeable + "'", e);
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/OrderedCollection.java b/apps/Pump/java/com/android/pump/util/OrderedCollection.java
new file mode 100644
index 0000000..a25ca14
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/OrderedCollection.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public class OrderedCollection<T, U> extends AbstractCollection<T> {
+    private final KeyRetriever<T, U> mKeyRetriever;
+    private final KeyComparator<U> mKeyComparator;
+    private final List<T> mItems = new ArrayList<>();
+
+    @FunctionalInterface
+    public interface KeyRetriever<T, U> {
+        @NonNull U getKey(@NonNull T value);
+    }
+
+    @FunctionalInterface
+    public interface KeyComparator<U> {
+        int compare(@NonNull U lhs, @NonNull U rhs);
+    }
+
+    public OrderedCollection(@NonNull KeyRetriever<T, U> keyRetriever,
+            @NonNull KeyComparator<U> keyComparator) {
+        mKeyRetriever = keyRetriever;
+        mKeyComparator = keyComparator;
+    }
+
+    public @NonNull T get(@NonNull U key) {
+        int index = indexOfU(key);
+        if (index >= 0) {
+            return mItems.get(index);
+        }
+        throw new IllegalArgumentException();
+    }
+
+    @Override
+    public @NonNull Iterator<T> iterator() {
+        return mItems.iterator();
+    }
+
+    @Override
+    public int size() {
+        return mItems.size();
+    }
+
+    @Override
+    public boolean contains(@NonNull Object o) {
+        return indexOfO(o) >= 0;
+    }
+
+    @Override
+    public boolean add(@NonNull T e) {
+        int index = indexOfT(e);
+        if (index >= 0) {
+            return false;
+        }
+        mItems.add(~index, e);
+        return true;
+    }
+
+    @Override
+    public boolean remove(@NonNull Object o) {
+        int index = indexOfO(o);
+        if (index >= 0) {
+            mItems.remove(index);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void clear() {
+        mItems.clear();
+    }
+
+    @SuppressWarnings("unchecked")
+    private int indexOfO(@NonNull Object o) {
+        return indexOfT((T) o);
+    }
+
+    private int indexOfT(@NonNull T e) {
+        return indexOfU(mKeyRetriever.getKey(e));
+    }
+
+    private int indexOfU(@NonNull U key) {
+        int lo = 0;
+        int hi = mItems.size() - 1;
+
+        while (lo <= hi) {
+            int mid = (lo + hi) >>> 1;
+            int cmp = mKeyComparator.compare(mKeyRetriever.getKey(mItems.get(mid)), key);
+
+            if (cmp < 0) {
+                lo = mid + 1;
+            } else if (cmp > 0) {
+                hi = mid - 1;
+            } else {
+                return mid;
+            }
+        }
+        return ~lo;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Orientation.java b/apps/Pump/java/com/android/pump/util/Orientation.java
new file mode 100644
index 0000000..15ecd37
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Orientation.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import androidx.annotation.IntDef;
+
+@IntDef({
+    Orientation.UNKNOWN,
+    Orientation.LANDSCAPE,
+    Orientation.PORTRAIT
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface Orientation {
+    int UNKNOWN = 0;
+    int LANDSCAPE = 1;
+    int PORTRAIT = 2;
+}
diff --git a/apps/Pump/java/com/android/pump/util/OrientationCache.java b/apps/Pump/java/com/android/pump/util/OrientationCache.java
new file mode 100644
index 0000000..0f9093f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/OrientationCache.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import java.util.Map;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+
+@AnyThread
+class OrientationCache {
+    private final Map<Uri, Integer> mOrientationCache = new ArrayMap<>();
+
+    void put(@NonNull Uri key, @NonNull Bitmap bitmap) {
+        int orientation = bitmap.getWidth() < bitmap.getHeight() ?
+                Orientation.PORTRAIT : Orientation.LANDSCAPE;
+        mOrientationCache.put(key, orientation);
+    }
+
+    @Orientation int get(@NonNull Uri key) {
+        Integer value = mOrientationCache.get(key);
+        if (value != null) {
+            return value;
+        }
+        return Orientation.UNKNOWN;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/util/Scheme.java b/apps/Pump/java/com/android/pump/util/Scheme.java
new file mode 100644
index 0000000..ad544e3
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/util/Scheme.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.util;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+
+@AnyThread
+public final class Scheme {
+    private Scheme() { }
+
+    private final static String FILE = ContentResolver.SCHEME_FILE;
+    private final static String HTTP = "http";
+    private final static String HTTPS = "https";
+
+    public static boolean isFile(@NonNull Uri uri) {
+        return FILE.equals(uri.getScheme());
+    }
+
+    public static boolean isHttp(@NonNull Uri uri) {
+        return HTTP.equals(uri.getScheme());
+    }
+
+    public static boolean isHttps(@NonNull Uri uri) {
+        return HTTPS.equals(uri.getScheme());
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/widget/AspectRatioImageView.java b/apps/Pump/java/com/android/pump/widget/AspectRatioImageView.java
new file mode 100644
index 0000000..d67aa3f
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/widget/AspectRatioImageView.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+@UiThread
+public class AspectRatioImageView extends UriImageView {
+    public AspectRatioImageView(@NonNull Context context) {
+        super(context);
+    }
+
+    public AspectRatioImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AspectRatioImageView(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // TODO Make aspect ratio configurable
+        int aspectWidth = 16;
+        int aspectHeight = 9;
+
+        Drawable drawable = getDrawable();
+        if (drawable != null) {
+            // TODO Landscape/Portrait preference should be configurable
+            int intrinsicWidth = drawable.getIntrinsicWidth();
+            int intrinsicHeight = drawable.getIntrinsicHeight();
+
+            if (intrinsicWidth < intrinsicHeight) {
+                aspectWidth = 2;
+                aspectHeight = 3;
+            }
+        }
+
+        int width = getMeasuredWidth();
+        int height = width * aspectHeight / aspectWidth;
+        setMeasuredDimension(width, height);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/widget/HeaderRecyclerView.java b/apps/Pump/java/com/android/pump/widget/HeaderRecyclerView.java
new file mode 100644
index 0000000..c2a1c51
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/widget/HeaderRecyclerView.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
+import androidx.recyclerview.widget.RecyclerView;
+
+@UiThread
+public class HeaderRecyclerView extends RecyclerView {
+    public HeaderRecyclerView(@NonNull Context context) {
+        super(context);
+    }
+
+    public HeaderRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public HeaderRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExistingViews) {
+        if (adapter != null && !(adapter instanceof HeaderRecyclerAdapter)) {
+            adapter = new HeaderRecyclerAdapter<>(cast(adapter));
+        }
+        super.swapAdapter(adapter, removeAndRecycleExistingViews);
+    }
+
+    @Override
+    public void setAdapter(@Nullable Adapter adapter) {
+        if (adapter != null && !(adapter instanceof HeaderRecyclerAdapter)) {
+            adapter = new HeaderRecyclerAdapter<>(cast(adapter));
+        }
+        super.setAdapter(adapter);
+    }
+
+    @Override
+    public void setLayoutManager(@Nullable LayoutManager layoutManager) {
+        if (layoutManager instanceof GridLayoutManager) {
+            GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
+            // TODO override GridLayoutManager.setSpanCount() & setSpanSizeLookup()
+            SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
+            if (!(spanSizeLookup instanceof HeaderSpanSizeLookup)) {
+                gridLayoutManager.setSpanSizeLookup(new HeaderSpanSizeLookup(spanSizeLookup,
+                        gridLayoutManager.getSpanCount()));
+            }
+        }
+        super.setLayoutManager(layoutManager);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T cast(@Nullable Object obj) {
+        return (T) obj;
+    }
+
+    private static class HeaderRecyclerAdapter<VH extends ViewHolder> extends Adapter<ViewHolder> {
+        private final static int HEADER = Integer.MIN_VALUE;
+        //private final static int FOOTER = Integer.MAX_VALUE; TODO add footer
+
+        private final Adapter<VH> mDelegate;
+
+        private HeaderRecyclerAdapter(@NonNull Adapter<VH> delegate) {
+            mDelegate = delegate;
+            setHasStableIds(mDelegate.hasStableIds());
+            mDelegate.registerAdapterDataObserver(new AdapterDataObserver() {
+                @Override
+                public void onChanged() {
+                    notifyDataSetChanged();
+                }
+
+                @Override
+                public void onItemRangeChanged(int positionStart, int itemCount) {
+                    notifyItemRangeChanged(toPosition(positionStart), itemCount);
+                }
+
+                @Override
+                public void onItemRangeChanged(int positionStart, int itemCount,
+                                               @Nullable Object payload) {
+                    notifyItemRangeChanged(toPosition(positionStart), itemCount, payload);
+                }
+
+                @Override
+                public void onItemRangeInserted(int positionStart, int itemCount) {
+                    notifyItemRangeInserted(toPosition(positionStart), itemCount);
+                }
+
+                @Override
+                public void onItemRangeRemoved(int positionStart, int itemCount) {
+                    notifyItemRangeRemoved(toPosition(positionStart), itemCount);
+                }
+
+                @Override
+                public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+                    for (int i = 0; i < itemCount; ++i) {
+                        notifyItemMoved(toPosition(fromPosition + i), toPosition(toPosition + i));
+                    }
+                }
+            });
+        }
+
+        @Override
+        public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            if (viewType == HEADER) {
+                // TODO Handle this differently?
+                FrameLayout frameLayout = new FrameLayout(parent.getContext());
+                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                        FrameLayout.LayoutParams.MATCH_PARENT,
+                        FrameLayout.LayoutParams.WRAP_CONTENT);
+                frameLayout.setLayoutParams(params);
+
+                return new HeaderViewHolder(frameLayout);
+            } else {
+                return mDelegate.onCreateViewHolder(parent, viewType);
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+            if (isHeader(position)) {
+                // TODO
+            } else {
+                mDelegate.onBindViewHolder(cast(holder), fromPosition(position));
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ViewHolder holder, int position,
+                                     @NonNull List<Object> payloads) {
+            if (isHeader(position)) {
+                onBindViewHolder(holder, position);
+            } else {
+                mDelegate.onBindViewHolder(cast(holder), fromPosition(position), payloads);
+            }
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (isHeader(position)) {
+                return HEADER;
+            }
+            return mDelegate.getItemViewType(fromPosition(position));
+        }
+
+        @Override
+        public long getItemId(int position) {
+            if (isHeader(position)) {
+                return Long.MIN_VALUE;
+            }
+            return mDelegate.getItemId(fromPosition(position));
+        }
+
+        @Override
+        public int getItemCount() {
+            return getCount(mDelegate.getItemCount());
+        }
+
+        @Override
+        public void onViewRecycled(@NonNull ViewHolder holder) {
+            if (!(holder instanceof HeaderViewHolder)) {
+                mDelegate.onViewRecycled(cast(holder));
+            }
+        }
+
+        @Override
+        public boolean onFailedToRecycleView(@NonNull ViewHolder holder) {
+            if (!(holder instanceof HeaderViewHolder)) {
+                mDelegate.onFailedToRecycleView(cast(holder));
+            }
+            return false;
+        }
+
+        @Override
+        public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
+            if (!(holder instanceof HeaderViewHolder)) {
+                mDelegate.onViewAttachedToWindow(cast(holder));
+            }
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
+            if (!(holder instanceof HeaderViewHolder)) {
+                mDelegate.onViewDetachedFromWindow(cast(holder));
+            }
+        }
+
+        @Override
+        public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+            mDelegate.onAttachedToRecyclerView(recyclerView);
+        }
+
+        @Override
+        public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+            mDelegate.onDetachedFromRecyclerView(recyclerView);
+        }
+    }
+
+    private static class HeaderViewHolder extends ViewHolder {
+        private HeaderViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+    }
+
+    private static class HeaderSpanSizeLookup extends SpanSizeLookup {
+        private final SpanSizeLookup mDelegate;
+        private final int mSpanCount;
+
+        private HeaderSpanSizeLookup(@NonNull SpanSizeLookup delegate, int spanCount) {
+            mDelegate = delegate;
+            mSpanCount = spanCount;
+            setSpanIndexCacheEnabled(mDelegate.isSpanIndexCacheEnabled());
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            if (isHeader(position)) {
+                return mSpanCount;
+            }
+            return mDelegate.getSpanSize(fromPosition(position));
+        }
+
+        @Override
+        public int getSpanIndex(int position, int spanCount) {
+            if (isHeader(position)) {
+                return 0;
+            }
+            return mDelegate.getSpanIndex(fromPosition(position), spanCount);
+        }
+
+        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+            if (isHeader(adapterPosition)) {
+                return 0;
+            }
+            return mDelegate.getSpanIndex(fromPosition(adapterPosition), spanCount)
+                    + (hasHeader() ? 1 : 0);
+        }
+    }
+
+    private static boolean hasHeader() {
+        return true;
+    }
+
+    private static boolean isHeader(int position) {
+        return hasHeader() && position == 0;
+    }
+
+    private static int getCount(int count) {
+        if (hasHeader()) {
+            return count + 1;
+        }
+        return count;
+    }
+
+    private static int toPosition(int position) {
+        if (hasHeader()) {
+            return position + 1;
+        }
+        return position;
+    }
+
+    private static int fromPosition(int position) {
+        if (hasHeader()) {
+            return position - 1;
+        }
+        return position;
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/widget/PlaceholderImageView.java b/apps/Pump/java/com/android/pump/widget/PlaceholderImageView.java
new file mode 100644
index 0000000..0888d81
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/widget/PlaceholderImageView.java
@@ -0,0 +1,50 @@
+package com.android.pump.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import com.android.pump.R;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.core.content.ContextCompat;
+
+@UiThread
+public class PlaceholderImageView extends AppCompatImageView {
+    private static final @DrawableRes int PLACEHOLDER_DRAWABLE = R.drawable.ic_placeholder;
+
+    public PlaceholderImageView(@NonNull Context context) {
+        super(context);
+        initialize();
+    }
+
+    public PlaceholderImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        initialize();
+    }
+
+    public PlaceholderImageView(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize();
+    }
+
+    @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        if (drawable == null) {
+            drawable = ContextCompat.getDrawable(getContext(), PLACEHOLDER_DRAWABLE);
+        }
+        super.setImageDrawable(drawable);
+    }
+
+    private void initialize() {
+        if (getDrawable() == null) {
+            setImageDrawable(null);
+        }
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/widget/SortOrderSpinner.java b/apps/Pump/java/com/android/pump/widget/SortOrderSpinner.java
new file mode 100644
index 0000000..28d9a97
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/widget/SortOrderSpinner.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import com.android.pump.R;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.widget.AppCompatSpinner;
+
+@UiThread
+public class SortOrderSpinner extends AppCompatSpinner {
+    public SortOrderSpinner(@NonNull Context context) {
+        super(context);
+        initialize();
+    }
+
+    public SortOrderSpinner(@NonNull Context context, int mode) {
+        super(context, mode);
+        initialize();
+    }
+
+    public SortOrderSpinner(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        initialize();
+    }
+
+    public SortOrderSpinner(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize();
+    }
+
+    public SortOrderSpinner(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, int mode) {
+        super(context, attrs, defStyleAttr, mode);
+        initialize();
+    }
+
+    public SortOrderSpinner(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, int mode, @Nullable Resources.Theme popupTheme) {
+        super(context, attrs, defStyleAttr, mode, popupTheme);
+        initialize();
+    }
+
+    private void initialize() {
+        String options[] = {
+            "name",
+            "modified"
+        };
+        ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(
+                getContext(), android.R.layout.simple_spinner_item, options) {
+            @Override
+            public @NonNull View getView(int position, @Nullable View convertView,
+                    @NonNull ViewGroup parent) {
+                View view = super.getView(position, convertView, parent);
+                TextView textView = view.findViewById(android.R.id.text1);
+
+                CharSequence text = textView.getText();
+                SpannableString content = new SpannableString("Sort by " + text);
+                content.setSpan(new UnderlineSpan(), content.length() - text.length(),
+                        content.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                textView.setText(content);
+
+                return view;
+            }
+        };
+        adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item);
+
+        setAdapter(adapter);
+    }
+}
diff --git a/apps/Pump/java/com/android/pump/widget/UriImageView.java b/apps/Pump/java/com/android/pump/widget/UriImageView.java
new file mode 100644
index 0000000..0af5dd0
--- /dev/null
+++ b/apps/Pump/java/com/android/pump/widget/UriImageView.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pump.widget;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+
+import com.android.pump.util.Globals;
+import com.android.pump.util.ImageLoader;
+import com.android.pump.util.Scheme;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+@UiThread
+public class UriImageView extends PlaceholderImageView {
+    private Uri mUri;
+
+    public UriImageView(@NonNull Context context) {
+        super(context);
+    }
+
+    public UriImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public UriImageView(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setImageResource(@DrawableRes int resId) {
+        super.setImageResource(resId);
+        mUri = null;
+    }
+
+    @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        super.setImageDrawable(drawable);
+        mUri = null;
+    }
+
+    @Override
+    public void setImageBitmap(@Nullable Bitmap bm) {
+        super.setImageBitmap(bm);
+        mUri = null;
+    }
+
+    @Override
+    public void setImageURI(@Nullable Uri uri) {
+        setImageDrawable(null);
+        if (uri == null) {
+            return;
+        }
+        if (Scheme.isFile(uri) || Scheme.isHttp(uri) || Scheme.isHttps(uri)) {
+            mUri = uri;
+            loadImage();
+        } else {
+            super.setImageURI(uri);
+        }
+    }
+
+    private void loadImage() {
+        ImageLoader imageLoader = Globals.getImageLoader(getContext());
+        imageLoader.loadImage(mUri, (loadedUri, bitmap) -> {
+            if (mUri != null && mUri.equals(loadedUri)) {
+                setImageBitmap(bitmap);
+                mUri = loadedUri;
+            }
+        });
+    }
+}
diff --git a/apps/Pump/res/drawable/ic_favorite.xml b/apps/Pump/res/drawable/ic_favorite.xml
new file mode 100644
index 0000000..72227c9
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_favorite.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_home.xml b/apps/Pump/res/drawable/ic_home.xml
new file mode 100644
index 0000000..ce399ff
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_home.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_menu.xml b/apps/Pump/res/drawable/ic_menu.xml
new file mode 100644
index 0000000..3c9f038
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_menu.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_music_library.xml b/apps/Pump/res/drawable/ic_music_library.xml
new file mode 100644
index 0000000..de146c3
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_music_library.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_placeholder.xml b/apps/Pump/res/drawable/ic_placeholder.xml
new file mode 100644
index 0000000..be5a041
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_placeholder.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="1dp"
+    android:height="1dp"
+    android:viewportWidth="1"
+    android:viewportHeight="1">
+
+    <path
+        android:strokeWidth="0.05"
+        android:strokeColor="#ffff0000"
+        android:pathData="M0,0h1v1h-1zl1,1M0,1l1,-1"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_play.xml b/apps/Pump/res/drawable/ic_play.xml
new file mode 100644
index 0000000..af73f0b
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_play.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M10,16.5l6,-4.5 -6,-4.5v9zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_search.xml b/apps/Pump/res/drawable/ic_search.xml
new file mode 100644
index 0000000..a21b3d2
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_search.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/ic_video_library.xml b/apps/Pump/res/drawable/ic_video_library.xml
new file mode 100644
index 0000000..866d544
--- /dev/null
+++ b/apps/Pump/res/drawable/ic_video_library.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+
+    <path
+        android:fillColor="#ff000000"
+        android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
+
+</vector>
diff --git a/apps/Pump/res/drawable/selector_bottom_navigation.xml b/apps/Pump/res/drawable/selector_bottom_navigation.xml
new file mode 100644
index 0000000..1101b54
--- /dev/null
+++ b/apps/Pump/res/drawable/selector_bottom_navigation.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="true" android:color="?colorControlActivated"/>
+    <item android:color="?colorControlNormal"/>
+</selector>
diff --git a/apps/Pump/res/drawable/shadow.xml b/apps/Pump/res/drawable/shadow.xml
new file mode 100644
index 0000000..dbe64c8
--- /dev/null
+++ b/apps/Pump/res/drawable/shadow.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <gradient
+        android:angle="270"
+        android:startColor="#00202124"
+        android:endColor="#ff202124"/>
+
+</shape>
diff --git a/apps/Pump/res/layout/activity_album_details.xml b/apps/Pump/res/layout/activity_album_details.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/activity_album_details.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/activity_artist_details.xml b/apps/Pump/res/layout/activity_artist_details.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/activity_artist_details.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/activity_genre_details.xml b/apps/Pump/res/layout/activity_genre_details.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/activity_genre_details.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/activity_movie_details.xml b/apps/Pump/res/layout/activity_movie_details.xml
new file mode 100644
index 0000000..157b01f
--- /dev/null
+++ b/apps/Pump/res/layout/activity_movie_details.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/activity_movie_details_image"
+        android:layout_width="0dp"
+        android:layout_height="309dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:src="@tools:sample/backgrounds/scenic"/>
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@null"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:elevation="0dp">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/activity_movie_details_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?actionBarSize"/>
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/activity_movie_details_play"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        app:layout_constraintTop_toTopOf="@id/activity_movie_details_image"
+        app:layout_constraintBottom_toBottomOf="@id/activity_movie_details_image"
+        app:layout_constraintStart_toStartOf="@id/activity_movie_details_image"
+        app:layout_constraintEnd_toEndOf="@id/activity_movie_details_image"
+        app:srcCompat="@drawable/ic_play"
+        app:tint="?colorControlNormal"
+
+        android:clickable="true"
+        android:focusable="true"
+        android:foreground="?selectableItemBackground"/>
+
+    <android.view.View
+        android:layout_width="0dp"
+        android:layout_height="77dp"
+        android:background="@drawable/shadow"
+        app:layout_constraintBottom_toBottomOf="@id/activity_movie_details_image"
+        app:layout_constraintStart_toStartOf="@id/activity_movie_details_image"
+        app:layout_constraintEnd_toEndOf="@id/activity_movie_details_image"/>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/activity_movie_details_poster"
+        android:layout_width="108dp"
+        android:layout_height="162dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintBottom_toBottomOf="@id/activity_movie_details_image"
+        app:layout_constraintStart_toStartOf="@id/activity_movie_details_image"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/activity_movie_details_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:layout_marginTop="24dp"
+        android:textSize="18sp"
+        android:maxLines="3"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/activity_movie_details_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/activity_movie_details_attributes"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:textSize="12sp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/activity_movie_details_title"
+        app:layout_constraintStart_toStartOf="@id/activity_movie_details_title"
+        app:layout_constraintEnd_toEndOf="@id/activity_movie_details_title"
+        tools:text="1h 20m"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/activity_movie_details_synopsis"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:maxLines="3"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/activity_movie_details_attributes"
+        app:layout_constraintStart_toStartOf="@id/activity_movie_details_title"
+        app:layout_constraintEnd_toEndOf="@id/activity_movie_details_title"
+        tools:text="@tools:sample/lorem/random"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/activity_other_details.xml b/apps/Pump/res/layout/activity_other_details.xml
new file mode 100644
index 0000000..4ff1606
--- /dev/null
+++ b/apps/Pump/res/layout/activity_other_details.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/activity_other_details_image"
+        android:layout_width="0dp"
+        android:layout_height="309dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:src="@tools:sample/backgrounds/scenic"/>
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@null"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:elevation="0dp">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/activity_other_details_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?actionBarSize"/>
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/activity_other_details_play"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        app:layout_constraintTop_toTopOf="@id/activity_other_details_image"
+        app:layout_constraintBottom_toBottomOf="@id/activity_other_details_image"
+        app:layout_constraintStart_toStartOf="@id/activity_other_details_image"
+        app:layout_constraintEnd_toEndOf="@id/activity_other_details_image"
+        app:srcCompat="@drawable/ic_play"
+        app:tint="?colorControlNormal"
+
+        android:clickable="true"
+        android:focusable="true"
+        android:foreground="?selectableItemBackground"/>
+
+    <android.view.View
+        android:layout_width="0dp"
+        android:layout_height="77dp"
+        android:background="@drawable/shadow"
+        app:layout_constraintBottom_toBottomOf="@id/activity_other_details_image"
+        app:layout_constraintStart_toStartOf="@id/activity_other_details_image"
+        app:layout_constraintEnd_toEndOf="@id/activity_other_details_image"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/activity_other_details_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:layout_marginTop="24dp"
+        android:textSize="18sp"
+        android:maxLines="3"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/activity_other_details_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/activity_other_details_attributes"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:textSize="12sp"
+        android:maxLines="5"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/activity_other_details_title"
+        app:layout_constraintStart_toStartOf="@id/activity_other_details_title"
+        app:layout_constraintEnd_toEndOf="@id/activity_other_details_title"
+        tools:text="1h 20m"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/activity_playlist_details.xml b/apps/Pump/res/layout/activity_playlist_details.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/activity_playlist_details.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/activity_pump.xml b/apps/Pump/res/layout/activity_pump.xml
new file mode 100644
index 0000000..0d0256c
--- /dev/null
+++ b/apps/Pump/res/layout/activity_pump.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.drawerlayout.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/activity_pump_drawer_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <android.widget.LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:animateLayoutChanges="true"
+        android:orientation="vertical">
+
+        <androidx.coordinatorlayout.widget.CoordinatorLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+            <com.google.android.material.appbar.AppBarLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <androidx.appcompat.widget.Toolbar
+                    android:id="@+id/activity_pump_toolbar"
+                    android:layout_width="match_parent"
+                    android:layout_height="?actionBarSize"
+                    app:navigationIcon="@drawable/ic_menu"
+                    app:layout_scrollFlags="scroll|enterAlways"/>
+
+                <com.google.android.material.tabs.TabLayout
+                    android:id="@+id/activity_pump_tab_layout"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"/>
+
+                <!--android.view.View
+                    android:layout_width="match_parent"
+                    android:layout_height="1dp"
+                    android:background="#5f6368"/-->
+
+            </com.google.android.material.appbar.AppBarLayout>
+
+            <!-- TODO Switch to androidx.viewpager2.widget.ViewPager2 -->
+            <androidx.viewpager.widget.ViewPager
+                android:id="@+id/activity_pump_view_pager"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+
+        </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+        <com.google.android.material.bottomnavigation.BottomNavigationView
+            android:id="@+id/activity_pump_bottom_navigation_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="bottom"
+            android:background="?colorPrimary"
+            app:itemIconTint="@drawable/selector_bottom_navigation"
+            app:itemTextColor="@drawable/selector_bottom_navigation"
+            app:labelVisibilityMode="labeled"
+            app:menu="@menu/activity_pump_bottom_navigation_view"/>
+
+    </android.widget.LinearLayout>
+
+    <com.google.android.material.navigation.NavigationView
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:fitsSystemWindows="true"
+        app:menu="@menu/activity_pump_bottom_navigation_view"/>
+
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/apps/Pump/res/layout/activity_series_details.xml b/apps/Pump/res/layout/activity_series_details.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/activity_series_details.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/album.xml b/apps/Pump/res/layout/album.xml
new file mode 100644
index 0000000..0e024ea
--- /dev/null
+++ b/apps/Pump/res/layout/album.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/album_image"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/album_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/album_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/album_artist"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/album_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Artist"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/artist.xml b/apps/Pump/res/layout/artist.xml
new file mode 100644
index 0000000..fe1904e
--- /dev/null
+++ b/apps/Pump/res/layout/artist.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/artist_image"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/artist_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/artist_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Name"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/audio.xml b/apps/Pump/res/layout/audio.xml
new file mode 100644
index 0000000..57f1c21
--- /dev/null
+++ b/apps/Pump/res/layout/audio.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/audio_image"
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:layout_marginTop="14dp"
+        android:layout_marginBottom="14dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/audio_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="11dp"
+        android:layout_marginEnd="16dp"
+        android:ellipsize="end"
+        android:maxLines="1"
+        app:layout_constraintStart_toEndOf="@id/audio_image"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/audio_options"
+        tools:text="Title"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/audio_artist"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        app:layout_constraintStart_toStartOf="@id/audio_title"
+        app:layout_constraintTop_toBottomOf="@id/audio_title"
+        app:layout_constraintEnd_toEndOf="@id/audio_title"
+        tools:text="Artist"/>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/audio_options"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginTop="20dp"
+        android:layout_marginBottom="20dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/ic_menu"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/fragment_album.xml b/apps/Pump/res/layout/fragment_album.xml
new file mode 100644
index 0000000..2b324d7
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_album.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_album_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="2"
+    tools:listitem="@layout/album"/>
diff --git a/apps/Pump/res/layout/fragment_artist.xml b/apps/Pump/res/layout/fragment_artist.xml
new file mode 100644
index 0000000..5537c75
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_artist.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_artist_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="2"
+    tools:listitem="@layout/artist"/>
diff --git a/apps/Pump/res/layout/fragment_audio.xml b/apps/Pump/res/layout/fragment_audio.xml
new file mode 100644
index 0000000..1941d6c
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_audio.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_audio_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="16dp"
+    android:paddingEnd="16dp"
+    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+    tools:listitem="@layout/audio"/>
diff --git a/apps/Pump/res/layout/fragment_genre.xml b/apps/Pump/res/layout/fragment_genre.xml
new file mode 100644
index 0000000..a1cbb08
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_genre.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_genre_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="2"
+    tools:listitem="@layout/genre"/>
diff --git a/apps/Pump/res/layout/fragment_home.xml b/apps/Pump/res/layout/fragment_home.xml
new file mode 100644
index 0000000..6766635
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_home.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<android.view.View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ffb6c1"/>
diff --git a/apps/Pump/res/layout/fragment_movie.xml b/apps/Pump/res/layout/fragment_movie.xml
new file mode 100644
index 0000000..05cba1a
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_movie.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_movie_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="3"
+    tools:listitem="@layout/movie"/>
diff --git a/apps/Pump/res/layout/fragment_other.xml b/apps/Pump/res/layout/fragment_other.xml
new file mode 100644
index 0000000..045031b
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_other.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_other_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="6"
+    tools:listitem="@layout/other"/>
diff --git a/apps/Pump/res/layout/fragment_playlist.xml b/apps/Pump/res/layout/fragment_playlist.xml
new file mode 100644
index 0000000..3967526
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_playlist.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_playlist_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="2"
+    tools:listitem="@layout/playlist"/>
diff --git a/apps/Pump/res/layout/fragment_series.xml b/apps/Pump/res/layout/fragment_series.xml
new file mode 100644
index 0000000..467f54d
--- /dev/null
+++ b/apps/Pump/res/layout/fragment_series.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_series_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+    app:spanCount="3"
+    tools:listitem="@layout/movie"/>
diff --git a/apps/Pump/res/layout/genre.xml b/apps/Pump/res/layout/genre.xml
new file mode 100644
index 0000000..9a605aa
--- /dev/null
+++ b/apps/Pump/res/layout/genre.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/genre_image"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/genre_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Genre"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/header.xml b/apps/Pump/res/layout/header.xml
new file mode 100644
index 0000000..74221e3
--- /dev/null
+++ b/apps/Pump/res/layout/header.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.pump.widget.SortOrderSpinner
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"/>
diff --git a/apps/Pump/res/layout/movie.xml b/apps/Pump/res/layout/movie.xml
new file mode 100644
index 0000000..e16d99a
--- /dev/null
+++ b/apps/Pump/res/layout/movie.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/movie_image"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="2:3"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/movie_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/movie_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/other.xml b/apps/Pump/res/layout/other.xml
new file mode 100644
index 0000000..5fdff9c
--- /dev/null
+++ b/apps/Pump/res/layout/other.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.pump.widget.AspectRatioImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/other_image"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:scaleType="centerCrop"
+    tools:src="@tools:sample/avatars"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground"/>
diff --git a/apps/Pump/res/layout/playlist.xml b/apps/Pump/res/layout/playlist.xml
new file mode 100644
index 0000000..8ee83a5
--- /dev/null
+++ b/apps/Pump/res/layout/playlist.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/playlist_image_0"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/playlist_image_1"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/playlist_image_1"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="@id/playlist_image_0"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/playlist_image_2"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toBottomOf="@id/playlist_image_0"
+        app:layout_constraintStart_toStartOf="@id/playlist_image_0"
+        app:layout_constraintEnd_toEndOf="@id/playlist_image_0"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/playlist_image_3"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toBottomOf="@id/playlist_image_1"
+        app:layout_constraintStart_toStartOf="@id/playlist_image_1"
+        app:layout_constraintEnd_toEndOf="@id/playlist_image_1"
+        app:layout_constraintDimensionRatio="1:1"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/playlist_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/playlist_image_2"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/playlist_artists"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/playlist_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Artists"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/layout/series.xml b/apps/Pump/res/layout/series.xml
new file mode 100644
index 0000000..222297a
--- /dev/null
+++ b/apps/Pump/res/layout/series.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    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="wrap_content"
+
+    android:clickable="true"
+    android:focusable="true"
+    android:foreground="?selectableItemBackground">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/series_image"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintDimensionRatio="2:3"
+        tools:src="@tools:sample/avatars"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/series_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:maxLines="1"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@id/series_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:text="Title"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/Pump/res/menu/activity_pump.xml b/apps/Pump/res/menu/activity_pump.xml
new file mode 100644
index 0000000..9f5c54f
--- /dev/null
+++ b/apps/Pump/res/menu/activity_pump.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu 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">
+
+    <item
+        android:title="Cast"
+        app:showAsAction="ifRoom"
+        app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
+        tools:icon="@drawable/mr_button_dark"/>
+
+    <item
+        android:icon="@drawable/ic_search"
+        android:title="Search"
+        app:showAsAction="ifRoom"
+        app:actionViewClass="androidx.appcompat.widget.SearchView"/>
+
+</menu>
diff --git a/apps/Pump/res/menu/activity_pump_bottom_navigation_view.xml b/apps/Pump/res/menu/activity_pump_bottom_navigation_view.xml
new file mode 100644
index 0000000..bc41fac
--- /dev/null
+++ b/apps/Pump/res/menu/activity_pump_bottom_navigation_view.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:showIn="navigation_view">
+
+    <item
+        android:id="@+id/menu_home"
+        android:icon="@drawable/ic_home"
+        android:title="Home"/>
+
+    <item
+        android:id="@+id/menu_video"
+        android:icon="@drawable/ic_video_library"
+        android:title="Videos"/>
+
+    <item
+        android:id="@+id/menu_audio"
+        android:icon="@drawable/ic_music_library"
+        android:title="Audios"/>
+
+    <item
+        android:id="@+id/menu_favorite"
+        android:icon="@drawable/ic_favorite"
+        android:title="Favorites"/>
+
+</menu>
diff --git a/apps/Pump/res/values/colors.xml b/apps/Pump/res/values/colors.xml
new file mode 100644
index 0000000..a220ace
--- /dev/null
+++ b/apps/Pump/res/values/colors.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <color name="colorForeground">#ff0000</color>
+    <color name="colorBackground">#202124</color>
+
+    <color name="colorPrimary">#313235</color>
+    <color name="colorPrimaryDark">#313235</color>
+    <color name="colorAccent">#ff0000</color>
+    <color name="colorControlNormal">#f8f9fa</color>
+    <color name="colorControlActivated">#669df6</color>
+    <color name="colorControlHighlight">#ff0000</color>
+    <color name="colorButtonNormal">#ff0000</color>
+
+    <color name="textColorPrimary">#f8f9fa</color>
+    <color name="textColorSecondary">#80868b</color>
+    <color name="textColorTertiary">#f8f9fa</color>
+</resources>
diff --git a/apps/Pump/res/values/styles.xml b/apps/Pump/res/values/styles.xml
new file mode 100644
index 0000000..80ce0a1
--- /dev/null
+++ b/apps/Pump/res/values/styles.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <style name="PumpTheme" parent="Theme.MaterialComponents.NoActionBar">
+        <item name="android:colorForeground">@color/colorForeground</item>
+        <item name="android:colorBackground">@color/colorBackground</item>
+
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="colorControlNormal">@color/colorControlNormal</item>
+        <item name="colorControlActivated">@color/colorControlActivated</item>
+        <item name="colorControlHighlight">@color/colorControlHighlight</item>
+        <item name="colorButtonNormal">@color/colorButtonNormal</item>
+
+        <item name="android:textColorPrimary">@color/textColorPrimary</item>
+        <item name="android:textColorSecondary">@color/textColorSecondary</item>
+        <item name="android:textColorTertiary">@color/textColorTertiary</item>
+
+        <item name="tabStyle">@style/PumpTheme.TabLayout</item>
+        <item name="toolbarNavigationButtonStyle">@style/PumpTheme.ToolbarNavigationButtonStyle</item>
+        <item name="android:spinnerItemStyle">@style/PumpTheme.SpinnerItem</item>
+    </style>
+
+    <style name="PumpTheme.TabLayout" parent="Widget.MaterialComponents.TabLayout.Colored">
+        <item name="android:background">?android:colorPrimary</item>
+        <item name="tabIndicatorColor">?android:colorControlActivated</item>
+        <item name="tabRippleColor">?android:colorControlHighlight</item>
+    </style>
+
+    <style name="PumpTheme.ToolbarNavigationButtonStyle" parent="Widget.AppCompat.Toolbar.Button.Navigation">
+        <item name="tint">?android:textColorPrimary</item>
+    </style>
+
+    <style name="PumpTheme.SpinnerItem" parent="Widget.AppCompat.TextView.SpinnerItem">
+        <item name="android:gravity">end</item>
+    </style>
+
+</resources>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..02907f7
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+    repositories {
+        jcenter()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0-alpha08'
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+        maven { url 'https://maven.google.com' }
+        google()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/data/README.md b/data/README.md
new file mode 100644
index 0000000..0bd9982
--- /dev/null
+++ b/data/README.md
@@ -0,0 +1,5 @@
+# Dummy data
+
+Copy to device:
+
+    tar cvf - sdcard | adb exec-in tar xf -
diff --git a/data/flash b/data/flash
new file mode 100755
index 0000000..649458d
--- /dev/null
+++ b/data/flash
@@ -0,0 +1,4 @@
+#/bin/sh
+
+cd `dirname $0`
+tar cvf - sdcard | adb exec-in tar xf -
diff --git "a/data/sdcard/Movies/A Clockwork Orange \0501971\051.webm" "b/data/sdcard/Movies/A Clockwork Orange \0501971\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/A Clockwork Orange \0501971\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Alien \0501979\051.webm" "b/data/sdcard/Movies/Alien \0501979\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Alien \0501979\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Alien 3 \0501992\051.webm" "b/data/sdcard/Movies/Alien 3 \0501992\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Alien 3 \0501992\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Alien: Covenant \0502017\051.webm" "b/data/sdcard/Movies/Alien: Covenant \0502017\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Alien: Covenant \0502017\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Alien: Resurrection \0501997\051.webm" "b/data/sdcard/Movies/Alien: Resurrection \0501997\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Alien: Resurrection \0501997\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Aliens \0501986\051.webm" "b/data/sdcard/Movies/Aliens \0501986\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Aliens \0501986\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Avengers: Age of Ultron \0502015\051.webm" "b/data/sdcard/Movies/Avengers: Age of Ultron \0502015\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Avengers: Age of Ultron \0502015\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Avengers: Infinity War \0502018\051.webm" "b/data/sdcard/Movies/Avengers: Infinity War \0502018\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Avengers: Infinity War \0502018\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Batman Begins \0502005\051.webm" "b/data/sdcard/Movies/Batman Begins \0502005\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Batman Begins \0502005\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E01.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E01.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E01.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E02.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E02.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E02.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E03.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E03.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E03.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E04.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E04.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E04.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E05.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E05.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E05.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E06.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E06.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E06.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E07.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E07.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E07.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E08.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E08.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E08.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E09.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E09.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E09.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E10.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E10.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E10.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E11.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E11.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E11.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E12.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E12.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E12.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E13.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E13.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 1/Battlestar Galactica \0502004\051 S01E13.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E01.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E01.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E01.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E02.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E02.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E02.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E03.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E03.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E03.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E04.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E04.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E04.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E05.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E05.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E05.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E06.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E06.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E06.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E07.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E07.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E07.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E08.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E08.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E08.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E09.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E09.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E09.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E10.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E10.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E10.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E11.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E11.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E11.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E12.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E12.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E12.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E13.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E13.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E13.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E14.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E14.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E14.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E15.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E15.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E15.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E16.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E16.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E16.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E17.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E17.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E17.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E18.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E18.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E18.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E19.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E19.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E19.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E20.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E20.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 2/Battlestar Galactica \0502004\051 S02E20.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E01.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E01.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E01.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E02.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E02.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E02.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E03.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E03.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E03.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E04.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E04.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E04.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E05.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E05.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E05.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E06.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E06.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E06.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E07.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E07.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E07.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E08.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E08.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E08.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E09.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E09.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E09.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E10.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E10.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E10.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E11.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E11.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E11.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E12.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E12.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E12.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E13.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E13.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E13.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E14.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E14.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E14.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E15.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E15.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E15.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E16.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E16.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E16.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E17.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E17.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E17.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E18.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E18.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E18.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E19.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E19.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E19.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E20.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E20.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 3/Battlestar Galactica \0502004\051 S03E20.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E01.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E01.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E01.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E02.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E02.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E02.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E03.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E03.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E03.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E04.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E04.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E04.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E05.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E05.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E05.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E06.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E06.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E06.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E07.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E07.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E07.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E08.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E08.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E08.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E09.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E09.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E09.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E10.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E10.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E10.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E11.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E11.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E11.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E12.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E12.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E12.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E13.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E13.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E13.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E14.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E14.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E14.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E15.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E15.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E15.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E16.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E16.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E16.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E17.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E17.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E17.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E18.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E18.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E18.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E19.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E19.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E19.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E20.webm" "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E20.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Battlestar Galactica \0502004\051/Season 4/Battlestar Galactica \0502004\051 S04E20.webm"
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E01.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E02.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E03.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E04.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E05.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E06.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E07.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E08.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E09.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E10.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E11.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E12.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E13.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E14.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E15.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E16.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E17.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E18.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E19.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E20.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E21.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E22.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E23.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E23.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E23.webm
Binary files differ
diff --git a/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E24.webm b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E24.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Battlestar Galactica/Season 1/Battlestar Galactica S01E24.webm
Binary files differ
diff --git "a/data/sdcard/Movies/Breakfast at Tiffany\047s \0501961\051.webm" "b/data/sdcard/Movies/Breakfast at Tiffany\047s \0501961\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Breakfast at Tiffany\047s \0501961\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Das Boot \0501981\051.webm" "b/data/sdcard/Movies/Das Boot \0501981\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Das Boot \0501981\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Fargo \0501996\051.webm" "b/data/sdcard/Movies/Fargo \0501996\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Fargo \0501996\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Fight Club \0501999\051.webm" "b/data/sdcard/Movies/Fight Club \0501999\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Fight Club \0501999\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Forrest Gump \0501994\051.webm" "b/data/sdcard/Movies/Forrest Gump \0501994\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Forrest Gump \0501994\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Full Metal Jacket \0501987\051.webm" "b/data/sdcard/Movies/Full Metal Jacket \0501987\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Full Metal Jacket \0501987\051.webm"
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E01.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E02.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E03.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E04.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E05.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E06.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E07.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E08.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E09.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E10.webm b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 01/Game of Thrones S01E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E01.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E02.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E03.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E04.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E05.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E06.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E07.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E08.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E09.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E10.webm b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 02/Game of Thrones S02E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E01.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E02.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E03.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E04.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E05.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E06.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E07.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E08.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E09.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E10.webm b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 03/Game of Thrones S03E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E01.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E02.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E03.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E04.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E05.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E06.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E07.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E08.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E09.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E10.webm b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 04/Game of Thrones S04E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E01.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E02.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E03.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E04.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E05.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E06.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E07.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E08.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E09.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E10.webm b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 05/Game of Thrones S05E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E01.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E02.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E03.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E04.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E05.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E06.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E07.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E08.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E09.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E10.webm b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 06/Game of Thrones S06E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E01.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E02.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E03.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E04.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E05.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E06.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E07.webm b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Game of Thrones/Season 07/Game of Thrones S07E07.webm
Binary files differ
diff --git "a/data/sdcard/Movies/It \0502017\051.webm" "b/data/sdcard/Movies/It \0502017\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/It \0502017\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Kiki\047s Delivery Service \0501989\051.webm" "b/data/sdcard/Movies/Kiki\047s Delivery Service \0501989\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Kiki\047s Delivery Service \0501989\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/My Neighbor Totoro \0501988\051.webm" "b/data/sdcard/Movies/My Neighbor Totoro \0501988\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/My Neighbor Totoro \0501988\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Ponyo \0502008\051.webm" "b/data/sdcard/Movies/Ponyo \0502008\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Ponyo \0502008\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Porco Rosso \0501992\051.webm" "b/data/sdcard/Movies/Porco Rosso \0501992\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Porco Rosso \0501992\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Princess Mononoke \0501997\051.webm" "b/data/sdcard/Movies/Princess Mononoke \0501997\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Princess Mononoke \0501997\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Prometheus \0502012\051.webm" "b/data/sdcard/Movies/Prometheus \0502012\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Prometheus \0502012\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Pulp Fiction \0501994\051.webm" "b/data/sdcard/Movies/Pulp Fiction \0501994\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Pulp Fiction \0501994\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Spirited Away \0502001\051.webm" "b/data/sdcard/Movies/Spirited Away \0502001\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Spirited Away \0502001\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Terminator 2 \0501991\051.webm" "b/data/sdcard/Movies/Terminator 2 \0501991\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Terminator 2 \0501991\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Terminator 3: Rise of the Machines \0502003\051.webm" "b/data/sdcard/Movies/Terminator 3: Rise of the Machines \0502003\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Terminator 3: Rise of the Machines \0502003\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Terminator Genisys \0502015\051.webm" "b/data/sdcard/Movies/Terminator Genisys \0502015\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Terminator Genisys \0502015\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Terminator Salvation \0502009\051.webm" "b/data/sdcard/Movies/Terminator Salvation \0502009\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Terminator Salvation \0502009\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Avengers \0502012\051.webm" "b/data/sdcard/Movies/The Avengers \0502012\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Avengers \0502012\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Big Lebowski \0501998\051.webm" "b/data/sdcard/Movies/The Big Lebowski \0501998\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Big Lebowski \0501998\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Dark Knight \0502008\051.webm" "b/data/sdcard/Movies/The Dark Knight \0502008\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Dark Knight \0502008\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Dark Knight Rises \0502012\051.webm" "b/data/sdcard/Movies/The Dark Knight Rises \0502012\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Dark Knight Rises \0502012\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Seventh Seal \0501957\051.webm" "b/data/sdcard/Movies/The Seventh Seal \0501957\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Seventh Seal \0501957\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Shining \0501980\051.webm" "b/data/sdcard/Movies/The Shining \0501980\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Shining \0501980\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/The Terminator \0501984\051.webm" "b/data/sdcard/Movies/The Terminator \0501984\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/The Terminator \0501984\051.webm"
Binary files differ
diff --git "a/data/sdcard/Movies/Trainspotting \0501996\051.webm" "b/data/sdcard/Movies/Trainspotting \0501996\051.webm"
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ "b/data/sdcard/Movies/Trainspotting \0501996\051.webm"
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E23.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E23.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E23.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E24.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E24.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 1/Xena: Warrior Princess S01E24.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 2/Xena: Warrior Princess S02E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 3/Xena: Warrior Princess S03E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 4/Xena: Warrior Princess S04E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 5/Xena: Warrior Princess S05E22.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E01.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E01.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E01.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E02.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E02.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E02.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E03.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E03.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E03.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E04.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E04.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E04.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E05.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E05.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E05.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E06.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E06.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E06.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E07.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E07.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E07.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E08.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E08.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E08.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E09.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E09.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E09.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E10.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E10.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E10.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E11.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E11.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E11.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E12.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E12.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E12.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E13.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E13.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E13.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E14.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E14.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E14.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E15.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E15.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E15.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E16.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E16.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E16.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E17.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E17.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E17.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E18.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E18.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E18.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E19.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E19.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E19.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E20.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E20.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E20.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E21.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E21.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E21.webm
Binary files differ
diff --git a/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E22.webm b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E22.webm
new file mode 100644
index 0000000..38e17b8
--- /dev/null
+++ b/data/sdcard/Movies/Xena: Warrior Princess/Season 6/Xena: Warrior Princess S06E22.webm
Binary files differ
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b31dd16
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,20 @@
+#
+# Copyright 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.gradle.jvmargs=-Xmx4096m
+android.useAndroidX=true
+android.enableJetifier=false
+android.injected.testOnly=false
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2e7d3a5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Aug 03 08:18:54 KST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3888cae
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include(':Pump')
+project(':Pump').projectDir = new File(rootDir, 'apps/Pump')
+
+// apply from: '../../../../frameworks/support/include-support-library.gradle'