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'