diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4838c7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.classpath +.project +bin/* +gen/* +private/* +nbandroid/* +.idea +subsonic-android.iml +releases/ +proguard_logs/ +/gen/ +/out/ +.gradle/* +/build/ +local.properties +*Thumbs.db \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..91dd332 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ServerProxy"] + path = ServerProxy + url = https://github.com/daneren2005/ServerProxy.git diff --git a/Audinaut.iml b/Audinaut.iml new file mode 100644 index 0000000..561e0d8 --- /dev/null +++ b/Audinaut.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 54999e6..ff1c333 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # Audinaut -A Libresonic client for Android. +
+A FOSS Libresonic client for Android. + +## Building +``` +git submodule update --init +gradle assemble +``` + +## SDK Project Dependencies +Under sdk -> extras:
+android -> support -> v7 -> appcompat
+android -> support -> v7 -> mediarouter
+ +## SDK Library Dependencies +android -> support -> v4 -> android-support-v4.jar
+android -> support -> v7 -> appcompat -> libs android-support-v7-appcompat.jar
+android -> support -> v7 -> mediarouter -> libs -> android-support-v7-mediarouter.jar
diff --git a/ServerProxy b/ServerProxy new file mode 160000 index 0000000..a4d9573 --- /dev/null +++ b/ServerProxy @@ -0,0 +1 @@ +Subproject commit a4d957353db2634906e0d5099d7a078a111bfab9 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..e8fa30f --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +*.iml diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..dcd8a59 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,63 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + useLibrary 'org.apache.http.legacy' + + defaultConfig { + applicationId "github.nvllsvm.audinaut" + minSdkVersion 19 + targetSdkVersion 23 + versionCode 186 + versionName '0.1.0' + setProperty("archivesBaseName", "Audinaut $versionName") + resConfigs "de", "es", "fr", "hu", "nl", "pt-rPT", "ru", "sv" + } + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard.cfg' + zipAlignEnabled true + } + fix { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard.cfg' + zipAlignEnabled true + } + } + + packagingOptions { + exclude 'META-INF/beans.xml' + } + + lintOptions { + checkReleaseBuilds false + warning 'InvalidPackage' + } + + signingConfigs { + debug { + storeFile file('../debug.keystore') + } + } +} + +dependencies { + compile project(':Server Proxy') + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:support-v4:23.4.+' + compile 'com.android.support:appcompat-v7:23.4.+' + compile 'com.android.support:mediarouter-v7:23.4.+' + compile 'com.android.support:recyclerview-v7:23.4.+' + compile 'com.android.support:design:23.4.+' + compile 'com.sothree.slidinguppanel:library:3.0.0' + compile 'de.hdodenhof:circleimageview:1.2.1' + compile group: 'org.fourthline.cling', name: 'cling-core', version:'2.1.1' + compile group: 'org.fourthline.cling', name: 'cling-support', version:'2.1.1' + compile group: 'org.eclipse.jetty', name: 'jetty-server', version:'8.1.16.v20140903' + compile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'8.1.16.v20140903' + compile group: 'org.eclipse.jetty', name: 'jetty-client', version:'8.1.16.v20140903' +} diff --git a/app/libs/kryo-2.21-all.jar b/app/libs/kryo-2.21-all.jar new file mode 100644 index 0000000..83f8b0f Binary files /dev/null and b/app/libs/kryo-2.21-all.jar differ diff --git a/app/proguard.cfg b/app/proguard.cfg new file mode 100644 index 0000000..a18ae91 --- /dev/null +++ b/app/proguard.cfg @@ -0,0 +1,62 @@ +-dontobfuscate +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +# Kryo +-keep,allowshrinking class java.beans.** { *; } +-keep,allowshrinking class sun.reflect.** { *; } +-dontwarn sun.reflect.** +-dontwarn java.beans.** +-keepclassmembers public class com.esotericsoftware.** { *; } + +-keepclasseswithmembernames class * { + native ; +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keep class android.support.v7.app.MediaRouteButton { *; } +-keep class android.support.v7.widget.SearchView { *; } + +-dontwarn android.support.** + +# DLNA/Cling +-keep class org.fourthline.cling.** { *; } +-keep interface org.fourthline.cling.** { *; } +-dontwarn javax.** +-dontwarn org.objectweb.** +-dontwarn org.slf4j.** +-dontwarn org.mortbay.** +-dontwarn org.fourthline.** +-dontwarn org.seamless.** +-dontwarn org.eclipse.** +-dontwarn java.** +-keepattributes *Annotation*, InnerClasses \ No newline at end of file diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java new file mode 100644 index 0000000..e5d0567 --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java @@ -0,0 +1,13 @@ +package github.nvllsvm.audinaut; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java new file mode 100644 index 0000000..0458300 --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java @@ -0,0 +1,34 @@ +package github.nvllsvm.audinaut.activity; + +import github.nvllsvm.audinaut.R; +import android.test.ActivityInstrumentationTestCase2; + +public class SubsonicFragmentActivityTest extends + ActivityInstrumentationTestCase2 { + + private SubsonicFragmentActivity activity; + + public SubsonicFragmentActivityTest() { + super(SubsonicFragmentActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + activity = getActivity(); + } + + /** + * Test the main layout. + */ + public void testLayout() { + assertNotNull(activity.findViewById(R.id.content_frame)); + } + + /** + * Test the bottom bar. + */ + public void testBottomBar() { + assertNotNull(activity.findViewById(R.id.bottom_bar)); + } +} diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java new file mode 100644 index 0000000..df36d9e --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java @@ -0,0 +1,68 @@ +package github.nvllsvm.audinaut.domain; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +public class GenreComparatorTest extends TestCase { + + /** + * Sort genres which doesn't have name + */ + public void testSortGenreWithoutNameComparator() { + Genre g1 = new Genre(); + g1.setName("Genre"); + + Genre g2 = new Genre(); + + List genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + + List sortedGenre = Genre.GenreComparator.sort(genres); + assertEquals(sortedGenre.get(0), g2); + } + + /** + * Sort genre with same name + */ + public void testSortGenreWithSameName() { + Genre g1 = new Genre(); + g1.setName("Genre"); + + Genre g2 = new Genre(); + g2.setName("genre"); + + List genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + + List sortedGenre = Genre.GenreComparator.sort(genres); + assertEquals(sortedGenre.get(0), g1); + } + + /** + * test nominal genre sort + */ + public void testSortGenre() { + Genre g1 = new Genre(); + g1.setName("Rock"); + + Genre g2 = new Genre(); + g2.setName("Pop"); + + Genre g3 = new Genre(); + g2.setName("Rap"); + + List genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + genres.add(g3); + + List sortedGenre = Genre.GenreComparator.sort(genres); + assertEquals(sortedGenre.get(0), g2); + assertEquals(sortedGenre.get(1), g3); + assertEquals(sortedGenre.get(2), g1); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java new file mode 100644 index 0000000..9366f2b --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java @@ -0,0 +1,296 @@ +package github.nvllsvm.audinaut.service; + +import static github.nvllsvm.audinaut.domain.PlayerState.COMPLETED; +import static github.nvllsvm.audinaut.domain.PlayerState.IDLE; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED; +import static github.nvllsvm.audinaut.domain.PlayerState.STARTED; +import static github.nvllsvm.audinaut.domain.PlayerState.STOPPED; +import java.util.List; + +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; + +import java.util.LinkedList; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +public class DownloadServiceTest extends + ActivityInstrumentationTestCase2 { + + private SubsonicFragmentActivity activity; + private DownloadService downloadService; + + public DownloadServiceTest() { + super(SubsonicFragmentActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + activity = getActivity(); + downloadService = activity.getDownloadService(); + downloadService.clear(); + } + + /** + * Test the get player duration without playlist. + */ + public void testGetPlayerDurationWithoutPlayList() { + int duration = downloadService.getPlayerDuration(); + assertEquals(0, duration); + } + + /** + * Test the get player position without playlist. + */ + public void testGetPlayerPositionWithoutPlayList() { + int position = downloadService.getPlayerPosition(); + assertEquals(0, position); + } + + public void testGetCurrentPlayingIndexWithoutPlayList() { + int currentPlayingIndex = activity.getDownloadService() + .getCurrentPlayingIndex(); + assertEquals(currentPlayingIndex, -1); + } + + /** + * Test next action without playlist. + */ + public void testNextWithoutPlayList() { + int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + downloadService.next(); + int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + assertTrue(oldCurrentPlayingIndex == newCurrentPlayingIndex); + } + + /** + * Test previous action without playlist. + */ + public void testPreviousWithoutPlayList() { + int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + downloadService.previous(); + int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + assertTrue(oldCurrentPlayingIndex == newCurrentPlayingIndex); + } + + /** + * Test next action with playlist. + */ + public void testNextWithPlayList() throws InterruptedException { + // Download two songs + downloadService.getDownloads().clear(); + downloadService.download(this.createMusicSongs(2), false, false, false, + false, 0, 0); + + Log.w("testPrevWithPlayList", "Start waiting to downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "Stop waiting downloads"); + + // Get the current index + int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + + // Do the next + downloadService.next(); + + // Check that the new current index is incremented + int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + assertEquals(oldCurrentPlayingIndex + 1, newCurrentPlayingIndex); + } + + /** + * Test previous action with playlist. + */ + public void testPrevWithPlayList() throws InterruptedException { + // Download two songs + downloadService.getDownloads().clear(); + downloadService.download(this.createMusicSongs(2), false, false, false, + false, 0, 0); + + Log.w("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "Stop waiting downloads"); + + // Get the current index + int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + + // Do a next before the previous + downloadService.next(); + + // Do the previous + downloadService.previous(); + + // Check that the new current index is incremented + int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex(); + assertEquals(oldCurrentPlayingIndex, newCurrentPlayingIndex); + } + + /** + * Test seek feature. + */ + public void testSeekTo() { + // seek with negative + downloadService.seekTo(Integer.MIN_VALUE); + + // seek with null + downloadService.seekTo(0); + + // seek with big value + downloadService.seekTo(Integer.MAX_VALUE); + } + + /** + * Test toggle play pause. + */ + public void testTogglePlayPause() { + PlayerState oldPlayState = downloadService.getPlayerState(); + downloadService.togglePlayPause(); + PlayerState newPlayState = downloadService.getPlayerState(); + if (oldPlayState == PAUSED || oldPlayState == COMPLETED + || oldPlayState == STOPPED) { + assertEquals(STARTED, newPlayState); + } else if (oldPlayState == STOPPED || oldPlayState == IDLE) { + if (downloadService.size() == 0) { + assertEquals(IDLE, newPlayState); + } else { + assertEquals(STARTED, newPlayState); + } + } else if (oldPlayState == STARTED) { + assertEquals(PAUSED, newPlayState); + } + downloadService.togglePlayPause(); + newPlayState = downloadService.getPlayerState(); + assertEquals(oldPlayState, newPlayState); + } + + /** + * Test toggle play pause without playlist. + */ + public void testTogglePlayPauseWithoutPlayList() { + PlayerState oldPlayState = downloadService.getPlayerState(); + downloadService.togglePlayPause(); + PlayerState newPlayState = downloadService.getPlayerState(); + + assertEquals(IDLE, oldPlayState); + assertEquals(IDLE, newPlayState); + } + + /** + * Test toggle play pause without playlist. + * + * @throws InterruptedException + */ + public void testTogglePlayPauseWithPlayList() throws InterruptedException { + // Download two songs + downloadService.getDownloads().clear(); + downloadService.download(this.createMusicSongs(2), false, false, false, + false, 0, 0); + + Log.w("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "Stop waiting downloads"); + + PlayerState oldPlayState = downloadService.getPlayerState(); + downloadService.togglePlayPause(); + Thread.sleep(500); + assertEquals(STARTED, downloadService.getPlayerState()); + downloadService.togglePlayPause(); + PlayerState newPlayState = downloadService.getPlayerState(); + assertEquals(PAUSED, newPlayState); + } + + /** + * Test the autoplay. + * + * @throws InterruptedException + */ + public void testAutoplay() throws InterruptedException { + // Download one songs + downloadService.getDownloads().clear(); + downloadService.download(this.createMusicSongs(1), false, true, false, + false, 0, 0); + + Log.w("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "Stop waiting downloads"); + + PlayerState playerState = downloadService.getPlayerState(); + assertEquals(STARTED, playerState); + } + + /** + * Test if the download list is empty. + */ + public void testGetDownloadsEmptyList() { + List list = downloadService.getDownloads(); + assertEquals(0, list.size()); + } + + /** + * Test if the download service add the given song to its queue. + */ + public void testAddMusicToDownload() { + assertNotNull(downloadService); + + // Download list before + List downloadList = downloadService.getDownloads(); + int beforeDownloadAction = 0; + if (downloadList != null) { + beforeDownloadAction = downloadList.size(); + } + + // Launch download + downloadService.download(this.createMusicSongs(1), false, false, false, + false, 0, 0); + + // Check number of download after + int afterDownloadAction = 0; + downloadList = downloadService.getDownloads(); + if (downloadList != null && !downloadList.isEmpty()) { + afterDownloadAction = downloadList.size(); + } + assertEquals(beforeDownloadAction + 1, afterDownloadAction); + } + + /** + * Generate a list containing some music directory entries. + * + * @return list containing some music directory entries. + */ + private List createMusicSongs(int size) { + MusicDirectory.Entry musicEntry = new MusicDirectory.Entry(); + musicEntry.setAlbum("Itchy Hitchhiker"); + musicEntry.setBitRate(198); + musicEntry.setAlbumId("49"); + musicEntry.setDuration(247); + musicEntry.setSize(Long.valueOf(6162717)); + musicEntry.setArtistId("23"); + musicEntry.setArtist("The Dada Weatherman"); + musicEntry.setCloseness(0); + musicEntry.setContentType("audio/mpeg"); + musicEntry.setCoverArt("433"); + musicEntry.setDirectory(false); + musicEntry.setGenre("Easy Listening/New Age"); + musicEntry.setGrandParent("306"); + musicEntry.setId("466"); + musicEntry.setParent("433"); + musicEntry + .setPath("The Dada Weatherman/Itchy Hitchhiker/08 - The Dada Weatherman - Harmonies.mp3"); + musicEntry.setStarred(true); + musicEntry.setSuffix("mp3"); + musicEntry.setTitle("Harmonies"); + musicEntry.setType(0); + musicEntry.setVideo(false); + + List musicEntries = new LinkedList(); + + for (int i = 0; i < size; i++) { + musicEntries.add(musicEntry); + } + + return musicEntries; + + } + +} diff --git a/app/src/audinaut-stacktrace.txt b/app/src/audinaut-stacktrace.txt new file mode 100644 index 0000000..57a6a54 --- /dev/null +++ b/app/src/audinaut-stacktrace.txt @@ -0,0 +1,26 @@ +Android API level: 23 +Subsonic version name: 5.3 +Subsonic version code: 186 + +android.content.res.Resources$NotFoundException: Resource ID #0xff33b5e5 + at android.content.res.Resources.getValue(Resources.java:1432) + at android.content.res.Resources.getValue(Resources.java:1412) + at android.content.res.Resources.getColor(Resources.java:1028) + at android.content.res.Resources.getColor(Resources.java:1001) + at android.support.v4.widget.SwipeRefreshLayout.setColorSchemeResources(SwipeRefreshLayout.java:529) + at github.nvllsvm.audinaut.fragments.SubsonicFragment.setupScrollList(SubsonicFragment.java:691) + at github.nvllsvm.audinaut.fragments.SelectRecyclerFragment.onCreateView(SelectRecyclerFragment.java:89) + at github.nvllsvm.audinaut.fragments.SelectArtistFragment.onCreateView(SelectArtistFragment.java:77) + at android.support.v4.app.Fragment.performCreateView(Fragment.java:1974) + at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1067) + at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1252) + at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:742) + at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1617) + at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:517) + at android.os.Handler.handleCallback(Handler.java:739) + at android.os.Handler.dispatchMessage(Handler.java:95) + at android.os.Looper.loop(Looper.java:148) + at android.app.ActivityThread.main(ActivityThread.java:5461) + at java.lang.reflect.Method.invoke(Native Method) + at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) diff --git a/app/src/main/.gradle/3.1/taskArtifacts/cache.properties b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties new file mode 100644 index 0000000..9e3a6ad --- /dev/null +++ b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties @@ -0,0 +1 @@ +#Sat Oct 01 15:10:08 EDT 2016 diff --git a/app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock new file mode 100644 index 0000000..405bc39 Binary files /dev/null and b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock differ diff --git a/app/src/main/.gradle/3.1/taskArtifacts/fileSnapshots.bin b/app/src/main/.gradle/3.1/taskArtifacts/fileSnapshots.bin new file mode 100644 index 0000000..bb89b57 Binary files /dev/null and b/app/src/main/.gradle/3.1/taskArtifacts/fileSnapshots.bin differ diff --git a/app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin b/app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin new file mode 100644 index 0000000..60fe4e4 Binary files /dev/null and b/app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3b66ddb --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java new file mode 100644 index 0000000..47b6ffb --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java @@ -0,0 +1,245 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.activity; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.widget.DrawerLayout; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.Spinner; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.Util; + +public class EditPlayActionActivity extends SubsonicActivity { + private CheckBox shuffleCheckbox; + private CheckBox startYearCheckbox; + private EditText startYearBox; + private CheckBox endYearCheckbox; + private EditText endYearBox; + private Button genreButton; + private Spinner offlineSpinner; + + private String doNothing; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.tasker_start_playing_title); + setContentView(R.layout.edit_play_action); + final Activity context = this; + doNothing = context.getResources().getString(R.string.tasker_edit_do_nothing); + + shuffleCheckbox = (CheckBox) findViewById(R.id.edit_shuffle_checkbox); + shuffleCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton view, boolean isChecked) { + startYearCheckbox.setEnabled(isChecked); + endYearCheckbox.setEnabled(isChecked); + genreButton.setEnabled(isChecked); + } + }); + + startYearCheckbox = (CheckBox) findViewById(R.id.edit_start_year_checkbox); + startYearBox = (EditText) findViewById(R.id.edit_start_year); + // Disable/enable number box if checked + startYearCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton view, boolean isChecked) { + startYearBox.setEnabled(isChecked); + } + }); + + endYearCheckbox = (CheckBox) findViewById(R.id.edit_end_year_checkbox); + endYearBox = (EditText) findViewById(R.id.edit_end_year); + endYearCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton view, boolean isChecked) { + endYearBox.setEnabled(isChecked); + } + }); + + genreButton = (Button) findViewById(R.id.edit_genre_spinner); + genreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + new LoadingTask>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getGenres(false, context, this); + } + + @Override + protected void done(final List genres) { + List names = new ArrayList(); + String blank = context.getResources().getString(R.string.select_genre_blank); + names.add(doNothing); + names.add(blank); + for(Genre genre: genres) { + names.add(genre.getName()); + } + final List finalNames = names; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.shuffle_pick_genre) + .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if(which == 1) { + genreButton.setText(""); + } else { + genreButton.setText(finalNames.get(which)); + } + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + }); + genreButton.setText(doNothing); + + offlineSpinner = (Spinner) findViewById(R.id.edit_offline_spinner); + ArrayAdapter offlineAdapter = ArrayAdapter.createFromResource(this, R.array.editServerOptions, android.R.layout.simple_spinner_item); + offlineAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + offlineSpinner.setAdapter(offlineAdapter); + + // Setup default for everything + Bundle extras = getIntent().getBundleExtra(Constants.TASKER_EXTRA_BUNDLE); + if(extras != null) { + if(extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE)) { + shuffleCheckbox.setChecked(true); + } + + String startYear = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, null); + if(startYear != null) { + startYearCheckbox.setEnabled(true); + startYearBox.setText(startYear); + } + String endYear = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, null); + if(endYear != null) { + endYearCheckbox.setEnabled(true); + endYearBox.setText(endYear); + } + + String genre = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, doNothing); + if(genre != null) { + genreButton.setText(genre); + } + + int offline = extras.getInt(Constants.PREFERENCES_KEY_OFFLINE, 0); + if(offline != 0) { + offlineSpinner.setSelection(offline); + } + } + + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.tasker_configuration, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(item.getItemId() == android.R.id.home) { + cancel(); + return true; + } else if(item.getItemId() == R.id.menu_accept) { + accept(); + return true; + } else if(item.getItemId() == R.id.menu_cancel) { + cancel(); + return true; + } + + return false; + } + + private void accept() { + Intent intent = new Intent(); + + String blurb = getResources().getString(shuffleCheckbox.isChecked() ? R.string.tasker_start_playing_shuffled : R.string.tasker_start_playing); + intent.putExtra("com.twofortyfouram.locale.intent.extra.BLURB", blurb); + + // Get settings user specified + Bundle data = new Bundle(); + boolean shuffle = shuffleCheckbox.isChecked(); + data.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, shuffle); + if(shuffle) { + if(startYearCheckbox.isChecked()) { + data.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYearBox.getText().toString()); + } + if(endYearCheckbox.isChecked()) { + data.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYearBox.getText().toString()); + } + String genre = genreButton.getText().toString(); + if(!genre.equals(doNothing)) { + data.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); + } + } + + int offline = offlineSpinner.getSelectedItemPosition(); + if(offline != 0) { + data.putInt(Constants.PREFERENCES_KEY_OFFLINE, offline); + } + + intent.putExtra(Constants.TASKER_EXTRA_BUNDLE, data); + + setResult(Activity.RESULT_OK, intent); + finish(); + } + private void cancel() { + setResult(Activity.RESULT_CANCELED); + finish(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/QueryReceiverActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/QueryReceiverActivity.java new file mode 100644 index 0000000..52a8f19 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/QueryReceiverActivity.java @@ -0,0 +1,85 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.activity; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.SearchRecentSuggestions; +import android.util.Log; + +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.provider.AudinautSearchProvider; + +/** + * Receives search queries and forwards to the SearchFragment. + * + * @author Sindre Mehus + */ +public class QueryReceiverActivity extends Activity { + + private static final String TAG = QueryReceiverActivity.class.getSimpleName(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + doSearch(); + } else if(Intent.ACTION_VIEW.equals(intent.getAction())) { + showResult(intent.getDataString(), intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + } + finish(); + Util.disablePendingTransition(this); + } + + private void doSearch() { + String query = getIntent().getStringExtra(SearchManager.QUERY); + if (query != null) { + Intent intent = new Intent(QueryReceiverActivity.this, SubsonicFragmentActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent); + } + } + private void showResult(String albumId, String name) { + if (albumId != null) { + Intent intent = new Intent(this, SubsonicFragmentActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true); + if(albumId.indexOf("ar-") == 0) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true); + albumId = albumId.replace("ar-", ""); + } else if(albumId.indexOf("so-") == 0) { + intent.putExtra(Constants.INTENT_EXTRA_SEARCH_SONG, name); + albumId = albumId.replace("so-", ""); + } + intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, albumId); + if (name != null) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, name); + } + Util.startActivityWithoutTransition(this, intent); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.java new file mode 100644 index 0000000..06a314b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.java @@ -0,0 +1,58 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.activity; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.Toolbar; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.fragments.PreferenceCompatFragment; +import github.nvllsvm.audinaut.fragments.SettingsFragment; +import github.nvllsvm.audinaut.util.Constants; + +public class SettingsActivity extends SubsonicActivity { + private static final String TAG = SettingsActivity.class.getSimpleName(); + private PreferenceCompatFragment fragment; + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lastSelectedPosition = R.id.drawer_settings; + setContentView(R.layout.settings_activity); + + if (savedInstanceState == null) { + fragment = new SettingsFragment(); + Bundle args = new Bundle(); + args.putInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, R.xml.settings); + + fragment.setArguments(args); + fragment.setRetainInstance(true); + + currentFragment = fragment; + currentFragment.setPrimaryFragment(true); + getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); + } + + Toolbar mainToolbar = (Toolbar) findViewById(R.id.main_toolbar); + setSupportActionBar(mainToolbar); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java new file mode 100644 index 0000000..3789292 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java @@ -0,0 +1,1055 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.activity; + +import android.app.UiModeManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ThemeUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.util.UserUtil; + +import static android.Manifest.*; + +public class SubsonicActivity extends AppCompatActivity implements OnItemSelectedListener { + private static final String TAG = SubsonicActivity.class.getSimpleName(); + private static ImageLoader IMAGE_LOADER; + protected static String theme; + protected static boolean fullScreen; + protected static boolean actionbarColored; + private static final int MENU_GROUP_SERVER = 10; + private static final int MENU_ITEM_SERVER_BASE = 100; + private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; + + private final List afterServiceAvailable = new ArrayList<>(); + private boolean drawerIdle = true; + private boolean destroyed = false; + private boolean finished = false; + protected List backStack = new ArrayList(); + protected SubsonicFragment currentFragment; + protected View primaryContainer; + protected View secondaryContainer; + protected boolean tv = false; + protected boolean touchscreen = true; + protected Handler handler = new Handler(); + Spinner actionBarSpinner; + ArrayAdapter spinnerAdapter; + ViewGroup rootView; + DrawerLayout drawer; + ActionBarDrawerToggle drawerToggle; + NavigationView drawerList; + View drawerHeader; + ImageView drawerHeaderToggle; + TextView drawerServerName; + TextView drawerUserName; + int lastSelectedPosition = 0; + boolean showingTabs = true; + boolean drawerOpen = false; + SharedPreferences.OnSharedPreferenceChangeListener preferencesListener; + + static { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); + } + + @Override + protected void onCreate(Bundle bundle) { + UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + // tv = true; + } + PackageManager pm = getPackageManager(); + if(!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { + touchscreen = false; + } + + setUncaughtExceptionHandler(); + applyTheme(); + applyFullscreen(); + super.onCreate(bundle); + startService(new Intent(this, DownloadService.class)); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + if(getIntent().hasExtra(Constants.FRAGMENT_POSITION)) { + lastSelectedPosition = getIntent().getIntExtra(Constants.FRAGMENT_POSITION, 0); + } + + if(preferencesListener == null) { + Util.getPreferences(this).registerOnSharedPreferenceChangeListener(preferencesListener); + } + + if (ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{ permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + switch (requestCode) { + case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + } else { + Util.toast(this, R.string.permission_external_storage_failed); + finish(); + } + } + } + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + if(spinnerAdapter == null) { + createCustomActionBarView(); + } + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + if(Util.shouldStartOnHeadphones(this)) { + Intent serviceIntent = new Intent(); + serviceIntent.setClassName(this.getPackageName(), HeadphoneListenerService.class.getName()); + this.startService(serviceIntent); + } + } + + protected void createCustomActionBarView() { + actionBarSpinner = (Spinner) getLayoutInflater().inflate(R.layout.actionbar_spinner, null); + if((this instanceof SubsonicFragmentActivity || this instanceof SettingsActivity) && (Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true) || ThemeUtil.getThemeRes(this) != R.style.Theme_Audinaut_Light_No_Color)) { + actionBarSpinner.setBackgroundDrawable(DrawableTint.getTintedDrawableFromColor(this, R.drawable.abc_spinner_mtrl_am_alpha, android.R.color.white)); + } + spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + actionBarSpinner.setOnItemSelectedListener(this); + actionBarSpinner.setAdapter(spinnerAdapter); + + getSupportActionBar().setCustomView(actionBarSpinner); + } + + @Override + protected void onResume() { + super.onResume(); + Util.registerMediaButtonEventReceiver(this); + + // Make sure to update theme + SharedPreferences prefs = Util.getPreferences(this); + if (theme != null && !theme.equals(ThemeUtil.getTheme(this)) || fullScreen != prefs.getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false) || actionbarColored != prefs.getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + restart(); + overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + DrawableTint.wipeTintCache(); + } + + populateTabs(); + getImageLoader().onUIVisible(); + UpdateView.addActiveActivity(); + } + + @Override + protected void onPause() { + super.onPause(); + + UpdateView.removeActiveActivity(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + destroyed = true; + Util.getPreferences(this).unregisterOnSharedPreferenceChangeListener(preferencesListener); + } + + @Override + public void finish() { + super.finish(); + Util.disablePendingTransition(this); + } + + @Override + public void setContentView(int viewId) { + if(isTv()) { + super.setContentView(R.layout.static_drawer_activity); + } else { + super.setContentView(R.layout.abstract_activity); + } + rootView = (ViewGroup) findViewById(R.id.content_frame); + + if(viewId != 0) { + LayoutInflater layoutInflater = getLayoutInflater(); + layoutInflater.inflate(viewId, rootView); + } + + drawerList = (NavigationView) findViewById(R.id.left_drawer); + drawerList.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(final MenuItem menuItem) { + if(showingTabs) { + // Settings are on a different selectable track + if (menuItem.getItemId() != R.id.drawer_settings && menuItem.getItemId() != R.id.drawer_offline) { + menuItem.setChecked(true); + lastSelectedPosition = menuItem.getItemId(); + } + + switch (menuItem.getItemId()) { + case R.id.drawer_library: + drawerItemSelected("Artist"); + return true; + case R.id.drawer_playlists: + drawerItemSelected("Playlist"); + return true; + case R.id.drawer_downloading: + drawerItemSelected("Download"); + return true; + case R.id.drawer_offline: + toggleOffline(); + return true; + case R.id.drawer_settings: + startActivity(new Intent(SubsonicActivity.this, SettingsActivity.class)); + drawer.closeDrawers(); + return true; + } + } else { + int activeServer = menuItem.getItemId() - MENU_ITEM_SERVER_BASE; + SubsonicActivity.this.setActiveServer(activeServer); + populateTabs(); + return true; + } + + return false; + } + }); + + drawerHeader = drawerList.inflateHeaderView(R.layout.drawer_header); + drawerHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(showingTabs) { + populateServers(); + } else { + populateTabs(); + } + } + }); + + drawerHeaderToggle = (ImageView) drawerHeader.findViewById(R.id.header_select_image); + drawerServerName = (TextView) drawerHeader.findViewById(R.id.header_server_name); + drawerUserName = (TextView) drawerHeader.findViewById(R.id.header_user_name); + + updateDrawerHeader(); + + if(!isTv()) { + drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + + // Pass in toolbar if it exists + Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar); + drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.common_appname, R.string.common_appname) { + @Override + public void onDrawerClosed(View view) { + drawerIdle = true; + drawerOpen = false; + + if(!showingTabs) { + populateTabs(); + } + } + + @Override + public void onDrawerOpened(View view) { + DownloadService downloadService = getDownloadService(); + boolean downloadingVisible = downloadService != null && !downloadService.getBackgroundDownloads().isEmpty(); + if(lastSelectedPosition == R.id.drawer_downloading) { + downloadingVisible = true; + } + setDrawerItemVisible(R.id.drawer_downloading, downloadingVisible); + + drawerIdle = true; + drawerOpen = true; + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, slideOffset); + drawerIdle = false; + } + }; + drawer.setDrawerListener(drawerToggle); + drawerToggle.setDrawerIndicatorEnabled(true); + + drawer.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (drawerIdle && currentFragment != null && currentFragment.getGestureDetector() != null) { + return currentFragment.getGestureDetector().onTouchEvent(event); + } else { + return false; + } + } + }); + } + + // Check whether this is a tablet or not + secondaryContainer = findViewById(R.id.fragment_second_container); + if(secondaryContainer != null) { + primaryContainer = findViewById(R.id.fragment_container); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + String[] ids = new String[backStack.size() + 1]; + ids[0] = currentFragment.getTag(); + int i = 1; + for(SubsonicFragment frag: backStack) { + ids[i] = frag.getTag(); + i++; + } + savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK, ids); + savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE, backStack.size() + 1); + savedInstanceState.putInt(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + int size = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE); + String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK); + FragmentManager fm = getSupportFragmentManager(); + currentFragment = (SubsonicFragment)fm.findFragmentByTag(ids[0]); + currentFragment.setPrimaryFragment(true); + currentFragment.setSupportTag(ids[0]); + supportInvalidateOptionsMenu(); + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + for(int i = 1; i < size; i++) { + SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[i]); + frag.setSupportTag(ids[i]); + if(secondaryContainer != null) { + frag.setPrimaryFragment(false, true); + } + trans.hide(frag); + backStack.add(frag); + } + trans.commit(); + + // Current fragment is hidden in secondaryContainer + if(secondaryContainer == null && !currentFragment.isVisible()) { + trans = getSupportFragmentManager().beginTransaction(); + trans.remove(currentFragment); + trans.commit(); + getSupportFragmentManager().executePendingTransactions(); + + trans = getSupportFragmentManager().beginTransaction(); + trans.add(R.id.fragment_container, currentFragment, ids[0]); + trans.commit(); + } + // Current fragment needs to be moved over to secondaryContainer + else if(secondaryContainer != null && secondaryContainer.findViewById(currentFragment.getRootId()) == null && backStack.size() > 0) { + trans = getSupportFragmentManager().beginTransaction(); + trans.remove(currentFragment); + trans.show(backStack.get(backStack.size() - 1)); + trans.commit(); + getSupportFragmentManager().executePendingTransactions(); + + trans = getSupportFragmentManager().beginTransaction(); + trans.add(R.id.fragment_second_container, currentFragment, ids[0]); + trans.commit(); + + secondaryContainer.setVisibility(View.VISIBLE); + } + + lastSelectedPosition = savedInstanceState.getInt(Constants.FRAGMENT_POSITION); + if(lastSelectedPosition != 0) { + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } + recreateSpinner(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + SubsonicFragment currentFragment = getCurrentFragment(); + if(currentFragment != null) { + try { + SubsonicFragment fragment = getCurrentFragment(); + fragment.setContext(this); + fragment.onCreateOptionsMenu(menu, menuInflater); + + if(isTouchscreen()) { + menu.setGroupVisible(R.id.not_touchscreen, false); + } + } catch(Exception e) { + Log.w(TAG, "Error on creating options menu", e); + } + } + return true; + } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) { + return true; + } else if(item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + + return getCurrentFragment().onOptionsItemSelected(item); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN; + boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP; + boolean isVolumeAdjust = isVolumeDown || isVolumeUp; + + return super.onKeyDown(keyCode, event); + } + + @Override + public void setTitle(CharSequence title) { + if(title != null && getSupportActionBar() != null && !title.equals(getSupportActionBar().getTitle())) { + getSupportActionBar().setTitle(title); + recreateSpinner(); + } + } + public void setSubtitle(CharSequence title) { + getSupportActionBar().setSubtitle(title); + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int top = spinnerAdapter.getCount() - 1; + if(position < top) { + for(int i = top; i > position && i >= 0; i--) { + removeCurrent(); + } + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + + private void populateTabs() { + drawerList.getMenu().clear(); + drawerList.inflateMenu(R.menu.drawer_navigation); + + SharedPreferences prefs = Util.getPreferences(this); + boolean sharedEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SHARED_ENABLED, true) && !Util.isOffline(this); + + MenuItem offlineMenuItem = drawerList.getMenu().findItem(R.id.drawer_offline); + if(Util.isOffline(this)) { + setDrawerItemVisible(R.id.drawer_library, false); + + if(lastSelectedPosition == 0 || lastSelectedPosition == R.id.drawer_library) { + String newFragment = Util.openToTab(this); + if(newFragment == null || "Library".equals(newFragment)) { + newFragment = "Artist"; + } + + lastSelectedPosition = getDrawerItemId(newFragment); + drawerItemSelected(newFragment); + } + + offlineMenuItem.setTitle(R.string.main_online); + } else { + offlineMenuItem.setTitle(R.string.main_offline); + } + + if(lastSelectedPosition != 0) { + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } + drawerHeaderToggle.setImageResource(R.drawable.main_select_server_dark); + + showingTabs = true; + } + private void populateServers() { + drawerList.getMenu().clear(); + + int serverCount = Util.getServerCount(this); + int activeServer = Util.getActiveServer(this); + for(int i = 1; i <= serverCount; i++) { + MenuItem item = drawerList.getMenu().add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_BASE + i, MENU_ITEM_SERVER_BASE + i, Util.getServerName(this, i)); + if(activeServer == i) { + item.setChecked(true); + } + } + drawerList.getMenu().setGroupCheckable(MENU_GROUP_SERVER, true, true); + drawerHeaderToggle.setImageResource(R.drawable.main_select_tabs_dark); + + showingTabs = false; + } + private void setDrawerItemVisible(int id, boolean visible) { + MenuItem item = drawerList.getMenu().findItem(id); + if(item != null) { + item.setVisible(visible); + } + } + + protected void drawerItemSelected(String fragmentType) { + if(currentFragment != null) { + currentFragment.stopActionMode(); + } + startFragmentActivity(fragmentType); + } + + public void startFragmentActivity(String fragmentType) { + Intent intent = new Intent(); + intent.setClass(SubsonicActivity.this, SubsonicFragmentActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + if(!"".equals(fragmentType)) { + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + } + if(lastSelectedPosition != 0) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } + startActivity(intent); + finish(); + } + + protected void exit() { + if(((Object) this).getClass() != SubsonicFragmentActivity.class) { + Intent intent = new Intent(this, SubsonicFragmentActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true); + Util.startActivityWithoutTransition(this, intent); + } else { + finished = true; + this.stopService(new Intent(this, DownloadService.class)); + this.finish(); + } + } + + public boolean onBackPressedSupport() { + if(drawerOpen) { + drawer.closeDrawers(); + return false; + } else if(backStack.size() > 0) { + removeCurrent(); + return false; + } else { + return true; + } + } + + @Override + public void onBackPressed() { + if(onBackPressedSupport()) { + super.onBackPressed(); + } + } + + public SubsonicFragment getCurrentFragment() { + return this.currentFragment; + } + + public void replaceFragment(SubsonicFragment fragment, int tag) { + replaceFragment(fragment, tag, false); + } + public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { + SubsonicFragment oldFragment = currentFragment; + if(currentFragment != null) { + currentFragment.setPrimaryFragment(false, secondaryContainer != null); + } + backStack.add(currentFragment); + + currentFragment = fragment; + currentFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + + if(secondaryContainer == null || oldFragment.isAlwaysFullscreen() || currentFragment.isAlwaysStartFullscreen()) { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + trans.hide(oldFragment); + trans.add(R.id.fragment_container, fragment, tag + ""); + trans.commit(); + } else { + // Make sure secondary container is visible now + secondaryContainer.setVisibility(View.VISIBLE); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + + // Check to see if you need to put on top of old left or not + if(backStack.size() > 1) { + // Move old right to left if there is a backstack already + SubsonicFragment newLeftFragment = backStack.get(backStack.size() - 1); + if(replaceCurrent) { + // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + } + trans.remove(newLeftFragment); + + // Only move right to left if replaceCurrent is false + if(!replaceCurrent) { + SubsonicFragment oldLeftFragment = backStack.get(backStack.size() - 2); + oldLeftFragment.setSecondaryFragment(false); + // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + trans.hide(oldLeftFragment); + + // Make sure remove is finished before adding + trans.commit(); + getSupportFragmentManager().executePendingTransactions(); + + trans = getSupportFragmentManager().beginTransaction(); + // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + trans.add(R.id.fragment_container, newLeftFragment, newLeftFragment.getSupportTag() + ""); + } else { + backStack.remove(backStack.size() - 1); + } + } + + // Add fragment to the right container + trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + trans.add(R.id.fragment_second_container, fragment, tag + ""); + + // Commit it all + trans.commit(); + + oldFragment.setIsOnlyVisible(false); + currentFragment.setIsOnlyVisible(false); + } + recreateSpinner(); + } + public void removeCurrent() { + // Don't try to remove current if there is no backstack to remove from + if(backStack.isEmpty()) { + return; + } + + if(currentFragment != null) { + currentFragment.setPrimaryFragment(false); + } + SubsonicFragment oldFragment = currentFragment; + + currentFragment = backStack.remove(backStack.size() - 1); + currentFragment.setPrimaryFragment(true, false); + supportInvalidateOptionsMenu(); + + if(secondaryContainer == null || currentFragment.isAlwaysFullscreen() || oldFragment.isAlwaysStartFullscreen()) { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left); + trans.remove(oldFragment); + trans.show(currentFragment); + trans.commit(); + } else { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + + // Remove old right fragment + trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left); + trans.remove(oldFragment); + + // Only switch places if there is a backstack, otherwise primary container is correct + if(backStack.size() > 0 && !backStack.get(backStack.size() - 1).isAlwaysFullscreen() && !currentFragment.isAlwaysStartFullscreen()) { + trans.setCustomAnimations(0, 0, 0, 0); + // Add current left fragment to right side + trans.remove(currentFragment); + + // Make sure remove is finished before adding + trans.commit(); + getSupportFragmentManager().executePendingTransactions(); + + trans = getSupportFragmentManager().beginTransaction(); + // trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left); + trans.add(R.id.fragment_second_container, currentFragment, currentFragment.getSupportTag() + ""); + + SubsonicFragment newLeftFragment = backStack.get(backStack.size() - 1); + newLeftFragment.setSecondaryFragment(true); + trans.show(newLeftFragment); + } else { + secondaryContainer.startAnimation(AnimationUtils.loadAnimation(this, R.anim.exit_to_right)); + secondaryContainer.setVisibility(View.GONE); + + currentFragment.setIsOnlyVisible(true); + } + + trans.commit(); + } + recreateSpinner(); + } + public void replaceExistingFragment(SubsonicFragment fragment, int tag) { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.remove(currentFragment); + trans.add(R.id.fragment_container, fragment, tag + ""); + trans.commit(); + + currentFragment = fragment; + currentFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + } + + public void invalidate() { + if(currentFragment != null) { + while(backStack.size() > 0) { + removeCurrent(); + } + + currentFragment.invalidate(); + populateTabs(); + } + + supportInvalidateOptionsMenu(); + } + + protected void recreateSpinner() { + if(currentFragment == null || currentFragment.getTitle() == null) { + return; + } + if(spinnerAdapter == null || getSupportActionBar().getCustomView() == null) { + createCustomActionBarView(); + } + + if(backStack.size() > 0) { + createCustomActionBarView(); + spinnerAdapter.clear(); + for(int i = 0; i < backStack.size(); i++) { + CharSequence title = backStack.get(i).getTitle(); + if(title != null) { + spinnerAdapter.add(title); + } else { + spinnerAdapter.add("null"); + } + } + if(currentFragment.getTitle() != null) { + spinnerAdapter.add(currentFragment.getTitle()); + } else { + spinnerAdapter.add("null"); + } + spinnerAdapter.notifyDataSetChanged(); + actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1); + if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(false); + getSupportActionBar().setDisplayShowCustomEnabled(true); + } + + if(drawerToggle.isDrawerIndicatorEnabled()) { + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + drawerToggle.setDrawerIndicatorEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } else if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(true); + getSupportActionBar().setTitle(currentFragment.getTitle()); + getSupportActionBar().setDisplayShowCustomEnabled(false); + drawerToggle.setDrawerIndicatorEnabled(true); + } + } + + protected void restart() { + restart(true); + } + protected void restart(boolean resumePosition) { + Intent intent = new Intent(this, this.getClass()); + intent.putExtras(getIntent()); + if(resumePosition) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } else { + String fragmentType = Util.openToTab(this); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + intent.putExtra(Constants.FRAGMENT_POSITION, getDrawerItemId(fragmentType)); + } + finish(); + Util.startActivityWithoutTransition(this, intent); + } + + private void applyTheme() { + theme = ThemeUtil.getTheme(this); + + if(theme != null && theme.indexOf("fullscreen") != -1) { + theme = theme.substring(0, theme.indexOf("_fullscreen")); + ThemeUtil.setTheme(this, theme); + } + + ThemeUtil.applyTheme(this, theme); + actionbarColored = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true); + } + private void applyFullscreen() { + fullScreen = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false); + if(fullScreen || isTv()) { + // Hide additional elements on higher Android versions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + getWindow().getDecorView().setSystemUiVisibility(flags); + } else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + getWindow().requestFeature(Window.FEATURE_NO_TITLE); + } + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + } + + public boolean isDestroyedCompat() { + return destroyed; + } + + public synchronized ImageLoader getImageLoader() { + if (IMAGE_LOADER == null) { + IMAGE_LOADER = new ImageLoader(this); + } + return IMAGE_LOADER; + } + public synchronized static ImageLoader getStaticImageLoader(Context context) { + if (IMAGE_LOADER == null) { + IMAGE_LOADER = new ImageLoader(context); + } + return IMAGE_LOADER; + } + + public DownloadService getDownloadService() { + if(finished) { + return null; + } + + // If service is not available, request it to start and wait for it. + for (int i = 0; i < 5; i++) { + DownloadService downloadService = DownloadService.getInstance(); + if (downloadService != null) { + break; + } + Log.w(TAG, "DownloadService not running. Attempting to start it."); + startService(new Intent(this, DownloadService.class)); + Util.sleepQuietly(50L); + } + + final DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && afterServiceAvailable.size() > 0) { + for(Runnable runnable: afterServiceAvailable) { + handler.post(runnable); + } + afterServiceAvailable.clear(); + } + return downloadService; + } + public void runWhenServiceAvailable(Runnable runnable) { + if(getDownloadService() != null) { + runnable.run(); + } else { + afterServiceAvailable.add(runnable); + checkIfServiceAvailable(); + } + } + private void checkIfServiceAvailable() { + if(getDownloadService() == null) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + checkIfServiceAvailable(); + } + }, 50); + } else if(afterServiceAvailable.size() > 0) { + for(Runnable runnable: afterServiceAvailable) { + handler.post(runnable); + } + afterServiceAvailable.clear(); + } + } + + public static String getThemeName() { + return theme; + } + + public boolean isTv() { + return tv; + } + public boolean isTouchscreen() { + return touchscreen; + } + + public void openNowPlaying() { + + } + public void closeNowPlaying() { + + } + + public void setActiveServer(int instance) { + if (Util.getActiveServer(this) != instance) { + final DownloadService service = getDownloadService(); + if (service != null) { + new SilentBackgroundTask(this) { + @Override + protected Void doInBackground() throws Throwable { + service.clearIncomplete(); + return null; + } + }.execute(); + + } + Util.setActiveServer(this, instance); + invalidate(); + UserUtil.refreshCurrentUser(this, false, true); + updateDrawerHeader(); + } + } + public void updateDrawerHeader() { + if(Util.isOffline(this)) { + drawerServerName.setText(R.string.select_album_offline); + drawerUserName.setText(""); + drawerHeader.setClickable(false); + drawerHeaderToggle.setVisibility(View.GONE); + } else { + drawerServerName.setText(Util.getServerName(this)); + drawerUserName.setText(UserUtil.getCurrentUsername(this)); + drawerHeader.setClickable(true); + drawerHeaderToggle.setVisibility(View.VISIBLE); + } + } + + public void toggleOffline() { + boolean isOffline = Util.isOffline(this); + Util.setOffline(this, !isOffline); + invalidate(); + DownloadService service = getDownloadService(); + if (service != null) { + service.setOnline(isOffline); + } + + UserUtil.seedCurrentUser(this); + this.updateDrawerHeader(); + drawer.closeDrawers(); + } + + public int getDrawerItemId(String fragmentType) { + if(fragmentType == null) { + return R.id.drawer_library; + } + + switch(fragmentType) { + case "Artist": + return R.id.drawer_library; + case "Playlist": + return R.id.drawer_playlists; + default: + return R.id.drawer_library; + } + } + + private void setUncaughtExceptionHandler() { + Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); + if (!(handler instanceof SubsonicActivity.SubsonicUncaughtExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new SubsonicActivity.SubsonicUncaughtExceptionHandler(this)); + } + } + + /** + * Logs the stack trace of uncaught exceptions to a file on the SD card. + */ + private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + + private final Thread.UncaughtExceptionHandler defaultHandler; + private final Context context; + + private SubsonicUncaughtExceptionHandler(Context context) { + this.context = context; + defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + File file = null; + PrintWriter printWriter = null; + try { + + PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.nvllsvm.audinaut", 0); + file = new File(Environment.getExternalStorageDirectory(), "audinaut-stacktrace.txt"); + printWriter = new PrintWriter(file); + printWriter.println("Android API level: " + Build.VERSION.SDK); + printWriter.println("Subsonic version name: " + packageInfo.versionName); + printWriter.println("Subsonic version code: " + packageInfo.versionCode); + printWriter.println(); + throwable.printStackTrace(printWriter); + Log.i(TAG, "Stack trace written to " + file); + } catch (Throwable x) { + Log.e(TAG, "Failed to write stack trace to " + file, x); + } finally { + Util.close(printWriter); + if (defaultHandler != null) { + defaultHandler.uncaughtException(thread, throwable); + } + + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java new file mode 100644 index 0000000..7dca8a4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java @@ -0,0 +1,929 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.activity; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import java.io.File; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.fragments.DownloadFragment; +import github.nvllsvm.audinaut.fragments.NowPlayingFragment; +import github.nvllsvm.audinaut.fragments.SearchFragment; +import github.nvllsvm.audinaut.fragments.SelectArtistFragment; +import github.nvllsvm.audinaut.fragments.SelectDirectoryFragment; +import github.nvllsvm.audinaut.fragments.SelectPlaylistFragment; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.updates.Updater; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; + +/** + * Created by Scott on 10/14/13. + */ +public class SubsonicFragmentActivity extends SubsonicActivity implements DownloadService.OnSongChangedListener { + private static String TAG = SubsonicFragmentActivity.class.getSimpleName(); + private static boolean infoDialogDisplayed; + private static boolean sessionInitialized = false; + private static long ALLOWED_SKEW = 30000L; + + private SlidingUpPanelLayout slideUpPanel; + private SlidingUpPanelLayout.PanelSlideListener panelSlideListener; + private boolean isPanelClosing = false; + private NowPlayingFragment nowPlayingFragment; + private SubsonicFragment secondaryFragment; + private Toolbar mainToolbar; + private Toolbar nowPlayingToolbar; + + private View bottomBar; + private ImageView coverArtView; + private TextView trackView; + private TextView artistView; + private ImageButton startButton; + private long lastBackPressTime = 0; + private DownloadFile currentPlaying; + private PlayerState currentState; + private ImageButton previousButton; + private ImageButton nextButton; + private ImageButton rewindButton; + private ImageButton fastforwardButton; + + @Override + public void onCreate(Bundle savedInstanceState) { + if(savedInstanceState == null) { + String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE); + boolean firstRun = false; + if (fragmentType == null) { + fragmentType = Util.openToTab(this); + if (fragmentType != null) { + firstRun = true; + } + } + + if ("".equals(fragmentType) || fragmentType == null || firstRun) { + // Initial startup stuff + if (!sessionInitialized) { + loadSession(); + } + } + } + + super.onCreate(savedInstanceState); + if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) { + stopService(new Intent(this, DownloadService.class)); + finish(); + getImageLoader().clearCache(); + } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) { + getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Download"); + lastSelectedPosition = R.id.drawer_downloading; + } + setContentView(R.layout.abstract_fragment_activity); + + if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) { + String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE); + if(fragmentType == null) { + fragmentType = Util.openToTab(this); + if(fragmentType != null) { + getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + lastSelectedPosition = getDrawerItemId(fragmentType); + } else { + lastSelectedPosition = R.id.drawer_library; + } + + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } else { + lastSelectedPosition = getDrawerItemId(fragmentType); + } + + currentFragment = getNewFragment(fragmentType); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ID)) { + Bundle currentArguments = currentFragment.getArguments(); + if(currentArguments == null) { + currentArguments = new Bundle(); + } + currentArguments.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + currentFragment.setArguments(currentArguments); + } + currentFragment.setPrimaryFragment(true); + getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); + + if(getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + SearchFragment fragment = new SearchFragment(); + replaceFragment(fragment, fragment.getSupportTag()); + } + + // If a album type is set, switch to that album type view + String albumType = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE); + if(albumType != null) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, albumType); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0); + + fragment.setArguments(args); + replaceFragment(fragment, fragment.getSupportTag()); + } + } + + slideUpPanel = (SlidingUpPanelLayout) findViewById(R.id.slide_up_panel); + panelSlideListener = new SlidingUpPanelLayout.PanelSlideListener() { + @Override + public void onPanelSlide(View panel, float slideOffset) { + + } + + @Override + public void onPanelCollapsed(View panel) { + isPanelClosing = false; + if(bottomBar.getVisibility() == View.GONE) { + bottomBar.setVisibility(View.VISIBLE); + nowPlayingToolbar.setVisibility(View.GONE); + nowPlayingFragment.setPrimaryFragment(false); + setSupportActionBar(mainToolbar); + recreateSpinner(); + } + } + + @Override + public void onPanelExpanded(View panel) { + isPanelClosing = false; + currentFragment.stopActionMode(); + + // Disable custom view before switching + getSupportActionBar().setDisplayShowCustomEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(true); + + bottomBar.setVisibility(View.GONE); + nowPlayingToolbar.setVisibility(View.VISIBLE); + setSupportActionBar(nowPlayingToolbar); + + if(secondaryFragment == null) { + nowPlayingFragment.setPrimaryFragment(true); + } else { + secondaryFragment.setPrimaryFragment(true); + } + + drawerToggle.setDrawerIndicatorEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public void onPanelAnchored(View panel) { + + } + + @Override + public void onPanelHidden(View panel) { + + } + }; + slideUpPanel.setPanelSlideListener(panelSlideListener); + + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD)) { + // Post this later so it actually runs + handler.postDelayed(new Runnable() { + @Override + public void run() { + openNowPlaying(); + } + }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); + } + + bottomBar = findViewById(R.id.bottom_bar); + mainToolbar = (Toolbar) findViewById(R.id.main_toolbar); + nowPlayingToolbar = (Toolbar) findViewById(R.id.now_playing_toolbar); + coverArtView = (ImageView) bottomBar.findViewById(R.id.album_art); + trackView = (TextView) bottomBar.findViewById(R.id.track_name); + artistView = (TextView) bottomBar.findViewById(R.id.artist_name); + + setSupportActionBar(mainToolbar); + + if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) { + nowPlayingFragment = new NowPlayingFragment(); + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.add(R.id.now_playing_fragment_container, nowPlayingFragment, nowPlayingFragment.getTag() + ""); + trans.commit(); + } + + rewindButton = (ImageButton) findViewById(R.id.download_rewind); + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().rewind(); + return null; + } + }.execute(); + } + }); + + previousButton = (ImageButton) findViewById(R.id.download_previous); + previousButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + getDownloadService().previous(); + return null; + } + }.execute(); + } + }); + + startButton = (ImageButton) findViewById(R.id.download_start); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + PlayerState state = getDownloadService().getPlayerState(); + if(state == PlayerState.STARTED) { + getDownloadService().pause(); + } else { + getDownloadService().start(); + } + + return null; + } + }.execute(); + } + }); + + nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + getDownloadService().next(); + return null; + } + }.execute(); + } + }); + + fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward); + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().fastForward(); + return null; + } + }.execute(); + } + }); + } + + @Override + protected void onPostCreate(Bundle bundle) { + super.onPostCreate(bundle); + + showInfoDialog(); + checkUpdates(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + + if(currentFragment instanceof SearchFragment) { + String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); + boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); + + if (query != null) { + ((SearchFragment)currentFragment).search(query, autoplay); + } + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY); + } else { + setIntent(intent); + + SearchFragment fragment = new SearchFragment(); + replaceFragment(fragment, fragment.getSupportTag()); + } + } else if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, false)) { + if(slideUpPanel.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) { + openNowPlaying(); + } + } else { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + setIntent(intent); + } + if(drawer != null) { + drawer.closeDrawers(); + } + } + + @Override + public void onResume() { + super.onResume(); + + if(getIntent().hasExtra(Constants.INTENT_EXTRA_VIEW_ALBUM)) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME)); + args.putString(Constants.INTENT_EXTRA_SEARCH_SONG, getIntent().getStringExtra(Constants.INTENT_EXTRA_SEARCH_SONG)); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ARTIST)) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + } + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID)) { + args.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID)); + } + fragment.setArguments(args); + + replaceFragment(fragment, fragment.getSupportTag()); + getIntent().removeExtra(Constants.INTENT_EXTRA_VIEW_ALBUM); + } + + UserUtil.seedCurrentUser(this); + createAccount(); + runWhenServiceAvailable(new Runnable() { + @Override + public void run() { + getDownloadService().addOnSongChangedListener(SubsonicFragmentActivity.this, true); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + DownloadService downloadService = getDownloadService(); + if(downloadService != null) { + downloadService.removeOnSongChangeListener(this); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING, nowPlayingFragment.getTag()); + if(secondaryFragment != null) { + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING_SECONDARY, secondaryFragment.getTag()); + } + savedInstanceState.putInt(Constants.MAIN_SLIDE_PANEL_STATE, slideUpPanel.getPanelState().hashCode()); + } + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + String id = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING); + FragmentManager fm = getSupportFragmentManager(); + nowPlayingFragment = (NowPlayingFragment) fm.findFragmentByTag(id); + + String secondaryId = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING_SECONDARY); + if(secondaryId != null) { + secondaryFragment = (SubsonicFragment) fm.findFragmentByTag(secondaryId); + + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.hide(nowPlayingFragment); + trans.commit(); + } + + if(drawerToggle != null && backStack.size() > 0) { + drawerToggle.setDrawerIndicatorEnabled(false); + } + + if(savedInstanceState.getInt(Constants.MAIN_SLIDE_PANEL_STATE, -1) == SlidingUpPanelLayout.PanelState.EXPANDED.hashCode()) { + panelSlideListener.onPanelExpanded(null); + } + } + + @Override + public void setContentView(int viewId) { + super.setContentView(viewId); + if(drawerToggle != null){ + drawerToggle.setDrawerIndicatorEnabled(true); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && secondaryFragment == null) { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } else if(onBackPressedSupport()) { + finish(); + } + } + + @Override + public boolean onBackPressedSupport() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + removeCurrent(); + return false; + } else { + return super.onBackPressedSupport(); + } + } + + @Override + public SubsonicFragment getCurrentFragment() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + if(secondaryFragment == null) { + return nowPlayingFragment; + } else { + return secondaryFragment; + } + } else { + return super.getCurrentFragment(); + } + } + + @Override + public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { + if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) { + secondaryFragment = fragment; + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right); + trans.hide(nowPlayingFragment); + trans.add(R.id.now_playing_fragment_container, secondaryFragment, tag + ""); + trans.commit(); + } else { + super.replaceFragment(fragment, tag, replaceCurrent); + } + } + @Override + public void removeCurrent() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && secondaryFragment != null) { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left); + trans.remove(secondaryFragment); + trans.show(nowPlayingFragment); + trans.commit(); + + secondaryFragment = null; + nowPlayingFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + } else { + super.removeCurrent(); + } + } + + @Override + public void setTitle(CharSequence title) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + getSupportActionBar().setTitle(title); + } else { + super.setTitle(title); + } + } + + @Override + protected void drawerItemSelected(String fragmentType) { + super.drawerItemSelected(fragmentType); + + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + } + + @Override + public void startFragmentActivity(String fragmentType) { + // Create a transaction that does all of this + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + + // Clear existing stack + for(int i = backStack.size() - 1; i >= 0; i--) { + trans.remove(backStack.get(i)); + } + trans.remove(currentFragment); + backStack.clear(); + + // Create new stack + currentFragment = getNewFragment(fragmentType); + currentFragment.setPrimaryFragment(true); + trans.add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + ""); + + // Done, cleanup + trans.commit(); + supportInvalidateOptionsMenu(); + recreateSpinner(); + if(drawer != null) { + drawer.closeDrawers(); + } + + if(secondaryContainer != null) { + secondaryContainer.setVisibility(View.GONE); + } + if(drawerToggle != null) { + drawerToggle.setDrawerIndicatorEnabled(true); + } + } + + @Override + public void openNowPlaying() { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED); + } + @Override + public void closeNowPlaying() { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; + } + + private SubsonicFragment getNewFragment(String fragmentType) { + if("Artist".equals(fragmentType)) { + return new SelectArtistFragment(); + } else if("Playlist".equals(fragmentType)) { + return new SelectPlaylistFragment(); + } else if("Download".equals(fragmentType)) { + return new DownloadFragment(); + } else { + return new SelectArtistFragment(); + } + } + + public void checkUpdates() { + try { + String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + int ver = Integer.parseInt(version.replace(".", "")); + Updater updater = new Updater(ver); + updater.checkUpdates(this); + } + catch(Exception e) { + + } + } + + private void loadSession() { + loadSettings(); + // If we are on Subsonic 5.2+, save play queue + if(!Util.isOffline(this)) { + loadRemotePlayQueue(); + } + + sessionInitialized = true; + } + private void loadSettings() { + PreferenceManager.setDefaultValues(this, R.xml.settings_appearance, false); + PreferenceManager.setDefaultValues(this, R.xml.settings_cache, false); + PreferenceManager.setDefaultValues(this, R.xml.settings_playback, false); + + SharedPreferences prefs = Util.getPreferences(this); + if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION) || prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null) == null) { + resetCacheLocation(prefs); + } else { + String path = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + File cacheLocation = new File(path); + if(!FileUtil.verifyCanWrite(cacheLocation)) { + // Only warn user if there is a difference saved + if(resetCacheLocation(prefs)) { + Util.info(this, R.string.common_warning, R.string.settings_cache_location_reset); + } + } + } + + if (!prefs.contains(Constants.PREFERENCES_KEY_OFFLINE)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, false); + + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server"); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org"); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "guest"); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + editor.commit(); + } + if(!prefs.contains(Constants.PREFERENCES_KEY_SERVER_COUNT)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + editor.commit(); + } + } + + private boolean resetCacheLocation(SharedPreferences prefs) { + String newDirectory = FileUtil.getDefaultMusicDirectory(this).getPath(); + String oldDirectory = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(newDirectory == null || (oldDirectory != null && newDirectory.equals(oldDirectory))) { + return false; + } else { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, newDirectory); + editor.commit(); + return true; + } + } + + private void loadRemotePlayQueue() { + if(Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER, false)) { + return; + } + + final SubsonicActivity context = this; + new SilentBackgroundTask(this) { + private PlayerQueue playerQueue; + + @Override + protected Void doInBackground() throws Throwable { + try { + MusicService musicService = MusicServiceFactory.getMusicService(context); + PlayerQueue remoteState = musicService.getPlayQueue(context, null); + + // Make sure we wait until download service is ready + DownloadService downloadService = getDownloadService(); + while(downloadService == null || !downloadService.isInitialized()) { + Util.sleepQuietly(100L); + downloadService = getDownloadService(); + } + + // If we had a remote state and it's changed is more recent than our existing state + if(remoteState != null && remoteState.changed != null) { + // Check if changed + 30 seconds since some servers have slight skew + Date remoteChange = new Date(remoteState.changed.getTime() - ALLOWED_SKEW); + Date localChange = downloadService.getLastStateChanged(); + if(localChange == null || localChange.before(remoteChange)) { + playerQueue = remoteState; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get playing queue to server", e); + } + + return null; + } + + @Override + protected void done(Void arg) { + if(!context.isDestroyedCompat() && playerQueue != null) { + promptRestoreFromRemoteQueue(playerQueue); + } + } + }.execute(); + } + private void promptRestoreFromRemoteQueue(final PlayerQueue remoteState) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + String message = getResources().getString(R.string.common_confirm_message, Util.formatDate(remoteState.changed)); + builder.setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.common_confirm) + .setMessage(message) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + downloadService.clear(); + downloadService.download(remoteState.songs, false, false, false, false, remoteState.currentPlayingIndex, remoteState.currentPlayingPosition); + return null; + } + }.execute(); + } + }) + .setNeutralButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + downloadService.serializeQueue(false); + return null; + } + }.execute(); + } + }) + .setNegativeButton(R.string.common_never, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + downloadService.serializeQueue(false); + + SharedPreferences.Editor editor = Util.getPreferences(SubsonicFragmentActivity.this).edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER, true); + editor.commit(); + return null; + } + }.execute(); + } + }); + + builder.create().show(); + } + + private void createAccount() { + final Context context = this; + + new SilentBackgroundTask(this) { + @Override + protected Void doInBackground() throws Throwable { + AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE); + Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); + accountManager.addAccountExplicitly(account, null, null); + + SharedPreferences prefs = Util.getPreferences(context); + boolean syncEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_ENABLED, true); + int syncInterval = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SYNC_INTERVAL, "60")); + + // Add enabled/frequency to playlist syncing + ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled); + ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval); + + return null; + } + + @Override + protected void done(Void result) { + + } + }.execute(); + } + + private void showInfoDialog() { + if (!infoDialogDisplayed) { + infoDialogDisplayed = true; + if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) { + Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text); + } + } + } + + public Toolbar getActiveToolbar() { + return slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED ? nowPlayingToolbar : mainToolbar; + } + + @Override + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + this.currentPlaying = currentPlaying; + + MusicDirectory.Entry song = null; + if (currentPlaying != null) { + song = currentPlaying.getSong(); + trackView.setText(song.getTitle()); + + if(song.getArtist() != null) { + artistView.setVisibility(View.VISIBLE); + artistView.setText(song.getArtist()); + } else { + artistView.setVisibility(View.GONE); + } + } else { + trackView.setText(R.string.main_title); + artistView.setText(R.string.main_artist); + } + + if (coverArtView != null) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + } + + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + + @Override + public void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex) { + if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { + onSongChanged(currentPlaying, currentPlayingIndex); + } + } + + @Override + public void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable) { + + } + + @Override + public void onStateUpdate(DownloadFile downloadFile, PlayerState playerState) { + int[] attrs = new int[]{(playerState == PlayerState.STARTED) ? R.attr.actionbar_pause : R.attr.actionbar_start}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + startButton.setImageResource(typedArray.getResourceId(0, 0)); + typedArray.recycle(); + } + + @Override + public void onMetadataUpdate(MusicDirectory.Entry song, int fieldChange) { + if(song != null && coverArtView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + + // We need to update it immediately since it won't update if updater is not running for it + if(nowPlayingFragment != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) { + nowPlayingFragment.onMetadataUpdate(song, fieldChange); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java new file mode 100644 index 0000000..4f22542 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.activity; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.SearchRecentSuggestions; +import android.util.Log; + +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.provider.AudinautSearchProvider; + +/** + * Receives voice search queries and forwards to the SearchFragment. + * + * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html + * + * @author Sindre Mehus + */ +public class VoiceQueryReceiverActivity extends Activity { + private static final String TAG = VoiceQueryReceiverActivity.class.getSimpleName(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String query = getIntent().getStringExtra(SearchManager.QUERY); + + if (query != null) { + Intent intent = new Intent(VoiceQueryReceiverActivity.this, SubsonicFragmentActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query); + intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + intent.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS)); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent); + } + finish(); + Util.disablePendingTransition(this); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java new file mode 100644 index 0000000..282562e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java @@ -0,0 +1,44 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; + +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.view.FastScroller; + +public class AlphabeticalAlbumAdapter extends EntryInfiniteGridAdapter implements FastScroller.BubbleTextGetter { + public AlphabeticalAlbumAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries, imageLoader, largeCell); + } + + @Override + public String getTextToShowInBubble(int position) { + // Make sure that we are not trying to get an item for the loading placeholder + if(position >= sections.get(0).size()) { + if(sections.get(0).size() > 0) { + return getTextToShowInBubble(position - 1); + } else { + return "*"; + } + } else { + return getNameIndex(getItemForPosition(position).getAlbum()); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java new file mode 100644 index 0000000..ef934b3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java @@ -0,0 +1,162 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.support.v7.widget.PopupMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.io.Serializable; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class ArtistAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_SONG = 3; + public static int VIEW_TYPE_ARTIST = 4; + + private List musicFolders; + private OnMusicFolderChanged onMusicFolderChanged; + + public ArtistAdapter(Context context, List artists, OnItemClickedListener listener) { + this(context, artists, null, listener, null); + } + + public ArtistAdapter(Context context, List artists, List musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { + super(context, artists); + this.musicFolders = musicFolders; + this.onItemClickedListener = onItemClickedListener; + this.onMusicFolderChanged = onMusicFolderChanged; + + if(musicFolders != null) { + this.singleSectionHeader = true; + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + final View header = LayoutInflater.from(context).inflate(R.layout.select_artist_header, parent, false); + header.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popup = new PopupMenu(context, header.findViewById(R.id.select_artist_folder_2)); + + popup.getMenu().add(R.string.select_artist_all_folders); + for (MusicFolder musicFolder : musicFolders) { + popup.getMenu().add(musicFolder.getName()); + } + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + for (MusicFolder musicFolder : musicFolders) { + if(item.getTitle().equals(musicFolder.getName())) { + if(onMusicFolderChanged != null) { + onMusicFolderChanged.onMusicFolderChanged(musicFolder); + } + return true; + } + } + + if(onMusicFolderChanged != null) { + onMusicFolderChanged.onMusicFolderChanged(null); + } + return true; + } + }); + popup.show(); + } + }); + + return new UpdateView.UpdateViewHolder(header, false); + } + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { + TextView folderName = (TextView) holder.getView().findViewById(R.id.select_artist_folder_2); + + String musicFolderId = Util.getSelectedMusicFolderId(context); + if(musicFolderId != null) { + for (MusicFolder musicFolder : musicFolders) { + if (musicFolder.getId().equals(musicFolderId)) { + folderName.setText(musicFolder.getName()); + break; + } + } + } else { + folderName.setText(R.string.select_artist_all_folders); + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + Entry entry = (Entry) item; + songView.setObject(entry, checkable); + } + } + + @Override + public int getItemViewType(Serializable item) { + if(item instanceof Artist) { + return VIEW_TYPE_ARTIST; + } else { + return VIEW_TYPE_SONG; + } + } + + @Override + public String getTextToShowInBubble(int position) { + Object item = getItemForPosition(position); + if(item instanceof Artist) { + return getNameIndex(((Artist) item).getName(), true); + } else { + return null; + } + } + + public interface OnMusicFolderChanged { + void onMusicFolderChanged(MusicFolder musicFolder); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java new file mode 100644 index 0000000..c6f7c3b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java @@ -0,0 +1,48 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.view.BasicListView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class BasicListAdapter extends SectionAdapter { + public static int VIEW_TYPE_LINE = 1; + + public BasicListAdapter(Context context, List strings, OnItemClickedListener listener) { + super(context, strings); + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new BasicListView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, String item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(String item) { + return VIEW_TYPE_LINE; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java new file mode 100644 index 0000000..f3ef084 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +import github.nvllsvm.audinaut.R; + +public class DetailsAdapter extends ArrayAdapter { + private List headers; + private List details; + + public DetailsAdapter(Context context, int layout, List headers, List details) { + super(context, layout, headers); + + this.headers = headers; + this.details = details; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent){ + View view; + if(convertView == null) { + view = LayoutInflater.from(getContext()).inflate(R.layout.details_item, null); + } else { + view = convertView; + } + + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + + nameView.setText(headers.get(position)); + + detailsView.setText(details.get(position)); + Linkify.addLinks(detailsView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + + return view; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java new file mode 100644 index 0000000..e9b7b49 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java @@ -0,0 +1,74 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFileAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_DOWNLOAD_FILE = 1; + + public DownloadFileAdapter(Context context, List entries, OnItemClickedListener onItemClickedListener) { + super(context, entries); + this.onItemClickedListener = onItemClickedListener; + this.checkable = true; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new SongView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) { + SongView songView = (SongView) holder.getUpdateView(); + songView.setObject(item.getSong(), Util.isBatchMode(context)); + songView.setDownloadFile(item); + } + + @Override + public int getItemViewType(DownloadFile item) { + return VIEW_TYPE_DOWNLOAD_FILE; + } + + @Override + public String getTextToShowInBubble(int position) { + return null; + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_nowplaying, menu); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java new file mode 100644 index 0000000..88d9a03 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java @@ -0,0 +1,156 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.view.UpdateView.UpdateViewHolder; + +public class EntryGridAdapter extends SectionAdapter { + private static String TAG = EntryGridAdapter.class.getSimpleName(); + + public static int VIEW_TYPE_ALBUM_CELL = 1; + public static int VIEW_TYPE_ALBUM_LINE = 2; + public static int VIEW_TYPE_SONG = 3; + + private ImageLoader imageLoader; + private boolean largeAlbums; + private boolean showArtist = false; + private boolean showAlbum = false; + private boolean removeFromPlaylist = false; + private View header; + + public EntryGridAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries); + this.imageLoader = imageLoader; + this.largeAlbums = largeCell; + + // Always show artist if they aren't all the same + String artist = null; + for(MusicDirectory.Entry entry: entries) { + if(artist == null) { + artist = entry.getArtist(); + } + + if(artist != null && !artist.equals(entry.getArtist())) { + showArtist = true; + } + } + checkable = true; + } + + @Override + public UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ALBUM_LINE || viewType == VIEW_TYPE_ALBUM_CELL) { + updateView = new AlbumView(context, viewType == VIEW_TYPE_ALBUM_CELL); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateViewHolder holder, Entry entry, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + AlbumView albumView = (AlbumView) view; + albumView.setShowArtist(showArtist); + albumView.setObject(entry, imageLoader); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + songView.setShowAlbum(showAlbum); + songView.setObject(entry, checkable); + } + } + + public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateViewHolder(header, false); + } + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { + + } + + @Override + public int getItemViewType(Entry entry) { + if(entry.isDirectory()) { + if (largeAlbums) { + return VIEW_TYPE_ALBUM_CELL; + } else { + return VIEW_TYPE_ALBUM_LINE; + } + } else { + return VIEW_TYPE_SONG; + } + } + + public void setHeader(View header) { + this.header = header; + this.singleSectionHeader = true; + } + public View getHeader() { + return header; + } + + public void setShowArtist(boolean showArtist) { + this.showArtist = showArtist; + } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } + + public void removeAt(int index) { + sections.get(0).remove(index); + if(header != null) { + index++; + } + notifyItemRemoved(index); + } + + public void setRemoveFromPlaylist(boolean removeFromPlaylist) { + this.removeFromPlaylist = removeFromPlaylist; + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_media_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_media, menu); + } + + if(!removeFromPlaylist) { + menu.removeItem(R.id.menu_remove_playlist); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java new file mode 100644 index 0000000..dfd1a4b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java @@ -0,0 +1,152 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.fragments.MainFragment; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.view.UpdateView; + +public class EntryInfiniteGridAdapter extends EntryGridAdapter { + public static int VIEW_TYPE_LOADING = 4; + + private String type; + private String extra; + private int size; + + private boolean loading = false; + private boolean allLoaded = false; + + public EntryInfiniteGridAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries, imageLoader, largeCell); + } + + @Override + public UpdateView.UpdateViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_LOADING) { + View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null); + progress.setVisibility(View.VISIBLE); + return new UpdateView.UpdateViewHolder(progress, false); + } + + return super.onCreateViewHolder(parent, viewType); + } + + @Override + public int getItemViewType(int position) { + if(isLoadingView(position)) { + return VIEW_TYPE_LOADING; + } + + return super.getItemViewType(position); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, int position) { + if(!isLoadingView(position)) { + super.onBindViewHolder(holder, position); + } + } + + @Override + public int getItemCount() { + int size = super.getItemCount(); + + if(!allLoaded) { + size++; + } + + return size; + } + + public void setData(String type, String extra, int size) { + this.type = type; + this.extra = extra; + this.size = size; + + if(super.getItemCount() < size) { + allLoaded = true; + } + } + + public void loadMore() { + if(loading || allLoaded) { + return; + } + loading = true; + + new SilentBackgroundTask(context) { + private List newData; + + @Override + protected Void doInBackground() throws Throwable { + newData = cacheInBackground(); + return null; + } + + @Override + protected void done(Void result) { + appendCachedData(newData); + loading = false; + + if(newData.size() < size) { + allLoaded = true; + notifyDataSetChanged(); + } + } + }.execute(); + } + + protected List cacheInBackground() throws Exception { + MusicService service = MusicServiceFactory.getMusicService(context); + MusicDirectory result; + int offset = sections.get(0).size(); + if("genres".equals(type) || "years".equals(type)) { + result = service.getAlbumList(type, extra, size, offset, false, context, null); + } else if("genres".equals(type) || "genres-songs".equals(type)) { + result = service.getSongsByGenre(extra, size, offset, context, null); + }else if(type.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(type, size, offset, context, null); + } else { + result = service.getAlbumList(type, size, offset, false, context, null); + } + return result.getChildren(); + } + + protected void appendCachedData(List newData) { + if(newData.size() > 0) { + int start = sections.get(0).size(); + sections.get(0).addAll(newData); + this.notifyItemRangeInserted(start, newData.size()); + } + } + + protected boolean isLoadingView(int position) { + return !allLoaded && position >= sections.get(0).size(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java new file mode 100644 index 0000000..6fdf3d4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java @@ -0,0 +1,150 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.UpdateView; + +public abstract class ExpandableSectionAdapter extends SectionAdapter { + private static final String TAG = ExpandableSectionAdapter.class.getSimpleName(); + private static final int DEFAULT_VISIBLE = 4; + private static final int EXPAND_TOGGLE = R.attr.select_server; + private static final int COLLAPSE_TOGGLE = R.attr.select_tabs; + + protected List sectionsDefaultVisible; + protected List> sectionsExtras; + protected int expandToggleRes; + protected int collapseToggleRes; + + protected ExpandableSectionAdapter() {} + public ExpandableSectionAdapter(Context context, List section) { + List> sections = new ArrayList<>(); + sections.add(section); + + init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null)); + } + public ExpandableSectionAdapter(Context context, List headers, List> sections) { + init(context, headers, sections, null); + } + public ExpandableSectionAdapter(Context context, List headers, List> sections, List sectionsDefaultVisible) { + init(context, headers, sections, sectionsDefaultVisible); + } + protected void init(Context context, List headers, List> fullSections, List sectionsDefaultVisible) { + this.context = context; + this.headers = headers; + this.sectionsDefaultVisible = sectionsDefaultVisible; + if(sectionsDefaultVisible == null) { + sectionsDefaultVisible = new ArrayList<>(fullSections.size()); + for(int i = 0; i < fullSections.size(); i++) { + sectionsDefaultVisible.add(DEFAULT_VISIBLE); + } + } + + this.sections = new ArrayList<>(); + this.sectionsExtras = new ArrayList<>(); + int i = 0; + for(List fullSection: fullSections) { + List visibleSection = new ArrayList<>(); + + Integer defaultVisible = sectionsDefaultVisible.get(i); + if(defaultVisible == null || defaultVisible >= fullSection.size()) { + visibleSection.addAll(fullSection); + this.sectionsExtras.add(null); + } else { + visibleSection.addAll(fullSection.subList(0, defaultVisible)); + this.sectionsExtras.add(fullSection.subList(defaultVisible, fullSection.size())); + } + this.sections.add(visibleSection); + + i++; + } + + expandToggleRes = DrawableTint.getDrawableRes(context, EXPAND_TOGGLE); + collapseToggleRes = DrawableTint.getDrawableRes(context, COLLAPSE_TOGGLE); + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.expandable_header)); + } + + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, final int sectionIndex) { + UpdateView view = holder.getUpdateView(); + ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); + + List visibleSelection = sections.get(sectionIndex); + List sectionExtras = sectionsExtras.get(sectionIndex); + + if(sectionExtras != null && !sectionExtras.isEmpty()) { + toggleSelectionView.setVisibility(View.VISIBLE); + toggleSelectionView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List visibleSelection = sections.get(sectionIndex); + List sectionExtras = sectionsExtras.get(sectionIndex); + + // Update icon + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = COLLAPSE_TOGGLE; + + // Update how many are displayed + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + visibleSelection.addAll(sectionExtras); + notifyItemRangeInserted(lastIndex, sectionExtras.size()); + } else { + selectToggleAttr = EXPAND_TOGGLE; + + // Update how many are displayed + visibleSelection.removeAll(sectionExtras); + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + notifyItemRangeRemoved(lastIndex, sectionExtras.size()); + } + + ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } + }); + + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = EXPAND_TOGGLE; + } else { + selectToggleAttr = COLLAPSE_TOGGLE; + } + + toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } else { + toggleSelectionView.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(header); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java new file mode 100644 index 0000000..aa97469 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java @@ -0,0 +1,54 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.ViewGroup; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.GenreView; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.List; + +public class GenreAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter{ + public static int VIEW_TYPE_GENRE = 1; + + public GenreAdapter(Context context, List genres, OnItemClickedListener listener) { + super(context, genres); + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new GenreView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Genre item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(Genre item) { + return VIEW_TYPE_GENRE; + } + + @Override + public String getTextToShowInBubble(int position) { + return getNameIndex(getItemForPosition(position).getName()); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java new file mode 100644 index 0000000..5a8bca6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java @@ -0,0 +1,96 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumListCountView; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.BasicListView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class MainAdapter extends SectionAdapter { + public static final int VIEW_TYPE_ALBUM_LIST = 1; + public static final int VIEW_TYPE_ALBUM_COUNT_LIST = 2; + + public MainAdapter(Context context, List headers, List> sections, OnItemClickedListener onItemClickedListener) { + super(context, headers, sections); + this.onItemClickedListener = onItemClickedListener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView; + if(viewType == VIEW_TYPE_ALBUM_LIST) { + updateView = new BasicListView(context); + } else { + updateView = new AlbumListCountView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Integer item, int viewType) { + UpdateView updateView = holder.getUpdateView(); + + if(viewType == VIEW_TYPE_ALBUM_LIST) { + updateView.setObject(context.getResources().getString(item)); + } else { + updateView.setObject(item); + } + } + + @Override + public int getItemViewType(Integer item) { + if(item == R.string.main_albums_newest) { + return VIEW_TYPE_ALBUM_COUNT_LIST; + } else { + return VIEW_TYPE_ALBUM_LIST; + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header)); + } + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { + UpdateView view = holder.getUpdateView(); + CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox); + + String display; + if("songs".equals(header)) { + display = context.getResources().getString(R.string.search_songs); + checkBox.setVisibility(View.GONE); + } else { + display = header; + checkBox.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(display); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java new file mode 100644 index 0000000..9ac61dc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java @@ -0,0 +1,72 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; + +import java.util.List; + +import android.view.ViewGroup; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.PlaylistView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class PlaylistAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_PLAYLIST = 1; + + private ImageLoader imageLoader; + private boolean largeCell; + + public PlaylistAdapter(Context context, List playlists, ImageLoader imageLoader, boolean largeCell, OnItemClickedListener listener) { + super(context, playlists); + this.imageLoader = imageLoader; + this.largeCell = largeCell; + this.onItemClickedListener = listener; + } + public PlaylistAdapter(Context context, List headers, List> sections, ImageLoader imageLoader, boolean largeCell, OnItemClickedListener listener) { + super(context, headers, sections); + this.imageLoader = imageLoader; + this.largeCell = largeCell; + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new PlaylistView(context, imageLoader, largeCell)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Playlist playlist, int viewType) { + holder.getUpdateView().setObject(playlist); + holder.setItem(playlist); + } + + @Override + public int getItemViewType(Playlist playlist) { + return VIEW_TYPE_PLAYLIST; + } + + @Override + public String getTextToShowInBubble(int position) { + Object item = getItemForPosition(position); + if(item instanceof Playlist) { + return getNameIndex(((Playlist) item).getName()); + } else { + return null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java new file mode 100644 index 0000000..1e7376c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java @@ -0,0 +1,140 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.content.res.Resources; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +import static github.nvllsvm.audinaut.adapter.ArtistAdapter.VIEW_TYPE_ARTIST; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_CELL; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_SONG; + +public class SearchAdapter extends ExpandableSectionAdapter { + private ImageLoader imageLoader; + private boolean largeAlbums; + + private static final int MAX_ARTISTS = 10; + private static final int MAX_ALBUMS = 4; + private static final int MAX_SONGS = 10; + + public SearchAdapter(Context context, SearchResult searchResult, ImageLoader imageLoader, boolean largeAlbums, OnItemClickedListener listener) { + this.imageLoader = imageLoader; + this.largeAlbums = largeAlbums; + + List> sections = new ArrayList<>(); + List headers = new ArrayList<>(); + List defaultVisible = new ArrayList<>(); + Resources res = context.getResources(); + if(!searchResult.getArtists().isEmpty()) { + sections.add((List) (List) searchResult.getArtists()); + headers.add(res.getString(R.string.search_artists)); + defaultVisible.add(MAX_ARTISTS); + } + if(!searchResult.getAlbums().isEmpty()) { + sections.add((List) (List) searchResult.getAlbums()); + headers.add(res.getString(R.string.search_albums)); + defaultVisible.add(MAX_ALBUMS); + } + if(!searchResult.getSongs().isEmpty()) { + sections.add((List) (List) searchResult.getSongs()); + headers.add(res.getString(R.string.search_songs)); + defaultVisible.add(MAX_SONGS); + } + init(context, headers, sections, defaultVisible); + + this.onItemClickedListener = listener; + checkable = true; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + updateView = new AlbumView(context, viewType == VIEW_TYPE_ALBUM_CELL); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } else if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + AlbumView albumView = (AlbumView) view; + albumView.setObject((Entry) item, imageLoader); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + songView.setObject((Entry) item, true); + } else if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } + } + + @Override + public int getItemViewType(Serializable item) { + if(item instanceof Entry) { + Entry entry = (Entry) item; + if (entry.isDirectory()) { + if (largeAlbums) { + return VIEW_TYPE_ALBUM_CELL; + } else { + return VIEW_TYPE_ALBUM_LINE; + } + } else { + return VIEW_TYPE_SONG; + } + } else { + return VIEW_TYPE_ARTIST; + } + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_media_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_media, menu); + } + + menu.removeItem(R.id.menu_remove_playlist); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java new file mode 100644 index 0000000..238a9aa --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java @@ -0,0 +1,516 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.support.v7.view.ActionMode; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.PopupMenu; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.MenuUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.view.UpdateView.UpdateViewHolder; + +public abstract class SectionAdapter extends RecyclerView.Adapter> { + private static String TAG = SectionAdapter.class.getSimpleName(); + public static int VIEW_TYPE_HEADER = 0; + public static String[] ignoredArticles; + + protected Context context; + protected List headers; + protected List> sections; + protected boolean singleSectionHeader; + protected OnItemClickedListener onItemClickedListener; + protected List selected = new ArrayList<>(); + protected List selectedViews = new ArrayList<>(); + protected ActionMode currentActionMode; + protected boolean checkable = false; + + protected SectionAdapter() {} + public SectionAdapter(Context context, List section) { + this(context, section, false); + } + public SectionAdapter(Context context, List section, boolean singleSectionHeader) { + this.context = context; + this.headers = Arrays.asList("Section"); + this.sections = new ArrayList<>(); + this.sections.add(section); + this.singleSectionHeader = singleSectionHeader; + } + public SectionAdapter(Context context, List headers, List> sections) { + this(context, headers, sections, true); + } + public SectionAdapter(Context context, List headers, List> sections, boolean singleSectionHeader){ + this.context = context; + this.headers = headers; + this.sections = sections; + this.singleSectionHeader = singleSectionHeader; + } + + public void replaceExistingData(List section) { + this.sections = new ArrayList<>(); + this.sections.add(section); + notifyDataSetChanged(); + } + public void replaceExistingData(List headers, List> sections) { + this.headers = headers; + this.sections = sections; + notifyDataSetChanged(); + } + + @Override + public UpdateViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_HEADER) { + return onCreateHeaderHolder(parent); + } else { + final UpdateViewHolder holder = onCreateSectionViewHolder(parent, viewType); + final UpdateView updateView = holder.getUpdateView(); + + if(updateView != null) { + updateView.getChildAt(0).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + T item = holder.getItem(); + updateView.onClick(); + if (currentActionMode != null) { + if(updateView.isCheckable()) { + if (selected.contains(item)) { + selected.remove(item); + selectedViews.remove(updateView); + setChecked(updateView, false); + } else { + selected.add(item); + selectedViews.add(updateView); + setChecked(updateView, true); + } + + if (selected.isEmpty()) { + currentActionMode.finish(); + } else { + currentActionMode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); + } + } + } else if (onItemClickedListener != null) { + onItemClickedListener.onItemClicked(updateView, item); + } + } + }); + + View moreButton = updateView.findViewById(R.id.item_more); + if (moreButton != null) { + moreButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + final T item = holder.getItem(); + if (onItemClickedListener != null) { + PopupMenu popup = new PopupMenu(context, v); + onItemClickedListener.onCreateContextMenu(popup.getMenu(), popup.getMenuInflater(), updateView, item); + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + return onItemClickedListener.onContextItemSelected(menuItem, updateView, item); + } + }); + popup.show(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to show popup", e); + } + } + }); + + if(checkable) { + updateView.getChildAt(0).setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if(updateView.isCheckable()) { + if (currentActionMode == null) { + startActionMode(holder); + } else { + updateView.getChildAt(0).performClick(); + } + } + return true; + } + }); + } + } + } + + return holder; + } + } + + @Override + public void onBindViewHolder(UpdateViewHolder holder, int position) { + UpdateView updateView = holder.getUpdateView(); + + if(sections.size() == 1 && !singleSectionHeader) { + T item = sections.get(0).get(position); + onBindViewHolder(holder, item, getItemViewType(position)); + postBindView(updateView, item); + holder.setItem(item); + return; + } + + int subPosition = 0; + int subHeader = 0; + for(List section: sections) { + boolean validHeader = headers.get(subHeader) != null; + if(position == subPosition && validHeader) { + onBindHeaderHolder(holder, headers.get(subHeader), subHeader); + return; + } + + int headerOffset = validHeader ? 1 : 0; + if(position < (subPosition + section.size() + headerOffset)) { + T item = section.get(position - subPosition - headerOffset); + onBindViewHolder(holder, item, getItemViewType(item)); + + postBindView(updateView, item); + holder.setItem(item); + return; + } + + subPosition += section.size(); + if(validHeader) { + subPosition += 1; + } + subHeader++; + } + } + + private void postBindView(UpdateView updateView, T item) { + if(updateView.isCheckable()) { + setChecked(updateView, selected.contains(item)); + } + + View moreButton = updateView.findViewById(R.id.item_more); + if(moreButton != null) { + if(onItemClickedListener != null) { + PopupMenu popup = new PopupMenu(context, moreButton); + Menu menu = popup.getMenu(); + onItemClickedListener.onCreateContextMenu(popup.getMenu(), popup.getMenuInflater(), updateView, item); + if (menu.size() == 0) { + moreButton.setVisibility(View.GONE); + } else { + moreButton.setVisibility(View.VISIBLE); + } + } else { + moreButton.setVisibility(View.VISIBLE); + } + } + } + + @Override + public int getItemCount() { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).size(); + } + + int count = 0; + for(String header: headers) { + if(header != null) { + count++; + } + } + for(List section: sections) { + count += section.size(); + } + + return count; + } + + @Override + public int getItemViewType(int position) { + if(sections.size() == 1 && !singleSectionHeader) { + return getItemViewType(sections.get(0).get(position)); + } + + int subPosition = 0; + int subHeader = 0; + for(List section: sections) { + boolean validHeader = headers.get(subHeader) != null; + if(position == subPosition && validHeader) { + return VIEW_TYPE_HEADER; + } + + int headerOffset = validHeader ? 1 : 0; + if(position < (subPosition + section.size() + headerOffset)) { + return getItemViewType(section.get(position - subPosition - headerOffset)); + } + + subPosition += section.size(); + if(validHeader) { + subPosition += 1; + } + subHeader++; + } + + return -1; + } + + public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateViewHolder(new BasicHeaderView(context)); + } + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { + UpdateView view = holder.getUpdateView(); + if(view != null) { + view.setObject(header); + } + } + + public T getItemForPosition(int position) { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).get(position); + } + + int subPosition = 0; + for(List section: sections) { + if(position == subPosition) { + return null; + } + + if(position <= (subPosition + section.size())) { + return section.get(position - subPosition - 1); + } + + subPosition += section.size() + 1; + } + + return null; + } + public int getItemPosition(T item) { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).indexOf(item); + } + + int subPosition = 0; + for(List section: sections) { + subPosition += section.size() + 1; + + int position = section.indexOf(item); + if(position != -1) { + return position + subPosition; + } + } + + return -1; + } + + public void setOnItemClickedListener(OnItemClickedListener onItemClickedListener) { + this.onItemClickedListener = onItemClickedListener; + } + + public void addSelected(T item) { + selected.add(item); + } + public List getSelected() { + List selected = new ArrayList<>(); + selected.addAll(this.selected); + return selected; + } + + public void clearSelected() { + // TODO: This needs to work with multiple sections + for(T item: selected) { + int index = sections.get(0).indexOf(item); + + if(singleSectionHeader) { + index++; + } + } + selected.clear(); + + for(UpdateView updateView: selectedViews) { + updateView.setChecked(false); + } + } + + public void moveItem(int from, int to) { + List section = sections.get(0); + int max = section.size(); + if(to >= max) { + to = max - 1; + } else if(to < 0) { + to = 0; + } + + T moved = section.remove(from); + section.add(to, moved); + + notifyItemMoved(from, to); + } + public void removeItem(T item) { + int subPosition = 0; + for(List section: sections) { + if(sections.size() > 1 || singleSectionHeader) { + subPosition++; + } + + int index = section.indexOf(item); + if (index != -1) { + section.remove(item); + notifyItemRemoved(subPosition + index); + break; + } + + subPosition += section.size(); + } + } + + public abstract UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType); + public abstract void onBindViewHolder(UpdateViewHolder holder, T item, int viewType); + public abstract int getItemViewType(T item); + public void setCheckable(boolean checkable) { + this.checkable = checkable; + } + public void setChecked(UpdateView updateView, boolean checked) { + updateView.setChecked(checked); + } + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) {} + + private void startActionMode(final UpdateView.UpdateViewHolder holder) { + final UpdateView updateView = holder.getUpdateView(); + if (context instanceof SubsonicFragmentActivity && currentActionMode == null) { + final SubsonicFragmentActivity fragmentActivity = (SubsonicFragmentActivity) context; + fragmentActivity.startSupportActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + currentActionMode = mode; + + T item = holder.getItem(); + selected.add(item); + selectedViews.add(updateView); + setChecked(updateView, true); + + onCreateActionModeMenu(menu, mode.getMenuInflater()); + MenuUtil.hideMenuItems(context, menu, updateView); + + mode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(R.attr.colorPrimaryDark, typedValue, true); + int colorPrimaryDark = typedValue.data; + + Window window = ((SubsonicFragmentActivity) context).getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.setStatusBarColor(colorPrimaryDark); + } + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (fragmentActivity.onOptionsItemSelected(item)) { + currentActionMode.finish(); + return true; + } else { + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + currentActionMode = null; + selected.clear(); + for (UpdateView updateView : selectedViews) { + updateView.setChecked(false); + } + selectedViews.clear(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + Window window = ((SubsonicFragmentActivity) context).getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + }); + } + } + public void stopActionMode() { + if(currentActionMode != null) { + currentActionMode.finish(); + } + } + + public String getNameIndex(String name) { + return getNameIndex(name, false); + } + public String getNameIndex(String name, boolean removeIgnoredArticles) { + if(name == null) { + return "*"; + } + + if(removeIgnoredArticles) { + if (ignoredArticles == null) { + SharedPreferences prefs = Util.getPreferences(context); + String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les"); + ignoredArticles = ignoredArticlesString.split(" "); + } + + name = name.toLowerCase(); + for (String article : ignoredArticles) { + int index = name.indexOf(article.toLowerCase() + " "); + if (index == 0) { + name = name.substring(article.length() + 1); + } + } + } + + String index = name.substring(0, 1).toUpperCase(); + if (!Character.isLetter(index.charAt(0))) { + index = "#"; + } + + return index; + } + + public interface OnItemClickedListener { + void onItemClicked(UpdateView updateView, T item); + void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, T item); + boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, T item); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java new file mode 100644 index 0000000..308b662 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java @@ -0,0 +1,121 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.RecyclingImageView; +import github.nvllsvm.audinaut.view.SettingView; +import github.nvllsvm.audinaut.view.UpdateView; + +import static github.nvllsvm.audinaut.domain.User.Setting; + +public class SettingsAdapter extends SectionAdapter { + private static final String TAG = SettingsAdapter.class.getSimpleName(); + public final int VIEW_TYPE_SETTING = 1; + public final int VIEW_TYPE_SETTING_HEADER = 2; + + private final User user; + private final boolean editable; + private final ImageLoader imageLoader; + + public SettingsAdapter(Context context, User user, List headers, List> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener onItemClickedListener) { + super(context, headers, settingSections, imageLoader != null); + this.user = user; + this.imageLoader = imageLoader; + this.editable = editable; + this.onItemClickedListener = onItemClickedListener; + + for(List settings: sections) { + for (Setting setting : settings) { + if (setting.getValue()) { + addSelected(setting); + } + } + } + } + + @Override + public int getItemViewType(int position) { + int viewType = super.getItemViewType(position); + if(viewType == SectionAdapter.VIEW_TYPE_HEADER) { + if(position == 0 && imageLoader != null) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_SETTING_HEADER; + } + } else { + return viewType; + } + } + + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) { + View header = holder.getView(); + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_SETTING_HEADER) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context)); + } else { + return new UpdateView.UpdateViewHolder(new SettingView(context)); + } + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Setting item, int viewType) { + holder.getUpdateView().setObject(item, editable); + } + + @Override + public int getItemViewType(Setting item) { + return VIEW_TYPE_SETTING; + } + + @Override + public void setChecked(UpdateView updateView, boolean checked) { + if(updateView instanceof SettingView) { + updateView.setChecked(checked); + } + } + + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener onItemClickedListener) { + return getSettingsAdapter(context, user, imageLoader, true, onItemClickedListener); + } + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener onItemClickedListener) { + List headers = new ArrayList<>(); + List> settingsSections = new ArrayList<>(); + settingsSections.add(user.getSettings()); + + if(user.getMusicFolderSettings() != null) { + settingsSections.add(user.getMusicFolderSettings()); + } + + return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java new file mode 100644 index 0000000..a852f93 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java @@ -0,0 +1,69 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.audiofx; + +import android.content.Context; +import android.media.MediaPlayer; +import android.media.audiofx.AudioEffect; +import android.media.audiofx.LoudnessEnhancer; +import android.os.Build; +import android.util.Log; + +public class AudioEffectsController { + private static final String TAG = AudioEffectsController.class.getSimpleName(); + + private final Context context; + private int audioSessionId = 0; + + private boolean available = false; + + private EqualizerController equalizerController; + + public AudioEffectsController(Context context, int audioSessionId) { + this.context = context; + this.audioSessionId = audioSessionId; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + available = true; + } + } + + public boolean isAvailable() { + return available; + } + + public void release() { + if(equalizerController != null) { + equalizerController.release(); + } + } + + public EqualizerController getEqualizerController() { + if (available && equalizerController == null) { + equalizerController = new EqualizerController(context, audioSessionId); + if (!equalizerController.isAvailable()) { + equalizerController = null; + } else { + equalizerController.loadSettings(); + } + } + return equalizerController; + } +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/audiofx/EqualizerController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/EqualizerController.java new file mode 100644 index 0000000..59915a4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/audiofx/EqualizerController.java @@ -0,0 +1,198 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2011 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.audiofx; + +import java.io.Serializable; + +import android.content.Context; +import android.media.audiofx.BassBoost; +import android.media.audiofx.Equalizer; +import android.os.Build; +import android.util.Log; +import github.nvllsvm.audinaut.util.FileUtil; + +/** + * Backward-compatible wrapper for {@link Equalizer}, which is API Level 9. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class EqualizerController { + + private static final String TAG = EqualizerController.class.getSimpleName(); + + private final Context context; + private Equalizer equalizer; + private BassBoost bass; + private boolean loudnessAvailable = false; + private LoudnessEnhancerController loudnessEnhancerController; + private boolean released = false; + private int audioSessionId = 0; + + public EqualizerController(Context context, int audioSessionId) { + this.context = context; + this.audioSessionId = audioSessionId; + init(); + } + + private void init() { + equalizer = new Equalizer(0, audioSessionId); + bass = new BassBoost(0, audioSessionId); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + loudnessAvailable = true; + loudnessEnhancerController = new LoudnessEnhancerController(context, audioSessionId); + } + } + + public void saveSettings() { + try { + if (isAvailable()) { + FileUtil.serialize(context, new EqualizerSettings(equalizer, bass, loudnessEnhancerController), "equalizer.dat"); + } + } catch (Throwable x) { + Log.w(TAG, "Failed to save equalizer settings.", x); + } + } + + public void loadSettings() { + try { + if (isAvailable()) { + EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat", EqualizerSettings.class); + if (settings != null) { + settings.apply(equalizer, bass, loudnessEnhancerController); + } + } + } catch (Throwable x) { + Log.w(TAG, "Failed to load equalizer settings.", x); + } + } + + public boolean isAvailable() { + return equalizer != null && bass != null; + } + + public boolean isEnabled() { + try { + return isAvailable() && equalizer.getEnabled(); + } catch(Exception e) { + return false; + } + } + + public void release() { + if (isAvailable()) { + released = true; + equalizer.release(); + bass.release(); + if(loudnessEnhancerController != null && loudnessEnhancerController.isAvailable()) { + loudnessEnhancerController.release(); + } + } + } + + public Equalizer getEqualizer() { + if(released) { + released = false; + try { + init(); + } catch (Throwable x) { + equalizer = null; + released = true; + Log.w(TAG, "Failed to create equalizer.", x); + } + } + return equalizer; + } + public BassBoost getBassBoost() { + if(released) { + released = false; + try { + init(); + } catch (Throwable x) { + bass = null; + Log.w(TAG, "Failed to create bass booster.", x); + } + } + return bass; + } + public LoudnessEnhancerController getLoudnessEnhancerController() { + if(loudnessAvailable && released) { + released = false; + try { + init(); + } catch (Throwable x) { + loudnessEnhancerController = null; + Log.w(TAG, "Failed to create loudness enhancer.", x); + } + } + return loudnessEnhancerController; + } + + private static class EqualizerSettings implements Serializable { + + private short[] bandLevels; + private short preset; + private boolean enabled; + private short bass; + private int loudness; + + public EqualizerSettings() { + + } + public EqualizerSettings(Equalizer equalizer, BassBoost boost, LoudnessEnhancerController loudnessEnhancerController) { + enabled = equalizer.getEnabled(); + bandLevels = new short[equalizer.getNumberOfBands()]; + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + bandLevels[i] = equalizer.getBandLevel(i); + } + try { + preset = equalizer.getCurrentPreset(); + } catch (Exception x) { + preset = -1; + } + try { + bass = boost.getRoundedStrength(); + } catch(Exception e) { + bass = 0; + } + + try { + loudness = (int) loudnessEnhancerController.getGain(); + } catch(Exception e) { + loudness = 0; + } + } + + public void apply(Equalizer equalizer, BassBoost boost, LoudnessEnhancerController loudnessController) { + for (short i = 0; i < bandLevels.length; i++) { + equalizer.setBandLevel(i, bandLevels[i]); + } + equalizer.setEnabled(enabled); + if(bass != 0) { + boost.setEnabled(true); + boost.setStrength(bass); + } + if(loudness != 0) { + loudnessController.enable(); + loudnessController.setGain(loudness); + } + } + } +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java new file mode 100644 index 0000000..75bfbd4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java @@ -0,0 +1,77 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.audiofx; + +import android.content.Context; +import android.media.audiofx.LoudnessEnhancer; +import android.util.Log; + +public class LoudnessEnhancerController { + private static final String TAG = LoudnessEnhancerController.class.getSimpleName(); + + private final Context context; + private LoudnessEnhancer enhancer; + private boolean released = false; + private int audioSessionId = 0; + + public LoudnessEnhancerController(Context context, int audioSessionId) { + this.context = context; + try { + this.audioSessionId = audioSessionId; + enhancer = new LoudnessEnhancer(audioSessionId); + } catch (Throwable x) { + Log.w(TAG, "Failed to create enhancer", x); + } + } + + public boolean isAvailable() { + return enhancer != null; + } + + public boolean isEnabled() { + try { + return isAvailable() && enhancer.getEnabled(); + } catch(Exception e) { + return false; + } + } + + public void enable() { + enhancer.setEnabled(true); + } + public void disable() { + enhancer.setEnabled(false); + } + + public float getGain() { + return enhancer.getTargetGain(); + } + public void setGain(int gain) { + enhancer.setTargetGain(gain); + } + + public void release() { + if (isAvailable()) { + enhancer.release(); + released = true; + } + } + +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java new file mode 100644 index 0000000..a183d6b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java @@ -0,0 +1,138 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.util.Log; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * @author Sindre Mehus + */ +public class Artist implements Serializable { + private static final String TAG = Artist.class.getSimpleName(); + public static final String ROOT_ID = "-1"; + public static final String MISSING_ID = "-2"; + + private String id; + private String name; + private String index; + private int closeness; + + public Artist() { + + } + public Artist(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public String getIndex() { + return index; + } + public void setIndex(String index) { + this.index = index; + } + + public int getCloseness() { + return closeness; + } + public void setCloseness(int closeness) { + this.closeness = closeness; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Artist entry = (Artist) o; + return id.equals(entry.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return name; + } + + public static class ArtistComparator implements Comparator { + private String[] ignoredArticles; + private Collator collator; + + public ArtistComparator(String[] ignoredArticles) { + this.ignoredArticles = ignoredArticles; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); + } + + public int compare(Artist lhsArtist, Artist rhsArtist) { + String lhs = lhsArtist.getName().toLowerCase(); + String rhs = rhsArtist.getName().toLowerCase(); + + for (String article : ignoredArticles) { + int index = lhs.indexOf(article.toLowerCase() + " "); + if (index == 0) { + lhs = lhs.substring(article.length() + 1); + } + index = rhs.indexOf(article.toLowerCase() + " "); + if (index == 0) { + rhs = rhs.substring(article.length() + 1); + } + } + + return collator.compare(lhs, rhs); + } + } + + public static void sort(List artists, String[] ignoredArticles) { + try { + Collections.sort(artists, new ArtistComparator(ignoredArticles)); + } catch (Exception e) { + Log.w(TAG, "Failed to sort artists", e); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java new file mode 100644 index 0000000..77255f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java @@ -0,0 +1,69 @@ +package github.nvllsvm.audinaut.domain; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +public class Genre implements Serializable { + private String name; + private String index; + private Integer albumCount; + private Integer songCount; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + @Override + public String toString() { + return name; + } + + public Integer getAlbumCount() { + return albumCount; + } + + public void setAlbumCount(Integer albumCount) { + this.albumCount = albumCount; + } + + public Integer getSongCount() { + return songCount; + } + + public void setSongCount(Integer songCount) { + this.songCount = songCount; + } + + public static class GenreComparator implements Comparator { + @Override + public int compare(Genre genre1, Genre genre2) { + return genre1.getName().compareToIgnoreCase(genre2.getName()); + } + + public static List sort(List genres) { + Collections.sort(genres, new GenreComparator()); + return genres; + } + + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java new file mode 100644 index 0000000..0de26c2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java @@ -0,0 +1,87 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; + +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class Indexes implements Serializable { + + private long lastModified; + private List shortcuts; + private List artists; + private List entries; + + public Indexes() { + + } + public Indexes(long lastModified, List shortcuts, List artists) { + this.lastModified = lastModified; + this.shortcuts = shortcuts; + this.artists = artists; + this.entries = new ArrayList(); + } + public Indexes(long lastModified, List shortcuts, List artists, List entries) { + this.lastModified = lastModified; + this.shortcuts = shortcuts; + this.artists = artists; + this.entries = entries; + } + + public long getLastModified() { + return lastModified; + } + + public List getShortcuts() { + return shortcuts; + } + + public List getArtists() { + return artists; + } + + public void setArtists(List artists) { + this.shortcuts = new ArrayList(); + this.artists.clear(); + this.artists.addAll(artists); + } + + public List getEntries() { + return entries; + } + + public void sortChildren(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les"); + final String[] ignoredArticles = ignoredArticlesString.split(" "); + + Artist.sort(shortcuts, ignoredArticles); + Artist.sort(artists, ignoredArticles); + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java new file mode 100644 index 0000000..28f7308 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java @@ -0,0 +1,628 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.media.MediaMetadataRetriever; +import android.os.Build; +import android.util.Log; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.io.File; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class MusicDirectory implements Serializable { + private static final String TAG = MusicDirectory.class.getSimpleName(); + + private String name; + private String id; + private String parent; + private List children; + + public MusicDirectory() { + children = new ArrayList(); + } + public MusicDirectory(List children) { + this.children = children; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public void addChild(Entry child) { + if(child != null) { + children.add(child); + } + } + public void addChildren(List children) { + this.children.addAll(children); + } + + public void replaceChildren(List children) { + this.children = children; + } + + public synchronized List getChildren() { + return getChildren(true, true); + } + + public synchronized List getChildren(boolean includeDirs, boolean includeFiles) { + if (includeDirs && includeFiles) { + return children; + } + + List result = new ArrayList(children.size()); + for (Entry child : children) { + if (child != null && child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) { + result.add(child); + } + } + return result; + } + public synchronized List getSongs() { + List result = new ArrayList(); + for (Entry child : children) { + if (child != null && !child.isDirectory()) { + result.add(child); + } + } + return result; + } + + public synchronized int getChildrenSize() { + return children.size(); + } + + public void shuffleChildren() { + Collections.shuffle(this.children); + } + + public void sortChildren(Context context, int instance) { + sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true)); + } + public void sortChildren(boolean byYear) { + EntryComparator.sort(children, byYear); + } + + public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) { + boolean metadataUpdated = false; + Iterator it = children.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + int index = refreshedDirectory.children.indexOf(entry); + if(index != -1) { + final Entry refreshed = refreshedDirectory.children.get(index); + + entry.setTitle(refreshed.getTitle()); + entry.setAlbum(refreshed.getAlbum()); + entry.setArtist(refreshed.getArtist()); + entry.setTrack(refreshed.getTrack()); + entry.setYear(refreshed.getYear()); + entry.setGenre(refreshed.getGenre()); + entry.setTranscodedContentType(refreshed.getTranscodedContentType()); + entry.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + entry.setDiscNumber(refreshed.getDiscNumber()); + entry.setType(refreshed.getType()); + if(!Util.equals(entry.getCoverArt(), refreshed.getCoverArt())) { + metadataUpdated = true; + entry.setCoverArt(refreshed.getCoverArt()); + } + + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setTitle(refreshed.getTitle()); + found.setAlbum(refreshed.getAlbum()); + found.setArtist(refreshed.getArtist()); + found.setTrack(refreshed.getTrack()); + found.setYear(refreshed.getYear()); + found.setGenre(refreshed.getGenre()); + found.setTranscodedContentType(refreshed.getTranscodedContentType()); + found.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + found.setDiscNumber(refreshed.getDiscNumber()); + found.setType(refreshed.getType()); + if(!Util.equals(found.getCoverArt(), refreshed.getCoverArt())) { + found.setCoverArt(refreshed.getCoverArt()); + metadataUpdate = DownloadService.METADATA_UPDATED_COVER_ART; + } + } + }.execute(); + } + } + + return metadataUpdated; + } + public synchronized boolean updateEntriesList(Context context, int instance, MusicDirectory refreshedDirectory) { + boolean changed = false; + Iterator it = children.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + // No longer exists in here + if(refreshedDirectory.children.indexOf(entry) == -1) { + it.remove(); + changed = true; + } + } + + // Make sure we contain all children from refreshed set + boolean resort = false; + for(Entry refreshed: refreshedDirectory.children) { + if(!this.children.contains(refreshed)) { + this.children.add(refreshed); + resort = true; + changed = true; + } + } + + if(resort) { + this.sortChildren(context, instance); + } + + return changed; + } + + public static class Entry implements Serializable { + public static final int TYPE_SONG = 0; + + private String id; + private String parent; + private String grandParent; + private String albumId; + private String artistId; + private boolean directory; + private String title; + private String album; + private String artist; + private Integer track; + private Integer year; + private String genre; + private String contentType; + private String suffix; + private String transcodedContentType; + private String transcodedSuffix; + private String coverArt; + private Long size; + private Integer duration; + private Integer bitRate; + private String path; + private Integer discNumber; + private int type = 0; + private int closeness; + private transient Artist linkedArtist; + + public Entry() { + + } + public Entry(String id) { + this.id = id; + } + public Entry(Artist artist) { + this.id = artist.getId(); + this.title = artist.getName(); + this.directory = true; + this.linkedArtist = artist; + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) + public void loadMetadata(File file) { + try { + MediaMetadataRetriever metadata = new MediaMetadataRetriever(); + metadata.setDataSource(file.getAbsolutePath()); + String discNumber = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER); + if(discNumber == null) { + discNumber = "1/1"; + } + int slashIndex = discNumber.indexOf("/"); + if(slashIndex > 0) { + discNumber = discNumber.substring(0, slashIndex); + } + try { + setDiscNumber(Integer.parseInt(discNumber)); + } catch(Exception e) { + Log.w(TAG, "Non numbers in disc field!"); + } + String bitrate = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE); + setBitRate(Integer.parseInt((bitrate != null) ? bitrate : "0") / 1000); + String length = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + setDuration(Integer.parseInt(length) / 1000); + String artist = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); + if(artist != null) { + setArtist(artist); + } + String album = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); + if(album != null) { + setAlbum(album); + } + metadata.release(); + } catch(Exception e) { + Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver", e); + } + } + public void rebaseTitleOffPath() { + try { + String filename = getPath(); + if(filename == null) { + return; + } + + int index = filename.lastIndexOf('/'); + if (index != -1) { + filename = filename.substring(index + 1); + if (getTrack() != null) { + filename = filename.replace(String.format("%02d ", getTrack()), ""); + } + + index = filename.lastIndexOf('.'); + if(index != -1) { + filename = filename.substring(0, index); + } + + setTitle(filename); + } + } catch(Exception e) { + Log.w(TAG, "Failed to update title based off of path", e); + } + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public String getGrandParent() { + return grandParent; + } + + public void setGrandParent(String grandParent) { + this.grandParent = grandParent; + } + + public String getAlbumId() { + return albumId; + } + + public void setAlbumId(String albumId) { + this.albumId = albumId; + } + + public String getArtistId() { + return artistId; + } + + public void setArtistId(String artistId) { + this.artistId = artistId; + } + + public boolean isDirectory() { + return directory; + } + + public void setDirectory(boolean directory) { + this.directory = directory; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAlbum() { + return album; + } + + public boolean isAlbum() { + return getParent() != null || getArtist() != null; + } + + public String getAlbumDisplay() { + if(album != null && title.startsWith("Disc ")) { + return album; + } else { + return title; + } + } + + public void setAlbum(String album) { + this.album = album; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public Integer getTrack() { + return track; + } + + public void setTrack(Integer track) { + this.track = track; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getTranscodedContentType() { + return transcodedContentType; + } + + public void setTranscodedContentType(String transcodedContentType) { + this.transcodedContentType = transcodedContentType; + } + + public String getTranscodedSuffix() { + return transcodedSuffix; + } + + public void setTranscodedSuffix(String transcodedSuffix) { + this.transcodedSuffix = transcodedSuffix; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public Integer getDuration() { + return duration; + } + + public void setDuration(Integer duration) { + this.duration = duration; + } + + public Integer getBitRate() { + return bitRate; + } + + public void setBitRate(Integer bitRate) { + this.bitRate = bitRate; + } + + public String getCoverArt() { + return coverArt; + } + + public void setCoverArt(String coverArt) { + this.coverArt = coverArt; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Integer getDiscNumber() { + return discNumber; + } + + public void setDiscNumber(Integer discNumber) { + this.discNumber = discNumber; + } + + public int getType() { + return type; + } + public void setType(int type) { + this.type = type; + } + public boolean isSong() { + return type == TYPE_SONG; + } + + public int getCloseness() { + return closeness; + } + + public void setCloseness(int closeness) { + this.closeness = closeness; + } + + public boolean isOnlineId(Context context) { + try { + String cacheLocation = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + return cacheLocation == null || id == null || id.indexOf(cacheLocation) == -1; + } catch(Exception e) { + Log.w(TAG, "Failed to check online id validity"); + + // Err on the side of default functionality + return true; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Entry entry = (Entry) o; + return id.equals(entry.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return title; + } + } + + public static class EntryComparator implements Comparator { + private boolean byYear; + private Collator collator; + + public EntryComparator(boolean byYear) { + this.byYear = byYear; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); + } + + public int compare(Entry lhs, Entry rhs) { + if(lhs.isDirectory() && !rhs.isDirectory()) { + return -1; + } else if(!lhs.isDirectory() && rhs.isDirectory()) { + return 1; + } else if(lhs.isDirectory() && rhs.isDirectory()) { + if(byYear) { + Integer lhsYear = lhs.getYear(); + Integer rhsYear = rhs.getYear(); + if(lhsYear != null && rhsYear != null) { + return lhsYear.compareTo(rhsYear); + } else if(lhsYear != null) { + return -1; + } else if(rhsYear != null) { + return 1; + } + } + + return collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay()); + } + + Integer lhsDisc = lhs.getDiscNumber(); + Integer rhsDisc = rhs.getDiscNumber(); + + if(lhsDisc != null && rhsDisc != null) { + if(lhsDisc < rhsDisc) { + return -1; + } else if(lhsDisc > rhsDisc) { + return 1; + } + } + + Integer lhsTrack = lhs.getTrack(); + Integer rhsTrack = rhs.getTrack(); + if(lhsTrack != null && rhsTrack != null && lhsTrack != rhsTrack) { + return lhsTrack.compareTo(rhsTrack); + } else if(lhsTrack != null) { + return -1; + } else if(rhsTrack != null) { + return 1; + } + + return collator.compare(lhs.getTitle(), rhs.getTitle()); + } + + public static void sort(List entries) { + sort(entries, true); + } + public static void sort(List entries, boolean byYear) { + try { + Collections.sort(entries, new EntryComparator(byYear)); + } catch (Exception e) { + Log.w(TAG, "Failed to sort MusicDirectory"); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.java b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.java new file mode 100644 index 0000000..5906e68 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.java @@ -0,0 +1,80 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.util.Log; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Represents a top level directory in which music or other media is stored. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class MusicFolder implements Serializable { + private static final String TAG = MusicFolder.class.getSimpleName(); + private String id; + private String name; + private boolean enabled; + + public MusicFolder() { + + } + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean getEnabled() { + return enabled; + } + + public static class MusicFolderComparator implements Comparator { + public int compare(MusicFolder lhsMusicFolder, MusicFolder rhsMusicFolder) { + if(lhsMusicFolder == rhsMusicFolder || lhsMusicFolder.getName().equals(rhsMusicFolder.getName())) { + return 0; + } else { + return lhsMusicFolder.getName().compareToIgnoreCase(rhsMusicFolder.getName()); + } + } + } + + public static void sort(List musicFolders) { + try { + Collections.sort(musicFolders, new MusicFolderComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort music folders", e); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java new file mode 100644 index 0000000..6232203 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java @@ -0,0 +1,30 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class PlayerQueue implements Serializable { + public List songs = new ArrayList(); + public List toDelete = new ArrayList(); + public int currentPlayingIndex; + public int currentPlayingPosition; + public boolean renameCurrent = false; + public Date changed = null; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java new file mode 100644 index 0000000..fe90e89 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java @@ -0,0 +1,47 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.media.RemoteControlClient; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum PlayerState { + IDLE(RemoteControlClient.PLAYSTATE_STOPPED), + DOWNLOADING(RemoteControlClient.PLAYSTATE_BUFFERING), + PREPARING(RemoteControlClient.PLAYSTATE_BUFFERING), + PREPARED(RemoteControlClient.PLAYSTATE_STOPPED), + STARTED(RemoteControlClient.PLAYSTATE_PLAYING), + STOPPED(RemoteControlClient.PLAYSTATE_STOPPED), + PAUSED(RemoteControlClient.PLAYSTATE_PAUSED), + PAUSED_TEMP(RemoteControlClient.PLAYSTATE_PAUSED), + COMPLETED(RemoteControlClient.PLAYSTATE_STOPPED); + + private final int mRemoteControlClientPlayState; + + private PlayerState(int playState) { + mRemoteControlClientPlayState = playState; + } + + public int getRemoteControlClientPlayState() { + return mRemoteControlClientPlayState; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.java new file mode 100644 index 0000000..c79021a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.java @@ -0,0 +1,187 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * @author Sindre Mehus + */ +public class Playlist implements Serializable { + + private String id; + private String name; + private String owner; + private String comment; + private String songCount; + private Boolean pub; + private Date created; + private Date changed; + private Integer duration; + + public Playlist() { + + } + public Playlist(String id, String name) { + this.id = id; + this.name = name; + } + public Playlist(String id, String name, String owner, String comment, String songCount, String pub, String created, String changed, Integer duration) { + this.id = id; + this.name = name; + this.owner = (owner == null) ? "" : owner; + this.comment = (comment == null) ? "" : comment; + this.songCount = (songCount == null) ? "" : songCount; + this.pub = (pub == null) ? null : (pub.equals("true")); + setCreated(created); + setChanged(changed); + this.duration = duration; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getSongCount() { + return this.songCount; + } + + public void setSongCount(String songCount) { + this.songCount = songCount; + } + + public Boolean getPublic() { + return this.pub; + } + public void setPublic(Boolean pub) { + this.pub = pub; + } + + public Date getCreated() { + return created; + } + + public void setCreated(String created) { + if (created != null) { + try { + this.created = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(created); + } catch (ParseException e) { + this.created = null; + } + } else { + this.created = null; + } + } + public void setCreated(Date created) { + this.created = created; + } + + public Date getChanged() { + return changed; + } + public void setChanged(String changed) { + if (changed != null) { + try { + this.changed = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(changed); + } catch (ParseException e) { + this.changed = null; + } + } else { + this.changed = null; + } + } + public void setChanged(Date changed) { + this.changed = changed; + } + + public Integer getDuration() { + return duration; + } + public void setDuration(Integer duration) { + this.duration = duration; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if(o == this) { + return true; + } else if(o == null) { + return false; + } else if(o instanceof String) { + return o.equals(this.id); + } else if(o.getClass() != getClass()) { + return false; + } + + Playlist playlist = (Playlist) o; + return playlist.id.equals(this.id); + } + + public static class PlaylistComparator implements Comparator { + @Override + public int compare(Playlist playlist1, Playlist playlist2) { + return playlist1.getName().compareToIgnoreCase(playlist2.getName()); + } + + public static List sort(List playlists) { + Collections.sort(playlists, new PlaylistComparator()); + return playlists; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java b/app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java new file mode 100644 index 0000000..eba6a10 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java @@ -0,0 +1,63 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class RemoteStatus { + + private Integer positionSeconds; + private Integer currentPlayingIndex; + private Float gain; + private boolean playing; + + public Integer getPositionSeconds() { + return positionSeconds; + } + + public void setPositionSeconds(Integer positionSeconds) { + this.positionSeconds = positionSeconds; + } + + public Integer getCurrentPlayingIndex() { + return currentPlayingIndex; + } + + public void setCurrentIndex(Integer currentPlayingIndex) { + this.currentPlayingIndex = currentPlayingIndex; + } + + public boolean isPlaying() { + return playing; + } + + public void setPlaying(boolean playing) { + this.playing = playing; + } + + public Float getGain() { + return gain; + } + + public void setGain(float gain) { + this.gain = gain; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java b/app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java new file mode 100644 index 0000000..57f1ec8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java @@ -0,0 +1,28 @@ +package github.nvllsvm.audinaut.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum RepeatMode { + OFF { + @Override + public RepeatMode next() { + return ALL; + } + }, + ALL { + @Override + public RepeatMode next() { + return SINGLE; + } + }, + SINGLE { + @Override + public RepeatMode next() { + return OFF; + } + }; + + public abstract RepeatMode next(); +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java new file mode 100644 index 0000000..631a7c5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java @@ -0,0 +1,93 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.util.regex.Pattern; + +/** + * The criteria for a music search. + * + * @author Sindre Mehus + */ +public class SearchCritera { + + private final String query; + private final int artistCount; + private final int albumCount; + private final int songCount; + private Pattern pattern; + + public SearchCritera(String query, int artistCount, int albumCount, int songCount) { + this.query = query; + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + } + + public String getQuery() { + return query; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } + + /** + * Returns and caches a pattern instance that can be used to check if a + * string matches the query. + */ + public Pattern getPattern() { + + // If the pattern wasn't already cached, create a new regular expression + // from the search string : + // * Surround the search string with ".*" (match anything) + // * Replace spaces and wildcard '*' characters with ".*" + // * All other characters are properly quoted + if (this.pattern == null) { + String regex = ".*"; + String currentPart = ""; + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + if (c == '*' || c == ' ') { + regex += Pattern.quote(currentPart); + regex += ".*"; + currentPart = ""; + } else { + currentPart += c; + } + } + if (currentPart.length() > 0) { + regex += Pattern.quote(currentPart); + } + + regex += ".*"; + this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + return this.pattern; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java new file mode 100644 index 0000000..bd15043 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; +import java.util.List; + +/** + * The result of a search. Contains matching artists, albums and songs. + * + * @author Sindre Mehus + */ +public class SearchResult implements Serializable { + + private final List artists; + private final List albums; + private final List songs; + + public SearchResult(List artists, List albums, List songs) { + this.artists = artists; + this.albums = albums; + this.songs = songs; + } + + public List getArtists() { + return artists; + } + + public List getAlbums() { + return albums; + } + + public List getSongs() { + return songs; + } + + public boolean hasArtists() { + return !artists.isEmpty(); + } + public boolean hasAlbums() { + return !albums.isEmpty(); + } + public boolean hasSongs() { + return !songs.isEmpty(); + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/User.java b/app/src/main/java/github/nvllsvm/audinaut/domain/User.java new file mode 100644 index 0000000..4a5e88b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/User.java @@ -0,0 +1,146 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.domain; + +import android.util.Pair; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class User implements Serializable { + public static final String ADMIN = "adminRole"; + public static final String SETTINGS = "settingsRole"; + public static final String DOWNLOAD = "downloadRole"; + public static final String UPLOAD = "uploadRole"; + public static final String COVERART = "coverArtRole"; + public static final String COMMENT = "commentRole"; + public static final String STREAM = "streamRole"; + public static final List ROLES = new ArrayList<>(); + + static { + ROLES.add(ADMIN); + ROLES.add(SETTINGS); + ROLES.add(STREAM); + ROLES.add(DOWNLOAD); + ROLES.add(UPLOAD); + ROLES.add(COVERART); + ROLES.add(COMMENT); + } + + private String username; + private String password; + private String email; + + private List settings = new ArrayList(); + private List musicFolders; + + public User() { + + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getSettings() { + return settings; + } + public void setSettings(List settings) { + this.settings.clear(); + this.settings.addAll(settings); + } + public void addSetting(String name, Boolean value) { + settings.add(new Setting(name, value)); + } + + public void addMusicFolder(MusicFolder musicFolder) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolder.getId(), musicFolder.getName(), false)); + } + public void addMusicFolder(MusicFolderSetting musicFolderSetting, boolean defaultValue) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolderSetting.getName(), musicFolderSetting.getLabel(), defaultValue)); + } + public List getMusicFolderSettings() { + return musicFolders; + } + + public static class Setting implements Serializable { + private String name; + private Boolean value; + + public Setting() { + + } + public Setting(String name, Boolean value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + public Boolean getValue() { + return value; + } + public void setValue(Boolean value) { + this.value = value; + } + } + + public static class MusicFolderSetting extends Setting { + private String label; + + public MusicFolderSetting() { + + } + public MusicFolderSetting(String name, String label, Boolean value) { + super(name, value); + this.label = label; + } + + public String getLabel() { + return label; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Version.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Version.java new file mode 100644 index 0000000..a069dfd --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Version.java @@ -0,0 +1,187 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; + +/** + * Represents the version number of the Subsonic Android app. + * + * @author Sindre Mehus + * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ + */ +public class Version implements Comparable, Serializable { + private int major; + private int minor; + private int beta; + private int bugfix; + + public Version() { + // For Kryo + } + + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public String getVersion() { + switch(major) { + case 1: + switch(minor) { + case 0: + return "3.8"; + case 1: + return "3.9"; + case 2: + return "4.0"; + case 3: + return "4.1"; + case 4: + return "4.2"; + case 5: + return "4.3.1"; + case 6: + return "4.5"; + case 7: + return "4.6"; + case 8: + return "4.7"; + case 9: + return "4.8"; + case 10: + return "4.9"; + case 11: + return "5.1"; + case 12: + return "5.2"; + case 13: + return "5.3"; + case 14: + return "6.0"; + } + } + return ""; + } + + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + @Override + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java new file mode 100644 index 0000000..1247575 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java @@ -0,0 +1,190 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.DownloadFileItemHelperCallback; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.adapter.DownloadFileAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFragment extends SelectRecyclerFragment implements SectionAdapter.OnItemClickedListener { + private long currentRevision; + private ScheduledExecutorService executorService; + + public DownloadFragment() { + serialize = false; + pullToRefresh = false; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + super.onCreateView(inflater, container, bundle); + + ItemTouchHelper touchHelper = new ItemTouchHelper(new DownloadFileItemHelperCallback(this, false)); + touchHelper.attachToRecyclerView(recyclerView); + + return rootView; + } + + @Override + public void onResume() { + super.onResume(); + + final Handler handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }; + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS); + } + + @Override + public void onPause() { + super.onPause(); + executorService.shutdown(); + } + + @Override + public int getOptionsMenu() { + return R.menu.downloading; + } + + @Override + public SectionAdapter getAdapter(List objs) { + return new DownloadFileAdapter(context, objs, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return new ArrayList(); + } + + List songList = new ArrayList(); + songList.addAll(downloadService.getBackgroundDownloads()); + currentRevision = downloadService.getDownloadListUpdateRevision(); + return songList; + } + + @Override + public int getTitleResource() { + return R.string.button_bar_downloading; + } + + @Override + public void onItemClicked(UpdateView updateView, DownloadFile item) { + + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, DownloadFile downloadFile) { + MusicDirectory.Entry selectedItem = downloadFile.getSong(); + onCreateContextMenuSupport(menu, menuInflater, updateView, selectedItem); + if(!Util.isOffline(context)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, DownloadFile downloadFile) { + MusicDirectory.Entry selectedItem = downloadFile.getSong(); + return onContextItemSelected(menuItem, selectedItem); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if(super.onOptionsItemSelected(menuItem)) { + return true; + } + + switch (menuItem.getItemId()) { + case R.id.menu_remove_all: + Util.confirmDialog(context, R.string.download_menu_remove_all, "", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().clearBackground(); + return null; + } + + @Override + protected void done(Void result) { + update(); + } + }.execute(); + } + }); + return true; + } + + return false; + } + + private void update() { + DownloadService downloadService = getDownloadService(); + if (downloadService == null || objects == null || adapter == null) { + return; + } + + if (currentRevision != downloadService.getDownloadListUpdateRevision()) { + List downloadFileList = downloadService.getBackgroundDownloads(); + objects.clear(); + objects.addAll(downloadFileList); + adapter.notifyDataSetChanged(); + + currentRevision = downloadService.getDownloadListUpdateRevision(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java new file mode 100644 index 0000000..c2c477c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java @@ -0,0 +1,459 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.fragments; + +import android.content.SharedPreferences; +import android.media.audiofx.BassBoost; +import android.media.audiofx.Equalizer; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.Map; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.audiofx.LoudnessEnhancerController; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +/** + * Created by Scott on 10/27/13. + */ +public class EqualizerFragment extends SubsonicFragment { + private static final String TAG = EqualizerFragment.class.getSimpleName(); + + private static final int MENU_GROUP_PRESET = 100; + + private final Map bars = new HashMap(); + private SeekBar bassBar; + private SeekBar loudnessBar; + private EqualizerController equalizerController; + private Equalizer equalizer; + private BassBoost bass; + private LoudnessEnhancerController loudnessEnhancer; + private short masterLevel = 0; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + rootView = inflater.inflate(R.layout.equalizer, container, false); + + try { + DownloadService service = DownloadService.getInstance(); + equalizerController = service.getEqualizerController(); + equalizer = equalizerController.getEqualizer(); + bass = equalizerController.getBassBoost(); + loudnessEnhancer = equalizerController.getLoudnessEnhancerController(); + + initEqualizer(); + } catch(Exception e) { + Log.e(TAG, "Failed to initialize EQ", e); + Util.toast(context, "Failed to initialize EQ"); + context.onBackPressed(); + } + + final View presetButton = rootView.findViewById(R.id.equalizer_preset); + registerForContextMenu(presetButton); + presetButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + presetButton.showContextMenu(); + } + }); + + CheckBox enabledCheckBox = (CheckBox) rootView.findViewById(R.id.equalizer_enabled); + enabledCheckBox.setChecked(equalizer.getEnabled()); + enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + try { + setEqualizerEnabled(b); + } catch(Exception e) { + Log.e(TAG, "Failed to set EQ enabled", e); + Util.toast(context, "Failed to set EQ enabled"); + context.onBackPressed(); + } + } + }); + + setTitle(R.string.equalizer_label); + setSubtitle(null); + + return rootView; + } + + @Override + public void onPause() { + super.onPause(); + + try { + equalizerController.saveSettings(); + + if (!equalizer.getEnabled()) { + equalizerController.release(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to release controller", e); + } + } + + @Override + public void onResume() { + super.onResume(); + equalizerController = DownloadService.getInstance().getEqualizerController(); + equalizer = equalizerController.getEqualizer(); + bass = equalizerController.getBassBoost(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, view, menuInfo); + if(!primaryFragment) { + return; + } + + short currentPreset; + try { + currentPreset = equalizer.getCurrentPreset(); + } catch (Exception x) { + currentPreset = -1; + } + + for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) { + MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset)); + if (preset == currentPreset) { + menuItem.setChecked(true); + } + } + menu.setGroupCheckable(MENU_GROUP_PRESET, true, true); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem) { + short preset = (short) menuItem.getItemId(); + for(int i = 0; i < 10; i++) { + try { + equalizer.usePreset(preset); + i = 10; + } catch (UnsupportedOperationException e) { + equalizerController.release(); + equalizer = equalizerController.getEqualizer(); + bass = equalizerController.getBassBoost(); + loudnessEnhancer = equalizerController.getLoudnessEnhancerController(); + } + } + updateBars(false); + return true; + } + + private void setEqualizerEnabled(boolean enabled) { + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_EQUALIZER_ON, enabled); + editor.commit(); + for(int i = 0; i < 10; i++) { + try { + equalizer.setEnabled(enabled); + updateBars(true); + i = 10; + } catch (UnsupportedOperationException e) { + equalizerController.release(); + equalizer = equalizerController.getEqualizer(); + bass = equalizerController.getBassBoost(); + loudnessEnhancer = equalizerController.getLoudnessEnhancerController(); + } + } + } + + private void updateBars(boolean changedEnabled) { + try { + boolean isEnabled = equalizer.getEnabled(); + short minEQLevel = equalizer.getBandLevelRange()[0]; + short maxEQLevel = equalizer.getBandLevelRange()[1]; + for (Map.Entry entry : bars.entrySet()) { + short band = entry.getKey(); + SeekBar bar = entry.getValue(); + bar.setEnabled(isEnabled); + if (band >= (short) 0) { + short setLevel; + if (changedEnabled) { + setLevel = (short) (equalizer.getBandLevel(band) - masterLevel); + if (isEnabled) { + bar.setProgress(equalizer.getBandLevel(band) - minEQLevel); + } else { + bar.setProgress(-minEQLevel); + } + } else { + bar.setProgress(equalizer.getBandLevel(band) - minEQLevel); + setLevel = (short) (equalizer.getBandLevel(band) + masterLevel); + } + if (setLevel < minEQLevel) { + setLevel = minEQLevel; + } else if (setLevel > maxEQLevel) { + setLevel = maxEQLevel; + } + equalizer.setBandLevel(band, setLevel); + } else if (!isEnabled) { + bar.setProgress(-minEQLevel); + } + } + + bassBar.setEnabled(isEnabled); + if (loudnessBar != null) { + loudnessBar.setEnabled(isEnabled); + } + if (changedEnabled && !isEnabled) { + bass.setStrength((short) 0); + bassBar.setProgress(0); + if (loudnessBar != null) { + loudnessEnhancer.setGain(0); + loudnessBar.setProgress(0); + } + } + + if (!isEnabled) { + masterLevel = 0; + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel); + editor.commit(); + } + } catch(Exception e) { + Log.e(TAG, "Failed to update bars"); + } + } + + private void initEqualizer() { + LinearLayout layout = (LinearLayout) rootView.findViewById(R.id.equalizer_layout); + + final short minEQLevel = equalizer.getBandLevelRange()[0]; + final short maxEQLevel = equalizer.getBandLevelRange()[1]; + + // Setup Pregain + SharedPreferences prefs = Util.getPreferences(context); + masterLevel = (short)prefs.getInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, 0); + initPregain(layout, minEQLevel, maxEQLevel); + + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + final short band = i; + + View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null); + TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz"); + + bars.put(band, bar); + bar.setMax(maxEQLevel - minEQLevel); + short level = equalizer.getBandLevel(band); + if(equalizer.getEnabled()) { + level = (short) (level - masterLevel); + } + bar.setProgress(level - minEQLevel); + bar.setEnabled(equalizer.getEnabled()); + updateLevelText(levelTextView, level); + + bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + try { + short level = (short) (progress + minEQLevel); + if (fromUser) { + equalizer.setBandLevel(band, (short) (level + masterLevel)); + } + updateLevelText(levelTextView, level); + } catch(Exception e) { + Log.e(TAG, "Failed to change equalizer", e); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + layout.addView(bandBar); + } + + LinearLayout specialLayout = (LinearLayout) rootView.findViewById(R.id.special_effects_layout); + + // Setup bass booster + View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null); + TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView bassTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + bassBar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText(R.string.equalizer_bass_booster); + bassBar.setEnabled(equalizer.getEnabled()); + short bassLevel = 0; + if(bass.getEnabled()) { + bassLevel = bass.getRoundedStrength(); + } + bassTextView.setText(context.getResources().getString(R.string.equalizer_bass_size, bassLevel)); + bassBar.setMax(1000); + bassBar.setProgress(bassLevel); + bassBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + try { + bassTextView.setText(context.getResources().getString(R.string.equalizer_bass_size, progress)); + if (fromUser) { + if (progress > 0) { + if (!bass.getEnabled()) { + bass.setEnabled(true); + } + bass.setStrength((short) progress); + } else if (progress == 0 && bass.getEnabled()) { + bass.setStrength((short) progress); + bass.setEnabled(false); + } + } + } catch(Exception e) { + Log.w(TAG, "Error on changing bass: ", e); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + specialLayout.addView(bandBar); + + if(loudnessEnhancer != null && loudnessEnhancer.isAvailable()) { + // Setup loudness enhancer + bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null); + freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView loudnessTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + loudnessBar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText(R.string.equalizer_voice_booster); + loudnessBar.setEnabled(equalizer.getEnabled()); + int loudnessLevel = 0; + if(loudnessEnhancer.isEnabled()) { + loudnessLevel = (int) loudnessEnhancer.getGain(); + } + loudnessBar.setProgress(loudnessLevel / 100); + loudnessTextView.setText(context.getResources().getString(R.string.equalizer_db_size, loudnessLevel / 100)); + loudnessBar.setMax(15); + loudnessBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + try { + loudnessTextView.setText(context.getResources().getString(R.string.equalizer_db_size, progress)); + if(fromUser) { + if(progress > 0) { + if(!loudnessEnhancer.isEnabled()) { + loudnessEnhancer.enable(); + } + loudnessEnhancer.setGain(progress * 100); + } else if(progress == 0 && loudnessEnhancer.isEnabled()) { + loudnessEnhancer.setGain(progress * 100); + loudnessEnhancer.disable(); + } + } + } catch(Exception e) { + Log.w(TAG, "Error on changing loudness: ", e); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + specialLayout.addView(bandBar); + } + } + + private void initPregain(LinearLayout layout, final short minEQLevel, final short maxEQLevel) { + View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null); + TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText("Master"); + + bars.put((short)-1, bar); + bar.setMax(maxEQLevel - minEQLevel); + bar.setProgress(masterLevel - minEQLevel); + bar.setEnabled(equalizer.getEnabled()); + updateLevelText(levelTextView, masterLevel); + + bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + try { + masterLevel = (short) (progress + minEQLevel); + if (fromUser) { + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel); + editor.commit(); + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + short level = (short) ((bars.get(i).getProgress() + minEQLevel) + masterLevel); + equalizer.setBandLevel(i, level); + } + } + updateLevelText(levelTextView, masterLevel); + } catch(Exception e) { + Log.e(TAG, "Failed to change equalizer", e); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + layout.addView(bandBar); + } + + private void updateLevelText(TextView levelTextView, short level) { + levelTextView.setText((level > 0 ? "+" : "") + context.getResources().getString(R.string.equalizer_db_size, level / 100)); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java new file mode 100644 index 0000000..e8d1325 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java @@ -0,0 +1,357 @@ +package github.nvllsvm.audinaut.fragments; + +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.StatFs; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.MainAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.EnvironmentVariables; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; + +public class MainFragment extends SelectRecyclerFragment { + private static final String TAG = MainFragment.class.getSimpleName(); + public static final String SONGS_LIST_PREFIX = "songs-"; + public static final String SONGS_NEWEST = SONGS_LIST_PREFIX + "newest"; + public static final String SONGS_TOP_PLAYED = SONGS_LIST_PREFIX + "topPlayed"; + public static final String SONGS_RECENT = SONGS_LIST_PREFIX + "recent"; + public static final String SONGS_FREQUENT = SONGS_LIST_PREFIX + "frequent"; + + public MainFragment() { + super(); + pullToRefresh = false; + serialize = false; + backgroundUpdate = false; + alwaysFullscreen = true; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + menuInflater.inflate(R.menu.main, menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(super.onOptionsItemSelected(item)) { + return true; + } + + return false; + } + + @Override + public int getOptionsMenu() { + return 0; + } + + @Override + public SectionAdapter getAdapter(List objs) { + List> sections = new ArrayList<>(); + List headers = new ArrayList<>(); + + List albums = new ArrayList<>(); + albums.add(R.string.main_albums_random); + albums.add(R.string.main_albums_alphabetical); + albums.add(R.string.main_albums_genres); + albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_recent); + albums.add(R.string.main_albums_frequent); + + sections.add(albums); + headers.add("albums"); + + return new MainAdapter(context, headers, sections, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + return Arrays.asList(0); + } + + @Override + public int getTitleResource() { + return R.string.common_appname; + } + + private void showAlbumList(String type) { + if("genres".equals(type)) { + SubsonicFragment fragment = new SelectGenreFragment(); + replaceFragment(fragment); + } else if("years".equals(type)) { + SubsonicFragment fragment = new SelectYearFragment(); + replaceFragment(fragment); + } else { + // Clear out recently added count when viewing + if("newest".equals(type)) { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0); + editor.commit(); + } + + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0); + fragment.setArguments(args); + + replaceFragment(fragment); + } + } + + private void showAboutDialog() { + new LoadingTask(context) { + Long[] used; + long bytesTotalFs; + long bytesAvailableFs; + + @Override + protected Void doInBackground() throws Throwable { + File rootFolder = FileUtil.getMusicDirectory(context); + StatFs stat = new StatFs(rootFolder.getPath()); + bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + + used = FileUtil.getUsedSize(context, rootFolder); + return null; + } + + @Override + protected void done(Void result) { + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + headers.add(R.string.details_author); + details.add("Andrew Rabert"); + + headers.add(R.string.details_email); + details.add("ar@nullsum.net"); + + try { + headers.add(R.string.details_version); + details.add(context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName); + } catch(Exception e) { + details.add(""); + } + + Resources res = context.getResources(); + headers.add(R.string.details_files_cached); + details.add(Long.toString(used[0])); + + headers.add(R.string.details_files_permanent); + details.add(Long.toString(used[1])); + + headers.add(R.string.details_used_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(used[2], context), Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context))); + + headers.add(R.string.details_available_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(bytesAvailableFs, context), Util.formatLocalizedBytes(bytesTotalFs, context))); + + Util.showDetailsDialog(context, R.string.main_about_title, headers, details); + } + }.execute(); + } + + private void rescanServer() { + new LoadingTask(context, false) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.startRescan(context, this); + return null; + } + + @Override + protected void done(Void value) { + Util.toast(context, R.string.main_scan_complete); + } + }.execute(); + } + + private void getLogs() { + try { + final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + new LoadingTask(context) { + @Override + protected String doInBackground() throws Throwable { + updateProgress("Gathering Logs"); + File logcat = new File(Environment.getExternalStorageDirectory(), "audinaut-logcat.txt"); + Util.delete(logcat); + Process logcatProc = null; + + try { + List progs = new ArrayList(); + progs.add("logcat"); + progs.add("-v"); + progs.add("time"); + progs.add("-d"); + progs.add("-f"); + progs.add(logcat.getCanonicalPath()); + progs.add("*:I"); + + logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()])); + logcatProc.waitFor(); + } finally { + if(logcatProc != null) { + logcatProc.destroy(); + } + } + + URL url = new URL("https://pastebin.com/api/api_post.php"); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + StringBuffer responseBuffer = new StringBuffer(); + try { + urlConnection.setReadTimeout(10000); + urlConnection.setConnectTimeout(15000); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + + OutputStream os = urlConnection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, Constants.UTF_8)); + writer.write("api_dev_key=" + URLEncoder.encode(EnvironmentVariables.PASTEBIN_DEV_KEY, Constants.UTF_8) + "&api_option=paste&api_paste_private=1&api_paste_code="); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logcat))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + + File stacktrace = new File(Environment.getExternalStorageDirectory(), "audinaut-stacktrace.txt"); + if(stacktrace.exists() && stacktrace.isFile()) { + writer.write("\n\nMost Recent Stacktrace:\n\n"); + + reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(stacktrace))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + } + + writer.flush(); + writer.close(); + os.close(); + + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseBuffer.append(inputLine); + } + in.close(); + } finally { + urlConnection.disconnect(); + } + + String response = responseBuffer.toString(); + if(response.indexOf("http") == 0) { + return response.replace("http:", "https:"); + } else { + throw new Exception("Pastebin Error: " + response); + } + } + + @Override + protected void error(Throwable error) { + Log.e(TAG, "Failed to gather logs", error); + Util.toast(context, "Failed to gather logs"); + } + + @Override + protected void done(String logcat) { + String footer = "Android SDK: " + Build.VERSION.SDK; + footer += "\nDevice Model: " + Build.MODEL; + footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT; + footer += "\nROM: " + Build.DISPLAY; + footer += "\nLogs: " + logcat; + footer += "\nBuild Number: " + packageInfo.versionCode; + + Intent email = new Intent(Intent.ACTION_SENDTO, + Uri.fromParts("mailto", "ar@nullsum.net", null)); + email.putExtra(Intent.EXTRA_SUBJECT, "Audinaut " + packageInfo.versionName + " Error Logs"); + email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer); + startActivity(email); + } + }.execute(); + } catch(Exception e) {} + } + + @Override + public void onItemClicked(UpdateView updateView, Integer item) { + if (item == R.string.main_albums_newest) { + showAlbumList("newest"); + } else if (item == R.string.main_albums_random) { + showAlbumList("random"); + } else if (item == R.string.main_albums_recent) { + showAlbumList("recent"); + } else if (item == R.string.main_albums_frequent) { + showAlbumList("frequent"); + } else if(item == R.string.main_albums_genres) { + showAlbumList("genres"); + } else if(item == R.string.main_albums_year) { + showAlbumList("years"); + } else if(item == R.string.main_albums_alphabetical) { + showAlbumList("alphabeticalByName"); + } else if (item == R.string.main_songs_newest) { + showAlbumList(SONGS_NEWEST); + } else if (item == R.string.main_songs_top_played) { + showAlbumList(SONGS_TOP_PLAYED); + } else if (item == R.string.main_songs_recent) { + showAlbumList(SONGS_RECENT); + } else if (item == R.string.main_songs_frequent) { + showAlbumList(SONGS_FREQUENT); + } + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Integer item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Integer item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java new file mode 100644 index 0000000..f7c40f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java @@ -0,0 +1,1109 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.fragments; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import android.annotation.TargetApi; +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.Log; +import android.view.Display; +import android.view.GestureDetector; +import android.view.GestureDetector.OnGestureListener; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnimationUtils; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.ViewFlipper; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadService.OnSongChangedListener; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.adapter.DownloadFileAdapter; +import github.nvllsvm.audinaut.view.FadeOutAnimation; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import static github.nvllsvm.audinaut.domain.PlayerState.*; +import github.nvllsvm.audinaut.util.*; +import github.nvllsvm.audinaut.view.AutoRepeatButton; +import java.util.ArrayList; +import java.util.concurrent.ScheduledFuture; + +public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener, OnSongChangedListener { + private static final String TAG = NowPlayingFragment.class.getSimpleName(); + private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; + + private static final int ACTION_PREVIOUS = 1; + private static final int ACTION_NEXT = 2; + private static final int ACTION_REWIND = 3; + private static final int ACTION_FORWARD = 4; + + private ViewFlipper playlistFlipper; + private TextView emptyTextView; + private TextView songTitleTextView; + private ImageView albumArtImageView; + private RecyclerView playlistView; + private TextView positionTextView; + private TextView durationTextView; + private TextView statusTextView; + private SeekBar progressBar; + private AutoRepeatButton previousButton; + private AutoRepeatButton nextButton; + private AutoRepeatButton rewindButton; + private AutoRepeatButton fastforwardButton; + private View pauseButton; + private View stopButton; + private View startButton; + private ImageButton repeatButton; + private View toggleListButton; + + private ScheduledExecutorService executorService; + private DownloadFile currentPlaying; + private int swipeDistance; + private int swipeVelocity; + private ScheduledFuture hideControlsFuture; + private List songList; + private DownloadFileAdapter songListAdapter; + private boolean seekInProgress = false; + private boolean startFlipped = false; + private boolean scrollWhenLoaded = false; + private int lastY = 0; + private int currentPlayingSize = 0; + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if(savedInstanceState != null) { + if(savedInstanceState.getInt(Constants.FRAGMENT_DOWNLOAD_FLIPPER) == 1) { + startFlipped = true; + } + } + primaryFragment = false; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(Constants.FRAGMENT_DOWNLOAD_FLIPPER, playlistFlipper.getDisplayedChild()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + rootView = inflater.inflate(R.layout.download, container, false); + setTitle(R.string.button_bar_now_playing); + + WindowManager w = context.getWindowManager(); + Display d = w.getDefaultDisplay(); + swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; + swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; + gestureScanner = new GestureDetector(this); + + playlistFlipper = (ViewFlipper)rootView.findViewById(R.id.download_playlist_flipper); + emptyTextView = (TextView)rootView.findViewById(R.id.download_empty); + songTitleTextView = (TextView)rootView.findViewById(R.id.download_song_title); + albumArtImageView = (ImageView)rootView.findViewById(R.id.download_album_art_image); + positionTextView = (TextView)rootView.findViewById(R.id.download_position); + durationTextView = (TextView)rootView.findViewById(R.id.download_duration); + statusTextView = (TextView)rootView.findViewById(R.id.download_status); + progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar); + previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous); + nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next); + rewindButton = (AutoRepeatButton) rootView.findViewById(R.id.download_rewind); + fastforwardButton = (AutoRepeatButton) rootView.findViewById(R.id.download_fastforward); + pauseButton =rootView.findViewById(R.id.download_pause); + stopButton =rootView.findViewById(R.id.download_stop); + startButton =rootView.findViewById(R.id.download_start); + repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat); + toggleListButton =rootView.findViewById(R.id.download_toggle_list); + + playlistView = (RecyclerView)rootView.findViewById(R.id.download_list); + FastScroller fastScroller = (FastScroller) rootView.findViewById(R.id.download_fast_scroller); + fastScroller.attachRecyclerView(playlistView); + setupLayoutManager(playlistView, false); + ItemTouchHelper touchHelper = new ItemTouchHelper(new DownloadFileItemHelperCallback(this, true)); + touchHelper.attachToRecyclerView(playlistView); + + View.OnTouchListener touchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent me) { + return gestureScanner.onTouchEvent(me); + } + }; + pauseButton.setOnTouchListener(touchListener); + stopButton.setOnTouchListener(touchListener); + startButton.setOnTouchListener(touchListener); + emptyTextView.setOnTouchListener(touchListener); + albumArtImageView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent me) { + if (me.getAction() == MotionEvent.ACTION_DOWN) { + lastY = (int) me.getRawY(); + } + return gestureScanner.onTouchEvent(me); + } + }); + + previousButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().previous(); + return null; + } + }.execute(); + setControlsVisible(true); + } + }); + previousButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getDownloadService().next(); + return true; + } + }.execute(); + setControlsVisible(true); + } + }); + nextButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); + } + }); + + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(true); + } + }); + rewindButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(false); + } + }); + fastforwardButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); + } + }); + + + pauseButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().pause(); + return null; + } + }.execute(); + } + }); + + stopButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().reset(); + return null; + } + }.execute(); + } + }); + + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + start(); + return null; + } + }.execute(); + } + }); + + repeatButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + RepeatMode repeatMode = getDownloadService().getRepeatMode().next(); + getDownloadService().setRepeatMode(repeatMode); + switch (repeatMode) { + case OFF: + Util.toast(context, R.string.download_repeat_off); + break; + case ALL: + Util.toast(context, R.string.download_repeat_all); + break; + case SINGLE: + Util.toast(context, R.string.download_repeat_single); + break; + default: + break; + } + updateRepeatButton(); + setControlsVisible(true); + } + }); + + toggleListButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + toggleFullscreenAlbumArt(); + setControlsVisible(true); + } + }); + + View overlay = rootView.findViewById(R.id.download_overlay_buttons); + final int overlayHeight = overlay != null ? overlay.getHeight() : -1; + albumArtImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (overlayHeight == -1 || lastY < (view.getBottom() - overlayHeight)) { + toggleFullscreenAlbumArt(); + setControlsVisible(true); + } + } + }); + + progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onStopTrackingTouch(final SeekBar seekBar) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().seekTo(progressBar.getProgress()); + return null; + } + + @Override + protected void done(Void result) { + seekInProgress = false; + } + }.execute(); + } + + @Override + public void onStartTrackingTouch(final SeekBar seekBar) { + seekInProgress = true; + } + + @Override + public void onProgressChanged(final SeekBar seekBar, final int position, final boolean fromUser) { + if (fromUser) { + positionTextView.setText(Util.formatDuration(position / 1000)); + setControlsVisible(true); + } + } + }); + + return rootView; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + DownloadService downloadService = getDownloadService(); + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.nowplaying, menu); + } + if(downloadService != null && downloadService.isRemovePlayed()) { + menu.findItem(R.id.menu_remove_played).setChecked(true); + } + + boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable(); + if(equalizerAvailable) { + SharedPreferences prefs = Util.getPreferences(context); + boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false); + if (equalizerOn && downloadService != null) { + if(downloadService.getEqualizerController() != null && downloadService.getEqualizerController().isEnabled()) { + menu.findItem(R.id.menu_equalizer).setChecked(true); + } + } + } else { + menu.removeItem(R.id.menu_equalizer); + } + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) { + menu.findItem(R.id.menu_batch_mode).setChecked(true); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if(menuItemSelected(menuItem.getItemId(), null)) { + return true; + } + + return super.onOptionsItemSelected(menuItem); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, DownloadFile downloadFile) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.nowplaying_context_offline, menu); + } else { + menuInflater.inflate(R.menu.nowplaying_context, menu); + } + + if (downloadFile.getSong().getParent() == null) { + menu.findItem(R.id.menu_show_album).setVisible(false); + menu.findItem(R.id.menu_show_artist).setVisible(false); + } + + MenuUtil.hideMenuItems(context, menu, updateView); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, DownloadFile downloadFile) { + if(onContextItemSelected(menuItem, downloadFile.getSong())) { + return true; + } + + return menuItemSelected(menuItem.getItemId(), downloadFile); + } + + private boolean menuItemSelected(int menuItemId, final DownloadFile song) { + List songs; + switch (menuItemId) { + case R.id.menu_show_album: case R.id.menu_show_artist: + Entry entry = song.getSong(); + + Intent intent = new Intent(context, SubsonicFragmentActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true); + String albumId; + String albumName; + if(menuItemId == R.id.menu_show_album) { + if(Util.isTagBrowsing(context)) { + albumId = entry.getAlbumId(); + } else { + albumId = entry.getParent(); + } + albumName = entry.getAlbum(); + } else { + if(Util.isTagBrowsing(context)) { + albumId = entry.getArtistId(); + } else { + albumId = entry.getGrandParent(); + if(albumId == null) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getParent()); + } + } + albumName = entry.getArtist(); + intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true); + } + intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, albumId); + intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, albumName); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Artist"); + + if(Util.isOffline(context)) { + try { + // This should only be successful if this is a online song in offline mode + Integer.parseInt(entry.getParent()); + String root = FileUtil.getMusicDirectory(context).getPath(); + String id = root + "/" + entry.getPath(); + id = id.substring(0, id.lastIndexOf("/")); + if(menuItemId == R.id.menu_show_album) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id); + } + id = id.substring(0, id.lastIndexOf("/")); + if(menuItemId != R.id.menu_show_album) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id); + intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); + intent.removeExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID); + } + } catch(Exception e) { + // Do nothing, entry.getParent() is fine + } + } + + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(context, intent); + return true; + case R.id.menu_remove_all: + Util.confirmDialog(context, R.string.download_menu_remove_all, "", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().setShufflePlayEnabled(false); + getDownloadService().clear(); + return null; + } + + @Override + protected void done(Void result) { + context.closeNowPlaying(); + } + }.execute(); + } + }); + return true; + case R.id.menu_remove_played: + if (getDownloadService().isRemovePlayed()) { + getDownloadService().setRemovePlayed(false); + } else { + getDownloadService().setRemovePlayed(true); + } + context.supportInvalidateOptionsMenu(); + return true; + case R.id.menu_shuffle: + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().shuffle(); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, R.string.download_menu_shuffle_notification); + } + }.execute(); + return true; + case R.id.menu_save_playlist: + List entries = new LinkedList(); + for (DownloadFile downloadFile : getDownloadService().getSongs()) { + entries.add(downloadFile.getSong()); + } + createNewPlaylist(entries, true); + return true; + case R.id.menu_info: + displaySongInfo(song.getSong()); + return true; + case R.id.menu_equalizer: { + DownloadService downloadService = getDownloadService(); + if (downloadService != null) { + EqualizerController controller = downloadService.getEqualizerController(); + if(controller != null) { + SubsonicFragment fragment = new EqualizerFragment(); + replaceFragment(fragment); + setControlsVisible(true); + + return true; + } + } + + // Any failed condition will get here + Util.toast(context, "Failed to start equalizer. Try restarting."); + return true; + }case R.id.menu_batch_mode: + if(Util.isBatchMode(context)) { + Util.setBatchMode(context, false); + songListAdapter.notifyDataSetChanged(); + } else { + Util.setBatchMode(context, true); + songListAdapter.notifyDataSetChanged(); + } + context.supportInvalidateOptionsMenu(); + + return true; + default: + return false; + } + } + + @Override + public void onResume() { + super.onResume(); + if(this.primaryFragment) { + onResumeHandlers(); + } else { + update(); + } + } + private void onResumeHandlers() { + executorService = Executors.newSingleThreadScheduledExecutor(); + setControlsVisible(true); + + final DownloadService downloadService = getDownloadService(); + if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) { + playlistFlipper.setDisplayedChild(1); + startFlipped = false; + } + + updateButtons(); + + if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) { + getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); + } + + context.runWhenServiceAvailable(new Runnable() { + @Override + public void run() { + if (primaryFragment) { + DownloadService downloadService = getDownloadService(); + downloadService.addOnSongChangedListener(NowPlayingFragment.this, true); + } + updateRepeatButton(); + updateTitle(); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + onPauseHandlers(); + } + private void onPauseHandlers() { + if(executorService != null) { + DownloadService downloadService = getDownloadService(); + if (downloadService != null) { + downloadService.removeOnSongChangeListener(this); + } + playlistFlipper.setDisplayedChild(0); + } + } + + @Override + public void setPrimaryFragment(boolean primary) { + super.setPrimaryFragment(primary); + if(rootView != null) { + if(primary) { + onResumeHandlers(); + } else { + onPauseHandlers(); + } + } + } + + @Override + public void setTitle(int title) { + this.title = context.getResources().getString(title); + if(this.primaryFragment) { + context.setTitle(this.title); + } + } + @Override + public void setSubtitle(CharSequence title) { + this.subtitle = title; + if(this.primaryFragment) { + context.setSubtitle(title); + } + } + + @Override + public SectionAdapter getCurrentAdapter() { + return songListAdapter; + } + + private void scheduleHideControls() { + if (hideControlsFuture != null) { + hideControlsFuture.cancel(false); + } + + final Handler handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + setControlsVisible(false); + } + }); + } + }; + hideControlsFuture = executorService.schedule(runnable, 3000L, TimeUnit.MILLISECONDS); + } + + private void setControlsVisible(boolean visible) { + DownloadService downloadService = getDownloadService(); + try { + long duration = 1700L; + FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration); + + if (visible) { + scheduleHideControls(); + } + } catch(Exception e) { + + } + } + + private void updateButtons() { + if(context == null) { + return; + } + } + + // Scroll to current playing/downloading. + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void scrollToCurrent() { + if (getDownloadService() == null || songListAdapter == null) { + scrollWhenLoaded = true; + return; + } + + // Try to get position of current playing/downloading + int position = songListAdapter.getItemPosition(currentPlaying); + if(position == -1) { + DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); + position = songListAdapter.getItemPosition(currentDownloading); + } + + // If found, scroll to it + if(position != -1) { + // RecyclerView.scrollToPosition just puts it on the screen (ie: bottom if scrolled below it) + LinearLayoutManager layoutManager = (LinearLayoutManager) playlistView.getLayoutManager(); + layoutManager.scrollToPositionWithOffset(position, 0); + } + } + + private void update() { + if(startFlipped) { + startFlipped = false; + scrollToCurrent(); + } + } + + private int getMinutes(int progress) { + if(progress < 30) { + return progress + 1; + } else if(progress < 49) { + return (progress - 30) * 5 + getMinutes(29); + } else if(progress < 57) { + return (progress - 48) * 30 + getMinutes(48); + } else if(progress < 81) { + return (progress - 56) * 60 + getMinutes(56); + } else { + return (progress - 80) * 150 + getMinutes(80); + } + } + + private void toggleFullscreenAlbumArt() { + if (playlistFlipper.getDisplayedChild() == 1) { + playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_in)); + playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_out)); + playlistFlipper.setDisplayedChild(0); + } else { + scrollToCurrent(); + playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_in)); + playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_out)); + playlistFlipper.setDisplayedChild(1); + + UpdateView.triggerUpdate(); + } + } + + private void start() { + DownloadService service = getDownloadService(); + PlayerState state = service.getPlayerState(); + if (state == PAUSED || state == COMPLETED || state == STOPPED) { + service.start(); + } else if (state == STOPPED || state == IDLE) { + warnIfStorageUnavailable(); + int current = service.getCurrentPlayingIndex(); + // TODO: Use play() method. + if (current == -1) { + service.play(0); + } else { + service.play(current); + } + } + } + + private void changeProgress(final boolean rewind) { + final DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + new SilentBackgroundTask(context) { + int seekTo; + + @Override + protected Void doInBackground() throws Throwable { + if(rewind) { + seekTo = downloadService.rewind(); + } else { + seekTo = downloadService.fastForward(); + } + return null; + } + + @Override + protected void done(Void result) { + progressBar.setProgress(seekTo); + } + }.execute(); + } + + @Override + public boolean onDown(MotionEvent me) { + setControlsVisible(true); + return false; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + final DownloadService downloadService = getDownloadService(); + if (downloadService == null || e1 == null || e2 == null) { + return false; + } + + // Right to Left swipe + int action = 0; + if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { + action = ACTION_NEXT; + } + // Left to Right swipe + else if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { + action = ACTION_PREVIOUS; + } + // Top to Bottom swipe + else if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + action = ACTION_FORWARD; + } + // Bottom to Top swipe + else if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + action = ACTION_REWIND; + } + + if(action > 0) { + final int performAction = action; + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + switch(performAction) { + case ACTION_NEXT: + downloadService.next(); + break; + case ACTION_PREVIOUS: + downloadService.previous(); + break; + case ACTION_FORWARD: + downloadService.seekTo(downloadService.getPlayerPosition() + DownloadService.FAST_FORWARD); + break; + case ACTION_REWIND: + downloadService.seekTo(downloadService.getPlayerPosition() - DownloadService.REWIND); + break; + } + return null; + } + }.execute(); + + return true; + } else { + return false; + } + } + + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + @Override + public void onItemClicked(UpdateView updateView, final DownloadFile item) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().play(item); + return null; + } + }.execute(); + } + + @Override + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + + if (currentPlaying != null && !currentPlaying.isSong()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + updateTitle(); + } + + private void setupSubtitle(int currentPlayingIndex) { + if (currentPlaying != null) { + Entry song = currentPlaying.getSong(); + songTitleTextView.setText(song.getTitle()); + getImageLoader().loadImage(albumArtImageView, song, true, true); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle)); + } else { + setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + } + } else { + songTitleTextView.setText(null); + getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); + setSubtitle(null); + } + } + + @Override + public void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex) { + currentPlayingSize = songs.size(); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + emptyTextView.setText(R.string.download_shuffle_loading); + } + else { + emptyTextView.setText(R.string.download_empty); + } + + if(songListAdapter == null) { + songList = new ArrayList<>(); + songList.addAll(songs); + playlistView.setAdapter(songListAdapter = new DownloadFileAdapter(context, songList, NowPlayingFragment.this)); + } else { + songList.clear(); + songList.addAll(songs); + songListAdapter.notifyDataSetChanged(); + } + + emptyTextView.setVisibility(songs.isEmpty() ? View.VISIBLE : View.GONE); + + if(scrollWhenLoaded) { + scrollToCurrent(); + scrollWhenLoaded = false; + } + + if(this.currentPlaying != currentPlaying) { + onSongChanged(currentPlaying, currentPlayingIndex); + onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + } else { + setupSubtitle(currentPlayingIndex); + } + + toggleListButton.setVisibility(View.VISIBLE); + repeatButton.setVisibility(View.VISIBLE); + } + + @Override + public void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable) { + if (currentPlaying != null) { + int millisTotal = duration == null ? 0 : duration; + + positionTextView.setText(Util.formatDuration(millisPlayed / 1000)); + if(millisTotal > 0) { + durationTextView.setText(Util.formatDuration(millisTotal / 1000)); + } else { + durationTextView.setText("-:--"); + } + progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug. + if(!seekInProgress) { + progressBar.setProgress(millisPlayed); + } + progressBar.setEnabled(isSeekable); + } else { + positionTextView.setText("0:00"); + durationTextView.setText("-:--"); + progressBar.setProgress(0); + progressBar.setEnabled(false); + } + } + + @Override + public void onStateUpdate(DownloadFile downloadFile, PlayerState playerState) { + switch (playerState) { + case DOWNLOADING: + if(currentPlaying != null) { + if(Util.isWifiRequiredForDownload(context)) { + statusTextView.setText(context.getResources().getString(R.string.download_playerstate_mobile_disabled)); + } else { + long bytes = currentPlaying.getPartialFile().length(); + statusTextView.setText(context.getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, context))); + } + } + break; + case PREPARING: + statusTextView.setText(R.string.download_playerstate_buffering); + break; + default: + if(currentPlaying != null) { + Entry entry = currentPlaying.getSong(); + if(entry.getAlbum() != null) { + String artist = ""; + if (entry.getArtist() != null) { + artist = currentPlaying.getSong().getArtist() + " - "; + } + statusTextView.setText(artist + entry.getAlbum()); + } else { + statusTextView.setText(null); + } + } else { + statusTextView.setText(null); + } + break; + } + + switch (playerState) { + case STARTED: + pauseButton.setVisibility(View.VISIBLE); + stopButton.setVisibility(View.INVISIBLE); + startButton.setVisibility(View.INVISIBLE); + break; + case DOWNLOADING: + case PREPARING: + pauseButton.setVisibility(View.INVISIBLE); + stopButton.setVisibility(View.VISIBLE); + startButton.setVisibility(View.INVISIBLE); + break; + default: + pauseButton.setVisibility(View.INVISIBLE); + stopButton.setVisibility(View.INVISIBLE); + startButton.setVisibility(View.VISIBLE); + break; + } + } + + @Override + public void onMetadataUpdate(Entry song, int fieldChange) { + if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + getImageLoader().loadImage(albumArtImageView, song, true, true); + } + } + + public void updateRepeatButton() { + DownloadService downloadService = getDownloadService(); + switch (downloadService.getRepeatMode()) { + case OFF: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_off)); + break; + case ALL: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_all)); + break; + case SINGLE: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_single)); + break; + default: + break; + } + } + private void updateTitle() { + DownloadService downloadService = getDownloadService(); + + String title = context.getResources().getString(R.string.button_bar_now_playing); + + setTitle(title); + } + + @Override + protected List getSelectedEntries() { + List selected = getCurrentAdapter().getSelected(); + List entries = new ArrayList<>(); + + for(DownloadFile downloadFile: selected) { + if(downloadFile.getSong() != null) { + entries.add(downloadFile.getSong()); + } + } + + return entries; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java new file mode 100644 index 0000000..f7084b8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java @@ -0,0 +1,334 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.Constants; + +public abstract class PreferenceCompatFragment extends SubsonicFragment { + private static final String TAG = PreferenceCompatFragment.class.getSimpleName(); + private static final int FIRST_REQUEST_CODE = 100; + private static final int MSG_BIND_PREFERENCES = 1; + private static final String PREFERENCES_TAG = "android:preferences"; + private boolean mHavePrefs; + private boolean mInitDone; + private ListView mList; + private PreferenceManager mPreferenceManager; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_BIND_PREFERENCES: + bindPreferences(); + break; + } + } + }; + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + private void bindPreferences() { + PreferenceScreen localPreferenceScreen = getPreferenceScreen(); + if (localPreferenceScreen != null) { + ListView localListView = getListView(); + localPreferenceScreen.bind(localListView); + } + } + + private void ensureList() { + if (mList == null) { + View view = getView(); + if (view == null) { + throw new IllegalStateException("Content view not yet created"); + } + + View listView = view.findViewById(android.R.id.list); + if (!(listView instanceof ListView)) { + throw new RuntimeException("Content has view with id attribute 'android.R.id.list' that is not a ListView class"); + } + + mList = (ListView)listView; + if (mList == null) { + throw new RuntimeException("Your content must have a ListView whose id attribute is 'android.R.id.list'"); + } + + mHandler.post(mRequestFocus); + } + } + + private void postBindPreferences() { + if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) { + mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); + } + } + + private void requirePreferenceManager() { + if (this.mPreferenceManager == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + } + + public void addPreferencesFromIntent(Intent intent) { + requirePreferenceManager(); + PreferenceScreen screen = inflateFromIntent(intent, getPreferenceScreen()); + setPreferenceScreen(screen); + } + + public PreferenceScreen addPreferencesFromResource(int resId) { + requirePreferenceManager(); + PreferenceScreen screen = inflateFromResource(getActivity(), resId, getPreferenceScreen()); + setPreferenceScreen(screen); + + for(int i = 0; i < screen.getPreferenceCount(); i++) { + Preference preference = screen.getPreference(i); + if(preference instanceof PreferenceScreen && preference.getKey() != null) { + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onStartNewFragment(preference.getKey()); + return false; + } + }); + } + } + + return screen; + } + + public Preference findPreference(CharSequence key) { + if (mPreferenceManager == null) { + return null; + } + return mPreferenceManager.findPreference(key); + } + + public ListView getListView() { + ensureList(); + return mList; + } + + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); + if (mHavePrefs) { + bindPreferences(); + } + mInitDone = true; + if (savedInstanceState != null) { + Bundle localBundle = savedInstanceState.getBundle(PREFERENCES_TAG); + if (localBundle != null) { + PreferenceScreen screen = getPreferenceScreen(); + if (screen != null) { + screen.restoreHierarchyState(localBundle); + } + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + dispatchActivityResult(requestCode, resultCode, data); + } + + @Override + public void onCreate(Bundle paramBundle) { + super.onCreate(paramBundle); + mPreferenceManager = createPreferenceManager(); + + int res = this.getArguments().getInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, 0); + if(res != 0) { + PreferenceScreen preferenceScreen = addPreferencesFromResource(res); + onInitPreferences(preferenceScreen); + } + } + + @Override + public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) { + return paramLayoutInflater.inflate(R.layout.preferences, paramViewGroup, false); + } + + @Override + public void onDestroy() { + super.onDestroy(); + dispatchActivityDestroy(); + } + + @Override + public void onDestroyView() { + mList = null; + mHandler.removeCallbacks(mRequestFocus); + mHandler.removeMessages(MSG_BIND_PREFERENCES); + super.onDestroyView(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + PreferenceScreen screen = getPreferenceScreen(); + if (screen != null) { + Bundle localBundle = new Bundle(); + screen.saveHierarchyState(localBundle); + bundle.putBundle(PREFERENCES_TAG, localBundle); + } + } + + @Override + public void onStop() { + super.onStop(); + dispatchActivityStop(); + } + + /** Access methods with visibility private **/ + + private PreferenceManager createPreferenceManager() { + try { + Constructor c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class); + c.setAccessible(true); + return c.newInstance(this.getActivity(), FIRST_REQUEST_CODE); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PreferenceScreen getPreferenceScreen() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen"); + m.setAccessible(true); + return (PreferenceScreen) m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected void setPreferenceScreen(PreferenceScreen preferenceScreen) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class); + m.setAccessible(true); + boolean result = (Boolean) m.invoke(mPreferenceManager, preferenceScreen); + if (result && preferenceScreen != null) { + mHavePrefs = true; + if (mInitDone) { + postBindPreferences(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityResult(int requestCode, int resultCode, Intent data) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class); + m.setAccessible(true); + m.invoke(mPreferenceManager, requestCode, resultCode, data); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityDestroy() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy"); + m.setAccessible(true); + m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityStop() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop"); + m.setAccessible(true); + m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private void setFragment(PreferenceFragment preferenceFragment) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("setFragment", PreferenceFragment.class); + m.setAccessible(true); + m.invoke(mPreferenceManager, preferenceFragment); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public PreferenceScreen inflateFromResource(Context context, int resId, PreferenceScreen rootPreferences) { + PreferenceScreen preferenceScreen ; + try { + Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class); + m.setAccessible(true); + preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, context, resId, rootPreferences); + } catch (Exception e) { + throw new RuntimeException(e); + } + return preferenceScreen; + } + + public PreferenceScreen inflateFromIntent(Intent queryIntent, PreferenceScreen rootPreferences) { + PreferenceScreen preferenceScreen ; + try { + Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class); + m.setAccessible(true); + preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, queryIntent, rootPreferences); + } catch (Exception e) { + throw new RuntimeException(e); + } + return preferenceScreen; + } + + protected abstract void onInitPreferences(PreferenceScreen preferenceScreen); + protected abstract void onStartNewFragment(String name); +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java new file mode 100644 index 0000000..c1a7763 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java @@ -0,0 +1,291 @@ +package github.nvllsvm.audinaut.fragments; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.MenuItem; +import android.net.Uri; +import android.view.ViewGroup; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.ArtistAdapter; +import github.nvllsvm.audinaut.adapter.EntryGridAdapter; +import github.nvllsvm.audinaut.adapter.SearchAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.BackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; + +public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SearchFragment.class.getSimpleName(); + + private static final int MAX_ARTISTS = 20; + private static final int MAX_ALBUMS = 20; + private static final int MAX_SONGS = 50; + private static final int MIN_CLOSENESS = 1; + + protected RecyclerView recyclerView; + protected SearchAdapter adapter; + protected boolean largeAlbums = false; + + private SearchResult searchResult; + private boolean skipSearch = false; + private String currentQuery; + + public SearchFragment() { + super(); + alwaysStartFullscreen = true; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if(savedInstanceState != null) { + searchResult = (SearchResult) savedInstanceState.getSerializable(Constants.FRAGMENT_LIST); + } + largeAlbums = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(Constants.FRAGMENT_LIST, searchResult); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + rootView = inflater.inflate(R.layout.abstract_recycler_fragment, container, false); + setTitle(R.string.search_title); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setEnabled(false); + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + setupLayoutManager(recyclerView, largeAlbums); + + registerForContextMenu(recyclerView); + context.onNewIntent(context.getIntent()); + + if(searchResult != null) { + skipSearch = true; + recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, this)); + } + + return rootView; + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && adapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = adapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == ArtistAdapter.VIEW_TYPE_ARTIST) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + menuInflater.inflate(R.menu.search, menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Serializable item) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + if(item instanceof MusicDirectory.Entry && !Util.isOffline(context)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Serializable item) { + return onContextItemSelected(menuItem, item); + } + + @Override + public void refresh(boolean refresh) { + context.onNewIntent(context.getIntent()); + } + + @Override + public void onItemClicked(UpdateView updateView, Serializable item) { + if (item instanceof Artist) { + onArtistSelected((Artist) item, false); + } else if (item instanceof MusicDirectory.Entry) { + MusicDirectory.Entry entry = (MusicDirectory.Entry) item; + if (entry.isDirectory()) { + onAlbumSelected(entry, false); + } else { + onSongSelected(entry, false, true, true, false); + } + } + } + + @Override + protected List getSelectedEntries() { + List selected = adapter.getSelected(); + List selectedMedia = new ArrayList<>(); + for(Serializable ser: selected) { + if(ser instanceof MusicDirectory.Entry) { + selectedMedia.add((MusicDirectory.Entry) ser); + } + } + + return selectedMedia; + } + + @Override + protected boolean isShowArtistEnabled() { + return true; + } + + public void search(final String query, final boolean autoplay) { + if(skipSearch) { + skipSearch = false; + return; + } + currentQuery = query; + + BackgroundTask task = new TabBackgroundTask(this) { + @Override + protected SearchResult doInBackground() throws Throwable { + SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS); + MusicService service = MusicServiceFactory.getMusicService(context); + return service.search(criteria, context, this); + } + + @Override + protected void done(SearchResult result) { + searchResult = result; + recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, SearchFragment.this)); + if (autoplay) { + autoplay(query); + } + + } + }; + task.execute(); + + if(searchItem != null) { + MenuItemCompat.collapseActionView(searchItem); + } + } + + protected String getCurrentQuery() { + return currentQuery; + } + + private void onArtistSelected(Artist artist, boolean autoplay) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + if(autoplay) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + } + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment); + } + + private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle()); + if(autoplay) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + } + fragment.setArguments(args); + + replaceFragment(fragment); + } + + private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) { + DownloadService downloadService = getDownloadService(); + if (downloadService != null) { + if (!append) { + downloadService.clear(); + } + downloadService.download(Arrays.asList(song), save, false, playNext, false); + if (autoplay) { + downloadService.play(downloadService.size() - 1); + } + + Util.toast(context, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)); + } + } + + private void autoplay(String query) { + query = query.toLowerCase(); + + Artist artist = null; + if(!searchResult.getArtists().isEmpty()) { + artist = searchResult.getArtists().get(0); + artist.setCloseness(Util.getStringDistance(artist.getName().toLowerCase(), query)); + } + MusicDirectory.Entry album = null; + if(!searchResult.getAlbums().isEmpty()) { + album = searchResult.getAlbums().get(0); + album.setCloseness(Util.getStringDistance(album.getTitle().toLowerCase(), query)); + } + MusicDirectory.Entry song = null; + if(!searchResult.getSongs().isEmpty()) { + song = searchResult.getSongs().get(0); + song.setCloseness(Util.getStringDistance(song.getTitle().toLowerCase(), query)); + } + + if(artist != null && (artist.getCloseness() <= MIN_CLOSENESS || + (album == null || artist.getCloseness() <= album.getCloseness()) && + (song == null || artist.getCloseness() <= song.getCloseness()))) { + onArtistSelected(artist, true); + } else if(album != null && (album.getCloseness() <= MIN_CLOSENESS || + song == null || album.getCloseness() <= song.getCloseness())) { + onAlbumSelected(album, true); + } else if(song != null) { + onSongSelected(song, false, false, true, false); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java new file mode 100644 index 0000000..81d5f29 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java @@ -0,0 +1,253 @@ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.ArtistAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class SelectArtistFragment extends SelectRecyclerFragment implements ArtistAdapter.OnMusicFolderChanged { + private static final String TAG = SelectArtistFragment.class.getSimpleName(); + + private List musicFolders = null; + private List entries; + private String groupId; + private String groupName; + + public SelectArtistFragment() { + super(); + } + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + if(bundle != null) { + musicFolders = (List) bundle.getSerializable(Constants.FRAGMENT_LIST2); + } + artist = true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(Constants.FRAGMENT_LIST2, (Serializable) musicFolders); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + Bundle args = getArguments(); + if(args != null) { + if(args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)) { + groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); + groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + + if (groupName != null) { + setTitle(groupName); + context.invalidateOptionsMenu(); + } + } + } + + super.onCreateView(inflater, container, bundle); + + return rootView; + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Serializable item) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Serializable item) { + return onContextItemSelected(menuItem, item); + } + + @Override + public void onItemClicked(UpdateView updateView, Serializable item) { + SubsonicFragment fragment; + if(item instanceof Artist) { + Artist artist = (Artist) item; + + if ((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || groupId != null) { + fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + + if (!Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + + fragment.setArguments(args); + } else { + fragment = new SelectArtistFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + if (!Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + + fragment.setArguments(args); + } + + replaceFragment(fragment); + } else { + Entry entry = (Entry) item; + onSongPress(entries, entry); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + super.onCreateOptionsMenu(menu, menuInflater); + + if(Util.isOffline(context) || Util.isTagBrowsing(context) || groupId != null) { + menu.removeItem(R.id.menu_first_level_artist); + } else { + if (Util.isFirstLevelArtist(context)) { + menu.findItem(R.id.menu_first_level_artist).setChecked(true); + } + } + } + + @Override + public int getOptionsMenu() { + return R.menu.select_artist; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(super.onOptionsItemSelected(item)) { + return true; + } + + switch (item.getItemId()) { + case R.id.menu_first_level_artist: + toggleFirstLevelArtist(); + break; + } + + return false; + } + + @Override + public SectionAdapter getAdapter(List objects) { + return new ArtistAdapter(context, objects, musicFolders, this, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List items; + if(groupId == null) { + if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) { + musicFolders = musicService.getMusicFolders(refresh, context, listener); + + // Hide folders option if there is only one + if (musicFolders.size() == 1) { + musicFolders = null; + Util.setSelectedMusicFolderId(context, null); + } + } else { + musicFolders = null; + } + String musicFolderId = Util.getSelectedMusicFolderId(context); + + Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener); + indexes.sortChildren(context); + items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); + items.addAll(indexes.getShortcuts()); + items.addAll(indexes.getArtists()); + entries = indexes.getEntries(); + items.addAll(entries); + } else { + List artists = new ArrayList<>(); + items = new ArrayList<>(); + MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener); + for(Entry entry: dir.getChildren(true, false)) { + Artist artist = new Artist(); + artist.setId(entry.getId()); + artist.setName(entry.getTitle()); + artists.add(artist); + } + + Indexes indexes = new Indexes(0, new ArrayList(), artists); + indexes.sortChildren(context); + items.addAll(indexes.getArtists()); + + entries = dir.getChildren(false, true); + for(Entry entry: entries) { + items.add(entry); + } + } + + return items; + } + + @Override + public int getTitleResource() { + return groupId == null ? R.string.button_bar_browse : 0; + } + + @Override + public void setEmpty(boolean empty) { + super.setEmpty(empty); + + if(empty && !Util.isOffline(context)) { + objects.clear(); + recyclerView.setAdapter(new ArtistAdapter(context, objects, musicFolders, this, this)); + recyclerView.setVisibility(View.VISIBLE); + + View view = rootView.findViewById(R.id.tab_progress); + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); + params.height = 0; + params.weight = 5; + view.setLayoutParams(params); + } + } + + private void toggleFirstLevelArtist() { + Util.toggleFirstLevelArtist(context); + context.invalidateOptionsMenu(); + } + + @Override + public void onMusicFolderChanged(MusicFolder selectedFolder) { + String startMusicFolderId = Util.getSelectedMusicFolderId(context); + String musicFolderId = selectedFolder == null ? null : selectedFolder.getId(); + + if(!Util.equals(startMusicFolderId, musicFolderId)) { + Util.setSelectedMusicFolderId(context, musicFolderId); + context.invalidate(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java new file mode 100644 index 0000000..f47f79b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java @@ -0,0 +1,840 @@ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.AlphabeticalAlbumAdapter; +import github.nvllsvm.audinaut.adapter.EntryInfiniteGridAdapter; +import github.nvllsvm.audinaut.adapter.EntryGridAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.CachedMusicService; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; + +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.GridSpacingDecoration; +import github.nvllsvm.audinaut.view.MyLeadingMarginSpan2; +import github.nvllsvm.audinaut.view.RecyclingImageView; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; + +public class SelectDirectoryFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SelectDirectoryFragment.class.getSimpleName(); + + private RecyclerView recyclerView; + private FastScroller fastScroller; + private EntryGridAdapter entryGridAdapter; + private List albums; + private List entries; + private LoadTask currentTask; + + private SilentBackgroundTask updateCoverArtTask; + private ImageView coverArtView; + private Entry coverArtRep; + private String coverArtId; + + String id; + String name; + Entry directory; + String playlistId; + String playlistName; + boolean playlistOwner; + String albumListType; + String albumListExtra; + int albumListSize; + boolean refreshListing = false; + boolean restoredInstance = false; + boolean lookupParent = false; + boolean largeAlbums = false; + boolean topTracks = false; + String lookupEntry; + + public SelectDirectoryFragment() { + super(); + } + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + if(bundle != null) { + entries = (List) bundle.getSerializable(Constants.FRAGMENT_LIST); + albums = (List) bundle.getSerializable(Constants.FRAGMENT_LIST2); + if(albums == null) { + albums = new ArrayList<>(); + } + restoredInstance = true; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries); + outState.putSerializable(Constants.FRAGMENT_LIST2, (Serializable) albums); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + Bundle args = getArguments(); + if(args != null) { + id = args.getString(Constants.INTENT_EXTRA_NAME_ID); + name = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + directory = (Entry) args.getSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY); + playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME); + playlistOwner = args.getBoolean(Constants.INTENT_EXTRA_NAME_PLAYLIST_OWNER, false); + Object shareObj = args.getSerializable(Constants.INTENT_EXTRA_NAME_SHARE); + albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE); + albumListExtra = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA); + albumListSize = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0); + refreshListing = args.getBoolean(Constants.INTENT_EXTRA_REFRESH_LISTINGS); + artist = args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false); + lookupEntry = args.getString(Constants.INTENT_EXTRA_SEARCH_SONG); + topTracks = args.getBoolean(Constants.INTENT_EXTRA_TOP_TRACKS); + + String childId = args.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID); + if(childId != null) { + id = childId; + lookupParent = true; + } + if(entries == null) { + entries = (List) args.getSerializable(Constants.FRAGMENT_LIST); + albums = (List) args.getSerializable(Constants.FRAGMENT_LIST2); + + if(albums == null) { + albums = new ArrayList(); + } + } + } + + rootView = inflater.inflate(R.layout.abstract_recycler_fragment, container, false); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setOnRefreshListener(this); + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true)) { + largeAlbums = true; + } + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + recyclerView.setHasFixedSize(true); + fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); + setupScrollList(recyclerView); + setupLayoutManager(recyclerView, largeAlbums); + + if(entries == null) { + if(primaryFragment || secondaryFragment) { + load(false); + } else { + invalidated = true; + } + } else { + finishLoading(); + } + + if(name != null) { + setTitle(name); + } + + return rootView; + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && entryGridAdapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + if(albumListType != null) { + menuInflater.inflate(R.menu.select_album_list, menu); + } else if(artist) { + menuInflater.inflate(R.menu.select_album, menu); + } else { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_song_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_song, menu); + + if(playlistId == null || !playlistOwner) { + menu.removeItem(R.id.menu_remove_playlist); + } + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_remove_playlist: + removeFromPlaylist(playlistId, playlistName, getSelectedIndexes()); + return true; + } + + return super.onOptionsItemSelected(item); + + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Entry entry) { + onCreateContextMenuSupport(menu, menuInflater, updateView, entry); + if(!Util.isOffline(context) && (playlistId == null || !playlistOwner)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + + recreateContextMenu(menu); + } + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Entry entry) { + if(onContextItemSelected(menuItem, entry)) { + return true; + } + + switch (menuItem.getItemId()) { + case R.id.song_menu_remove_playlist: + removeFromPlaylist(playlistId, playlistName, Arrays.asList(entries.indexOf(entry))); + break; + } + + return true; + } + + @Override + public void onItemClicked(UpdateView updateView, Entry entry) { + if (entry.isDirectory()) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle()); + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, entry); + if ("newest".equals(albumListType)) { + args.putBoolean(Constants.INTENT_EXTRA_REFRESH_LISTINGS, true); + } + if(!entry.isAlbum()) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + } + fragment.setArguments(args); + + replaceFragment(fragment, true); + } else { + onSongPress(entries, entry, albumListType == null); + } + } + + @Override + protected void refresh(boolean refresh) { + load(refresh); + } + + @Override + protected boolean isShowArtistEnabled() { + return albumListType != null; + } + + private void load(boolean refresh) { + if(refreshListing) { + refresh = true; + } + + if(currentTask != null) { + currentTask.cancel(); + } + + recyclerView.setVisibility(View.INVISIBLE); + if (playlistId != null) { + getPlaylist(playlistId, playlistName, refresh); + } else if (albumListType != null) { + getAlbumList(albumListType, albumListSize, refresh); + } else { + getMusicDirectory(id, name, refresh); + } + } + + private void getMusicDirectory(final String id, final String name, final boolean refresh) { + setTitle(name); + + new LoadTask(refresh) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + MusicDirectory dir = getMusicDirectory(id, name, refresh, service, this); + + if(lookupParent && dir.getParent() != null) { + dir = getMusicDirectory(dir.getParent(), name, refresh, service, this); + + // Update the fragment pointers so other stuff works correctly + SelectDirectoryFragment.this.id = dir.getId(); + SelectDirectoryFragment.this.name = dir.getName(); + } else if(id != null && directory == null && dir.getParent() != null && !artist) { + MusicDirectory parentDir = getMusicDirectory(dir.getParent(), name, refresh, true, service, this); + for(Entry child: parentDir.getChildren()) { + if(id.equals(child.getId())) { + directory = child; + break; + } + } + } + + return dir; + } + + @Override + protected void done(Pair result) { + SelectDirectoryFragment.this.name = result.getFirst().getName(); + setTitle(SelectDirectoryFragment.this.name); + super.done(result); + } + }.execute(); + } + + private void getRecursiveMusicDirectory(final String id, final String name, final boolean refresh) { + setTitle(name); + + new LoadTask(refresh) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + MusicDirectory root = getMusicDirectory(id, name, refresh, service, this); + List songs = new ArrayList(); + getSongsRecursively(root, songs); + root.replaceChildren(songs); + return root; + } + + private void getSongsRecursively(MusicDirectory parent, List songs) throws Exception { + songs.addAll(parent.getChildren(false, true)); + for (Entry dir : parent.getChildren(true, false)) { + MusicService musicService = MusicServiceFactory.getMusicService(context); + + MusicDirectory musicDirectory; + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this); + } else { + musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this); + } + getSongsRecursively(musicDirectory, songs); + } + } + + @Override + protected void done(Pair result) { + SelectDirectoryFragment.this.name = result.getFirst().getName(); + setTitle(SelectDirectoryFragment.this.name); + super.done(result); + } + }.execute(); + } + + private void getPlaylist(final String playlistId, final String playlistName, final boolean refresh) { + setTitle(playlistName); + + new LoadTask(refresh) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + return service.getPlaylist(refresh, playlistId, playlistName, context, this); + } + }.execute(); + } + + private void getTopTracks(final String id, final String name, final boolean refresh) { + setTitle(name); + + new LoadTask(refresh) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + return service.getTopTrackSongs(name, 50, context, this); + } + }.execute(); + } + + private void getAlbumList(final String albumListType, final int size, final boolean refresh) { + if ("newest".equals(albumListType)) { + setTitle(R.string.main_albums_newest); + } else if ("random".equals(albumListType)) { + setTitle(R.string.main_albums_random); + } else if ("recent".equals(albumListType)) { + setTitle(R.string.main_albums_recent); + } else if ("frequent".equals(albumListType)) { + setTitle(R.string.main_albums_frequent); + } else if("genres".equals(albumListType) || "years".equals(albumListType)) { + setTitle(albumListExtra); + } else if("alphabeticalByName".equals(albumListType)) { + setTitle(R.string.main_albums_alphabetical); + } if (MainFragment.SONGS_NEWEST.equals(albumListType)) { + setTitle(R.string.main_songs_newest); + } else if (MainFragment.SONGS_TOP_PLAYED.equals(albumListType)) { + setTitle(R.string.main_songs_top_played); + } else if (MainFragment.SONGS_RECENT.equals(albumListType)) { + setTitle(R.string.main_songs_recent); + } else if (MainFragment.SONGS_FREQUENT.equals(albumListType)) { + setTitle(R.string.main_songs_frequent); + } + + new LoadTask(true) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + MusicDirectory result; + if("genres".equals(albumListType) || "years".equals(albumListType)) { + result = service.getAlbumList(albumListType, albumListExtra, size, 0, refresh, context, this); + if(result.getChildrenSize() == 0 && "genres".equals(albumListType)) { + SelectDirectoryFragment.this.albumListType = "genres-songs"; + result = service.getSongsByGenre(albumListExtra, size, 0, context, this); + } + } else if("genres".equals(albumListType) || "genres-songs".equals(albumListType)) { + result = service.getSongsByGenre(albumListExtra, size, 0, context, this); + } else if(albumListType.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(albumListType, size, 0, context, this); + } else { + result = service.getAlbumList(albumListType, size, 0, refresh, context, this); + } + return result; + } + }.execute(); + } + + private abstract class LoadTask extends TabBackgroundTask> { + private boolean refresh; + + public LoadTask(boolean refresh) { + super(SelectDirectoryFragment.this); + this.refresh = refresh; + + currentTask = this; + } + + protected abstract MusicDirectory load(MusicService service) throws Exception; + + @Override + protected Pair doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory dir = load(musicService); + + albums = dir.getChildren(true, false); + entries = dir.getChildren(); + + // This isn't really an artist if no albums on it! + if(albums.size() == 0) { + artist = false; + } + + return new Pair<>(dir, true); + } + + @Override + protected void done(Pair result) { + finishLoading(); + currentTask = null; + } + + @Override + public void updateCache(int changeCode) { + if(entryGridAdapter != null && changeCode == CachedMusicService.CACHE_UPDATE_LIST) { + entryGridAdapter.notifyDataSetChanged(); + } else if(changeCode == CachedMusicService.CACHE_UPDATE_METADATA) { + if(coverArtView != null && coverArtRep != null && !Util.equals(coverArtRep.getCoverArt(), coverArtId)) { + synchronized (coverArtRep) { + if (updateCoverArtTask != null && updateCoverArtTask.isRunning()) { + updateCoverArtTask.cancel(); + } + updateCoverArtTask = getImageLoader().loadImage(coverArtView, coverArtRep, false, true); + coverArtId = coverArtRep.getCoverArt(); + } + } + } + } + } + + @Override + public SectionAdapter getCurrentAdapter() { + return entryGridAdapter; + } + + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = entryGridAdapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + + private void finishLoading() { + boolean validData = !entries.isEmpty() || !albums.isEmpty(); + if(!validData) { + setEmpty(true); + } + + if(validData) { + recyclerView.setVisibility(View.VISIBLE); + } + + if(albumListType == null) { + entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums); + entryGridAdapter.setRemoveFromPlaylist(playlistId != null); + } else { + if("alphabeticalByName".equals(albumListType)) { + entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums); + } else { + entryGridAdapter = new EntryInfiniteGridAdapter(context, entries, getImageLoader(), largeAlbums); + } + + // Setup infinite loading based on scrolling + final EntryInfiniteGridAdapter infiniteGridAdapter = (EntryInfiniteGridAdapter) entryGridAdapter; + infiniteGridAdapter.setData(albumListType, albumListExtra, albumListSize); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + int totalItemCount = layoutManager.getItemCount(); + int lastVisibleItem; + if(layoutManager instanceof GridLayoutManager) { + lastVisibleItem = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else if(layoutManager instanceof LinearLayoutManager) { + lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else { + return; + } + + if(totalItemCount > 0 && lastVisibleItem >= totalItemCount - 2) { + infiniteGridAdapter.loadMore(); + } + } + }); + } + entryGridAdapter.setOnItemClickedListener(this); + // Always show artist if this is not a artist we are viewing + if(!artist) { + entryGridAdapter.setShowArtist(true); + } + if(topTracks) { + entryGridAdapter.setShowAlbum(true); + } + + int scrollToPosition = -1; + if(lookupEntry != null) { + for(int i = 0; i < entries.size(); i++) { + if(lookupEntry.equals(entries.get(i).getTitle())) { + scrollToPosition = i; + entryGridAdapter.addSelected(entries.get(i)); + lookupEntry = null; + break; + } + } + } + + recyclerView.setAdapter(entryGridAdapter); + fastScroller.attachRecyclerView(recyclerView); + context.supportInvalidateOptionsMenu(); + + if(scrollToPosition != -1) { + recyclerView.scrollToPosition(scrollToPosition); + } + + Bundle args = getArguments(); + boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); + if (playAll && !restoredInstance) { + playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false, false); + } + } + + @Override + protected void playNow(final boolean shuffle, final boolean append, final boolean playNext) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + download(songs, append, false, !append, playNext, shuffle); + entryGridAdapter.clearSelected(); + } else { + playAll(shuffle, append, playNext); + } + } + private void playAll(final boolean shuffle, final boolean append, final boolean playNext) { + boolean hasSubFolders = albums != null && !albums.isEmpty(); + + if (hasSubFolders && id != null) { + downloadRecursively(id, false, append, !append, shuffle, false, playNext); + } else if(hasSubFolders && albumListType != null) { + downloadRecursively(albums, shuffle, append, playNext); + } else { + download(entries, append, false, !append, playNext, shuffle); + } + } + + private List getSelectedIndexes() { + List selected = entryGridAdapter.getSelected(); + List indexes = new ArrayList(); + + for(Entry entry: selected) { + indexes.add(entries.indexOf(entry)); + } + + return indexes; + } + + @Override + protected void downloadBackground(final boolean save) { + List songs = getSelectedEntries(); + if(playlistId != null) { + songs = entries; + } + + if(songs.isEmpty()) { + // Get both songs and albums + downloadRecursively(id, save, false, false, false, true); + } else { + downloadBackground(save, songs); + } + } + @Override + protected void downloadBackground(final boolean save, final List entries) { + if (getDownloadService() == null) { + return; + } + + warnIfStorageUnavailable(); + RecursiveLoader onValid = new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, true); + getDownloadService().downloadBackground(songs, save); + return null; + } + + @Override + protected void done(Boolean result) { + Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); + } + }; + } + + @Override + protected void download(List entries, boolean append, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(entries, append, save, autoplay, playNext, shuffle, playlistName, playlistId); + } + + @Override + protected void delete() { + List songs = getSelectedEntries(); + if(songs.isEmpty()) { + for(Entry entry: entries) { + if(entry.isDirectory()) { + deleteRecursively(entry); + } else { + songs.add(entry); + } + } + } + if (getDownloadService() != null) { + getDownloadService().delete(songs); + } + } + + public void removeFromPlaylist(final String id, final String name, final List indexes) { + new LoadingTask(context, true) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.removeFromPlaylist(id, indexes, context, null); + return null; + } + + @Override + protected void done(Void result) { + for(Integer index: indexes) { + entryGridAdapter.removeAt(index); + } + Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name)); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.updated_playlist_error, name) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + private void showTopTracks() { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(getArguments()); + args.putBoolean(Constants.INTENT_EXTRA_TOP_TRACKS, true); + fragment.setArguments(args); + + replaceFragment(fragment, true); + } + + private View createHeader() { + View header = LayoutInflater.from(context).inflate(R.layout.select_album_header, null, false); + + setupCoverArt(header); + setupTextDisplay(header); + + return header; + } + + private void setupCoverArt(View header) { + setupCoverArtImpl((RecyclingImageView) header.findViewById(R.id.select_album_art)); + } + private void setupCoverArtImpl(RecyclingImageView coverArtView) { + final ImageLoader imageLoader = getImageLoader(); + + if(entries.size() > 0) { + coverArtRep = null; + this.coverArtView = coverArtView; + for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) { + coverArtRep = entries.get(random.nextInt(entries.size())); + } + + coverArtView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (coverArtRep == null || coverArtRep.getCoverArt() == null) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + ImageView fullScreenView = new ImageView(context); + imageLoader.loadImage(fullScreenView, coverArtRep, true, true); + builder.setCancelable(true); + + AlertDialog imageDialog = builder.create(); + // Set view here with unecessary 0's to remove top/bottom border + imageDialog.setView(fullScreenView, 0, 0, 0, 0); + imageDialog.show(); + } + }); + synchronized (coverArtRep) { + coverArtId = coverArtRep.getCoverArt(); + updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true); + } + } + + coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { + @Override + public void onInvalidated(RecyclingImageView imageView) { + setupCoverArtImpl(imageView); + } + }); + } + private void setupTextDisplay(final View header) { + final TextView titleView = (TextView) header.findViewById(R.id.select_album_title); + if(playlistName != null) { + titleView.setText(playlistName); + } else if(name != null) { + titleView.setText(name); + } + + int songCount = 0; + + Set artists = new HashSet(); + Set years = new HashSet(); + Integer totalDuration = 0; + for (Entry entry : entries) { + if (!entry.isDirectory()) { + songCount++; + if (entry.getArtist() != null) { + artists.add(entry.getArtist()); + } + if(entry.getYear() != null) { + years.add(entry.getYear()); + } + Integer duration = entry.getDuration(); + if(duration != null) { + totalDuration += duration; + } + } + } + + final TextView artistView = (TextView) header.findViewById(R.id.select_album_artist); + if (artists.size() == 1) { + String artistText = artists.iterator().next(); + if(years.size() == 1) { + artistText += " - " + years.iterator().next(); + } + artistView.setText(artistText); + artistView.setVisibility(View.VISIBLE); + } else { + artistView.setVisibility(View.GONE); + } + + TextView songCountView = (TextView) header.findViewById(R.id.select_album_song_count); + TextView songLengthView = (TextView) header.findViewById(R.id.select_album_song_length); + String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount); + songCountView.setText(s.toUpperCase()); + songLengthView.setText(Util.formatDuration(totalDuration)); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java new file mode 100644 index 0000000..ca93101 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java @@ -0,0 +1,77 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.adapter.GenreAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.List; + +public class SelectGenreFragment extends SelectRecyclerFragment { + private static final String TAG = SelectGenreFragment.class.getSimpleName(); + + @Override + public int getOptionsMenu() { + return R.menu.empty; + } + + @Override + public SectionAdapter getAdapter(List objs) { + return new GenreAdapter(context, objs, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + return musicService.getGenres(refresh, context, listener); + } + + @Override + public int getTitleResource() { + return R.string.main_albums_genres; + } + + @Override + public void onItemClicked(UpdateView updateView, Genre genre) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "genres"); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, genre.getName()); + fragment.setArguments(args); + + replaceFragment(fragment); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Genre item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Genre item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java new file mode 100644 index 0000000..2cc6730 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java @@ -0,0 +1,341 @@ +package github.nvllsvm.audinaut.fragments; + +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SyncUtil; +import github.nvllsvm.audinaut.util.CacheCleaner; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.adapter.PlaylistAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SelectPlaylistFragment extends SelectRecyclerFragment { + private static final String TAG = SelectPlaylistFragment.class.getSimpleName(); + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true)) { + largeAlbums = true; + } + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Playlist playlist) { + if (Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_playlist_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_playlist_context, menu); + + if(playlist.getPublic() != null && playlist.getPublic() == true && playlist.getId().indexOf(".m3u") == -1 && !UserUtil.getCurrentUsername(context).equals(playlist.getOwner())) { + menu.removeItem(R.id.playlist_update_info); + menu.removeItem(R.id.playlist_menu_delete); + } + } + + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Playlist playlist) { + SubsonicFragment fragment; + Bundle args; + FragmentTransaction trans; + + switch (menuItem.getItemId()) { + case R.id.playlist_menu_download: + downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true); + break; + case R.id.playlist_menu_play_now: + fragment = new SelectDirectoryFragment(); + args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + fragment.setArguments(args); + + replaceFragment(fragment); + break; + case R.id.playlist_menu_play_shuffled: + fragment = new SelectDirectoryFragment(); + args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true); + args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + fragment.setArguments(args); + + replaceFragment(fragment); + break; + case R.id.playlist_menu_delete: + deletePlaylist(playlist); + break; + case R.id.playlist_info: + displayPlaylistInfo(playlist); + break; + case R.id.playlist_update_info: + updatePlaylistInfo(playlist); + break; + } + + return false; + } + + @Override + public int getOptionsMenu() { + return R.menu.abstract_top_menu; + } + + @Override + public SectionAdapter getAdapter(List playlists) { + List mine = new ArrayList<>(); + + String currentUsername = UserUtil.getCurrentUsername(context); + for(Playlist playlist: playlists) { + if(playlist.getOwner() == null || playlist.getOwner().equals(currentUsername)) { + mine.add(playlist); + } + } + + return new PlaylistAdapter(context, playlists, getImageLoader(), largeAlbums, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List playlists = musicService.getPlaylists(refresh, context, listener); + if(!Util.isOffline(context) && refresh) { + new CacheCleaner(context, getDownloadService()).cleanPlaylists(playlists); + } + return playlists; + } + + @Override + public int getTitleResource() { + return R.string.playlist_label; + } + + @Override + public void onItemClicked(UpdateView updateView, Playlist playlist) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); + if((playlist.getOwner() != null && playlist.getOwner().equals(UserUtil.getCurrentUsername(context)) || playlist.getId().indexOf(".m3u") != -1)) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_PLAYLIST_OWNER, true); + } + fragment.setArguments(args); + + replaceFragment(fragment); + } + + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String playlistId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (playlistId != null && objects != null) { + for (Playlist playlist : objects) { + if (playlistId.equals(playlist.getId())) { + onItemClicked(null, playlist); + break; + } + } + } + } + } + + private void deletePlaylist(final Playlist playlist) { + Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new LoadingTask(context, false) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.deletePlaylist(playlist.getId(), context, null); + SyncUtil.removeSyncedPlaylist(context, playlist.getId()); + return null; + } + + @Override + protected void done(Void result) { + adapter.removeItem(playlist); + Util.toast(context, context.getResources().getString(R.string.menu_deleted_playlist, playlist.getName())); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + }); + } + + private void displayPlaylistInfo(final Playlist playlist) { + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(playlist.getName()); + + if(playlist.getOwner() != null) { + headers.add(R.string.details_owner); + details.add(playlist.getOwner()); + } + + if(playlist.getComment() != null) { + headers.add(R.string.details_comments); + details.add(playlist.getComment()); + } + + headers.add(R.string.details_song_count); + details.add(playlist.getSongCount()); + + if(playlist.getDuration() != null) { + headers.add(R.string.details_length); + details.add(Util.formatDuration(playlist.getDuration())); + } + + if(playlist.getPublic() != null) { + headers.add(R.string.details_public); + details.add(Util.formatBoolean(context, playlist.getPublic())); + } + + if(playlist.getCreated() != null) { + headers.add(R.string.details_created); + details.add(Util.formatDate(playlist.getCreated())); + } + if(playlist.getChanged() != null) { + headers.add(R.string.details_updated); + details.add(Util.formatDate(playlist.getChanged())); + } + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); + } + + private void updatePlaylistInfo(final Playlist playlist) { + View dialogView = context.getLayoutInflater().inflate(R.layout.update_playlist, null); + final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_playlist_name); + final EditText commentBox = (EditText)dialogView.findViewById(R.id.get_playlist_comment); + final CheckBox publicBox = (CheckBox)dialogView.findViewById(R.id.get_playlist_public); + + nameBox.setText(playlist.getName()); + commentBox.setText(playlist.getComment()); + Boolean pub = playlist.getPublic(); + if(pub == null) { + publicBox.setEnabled(false); + } else { + publicBox.setChecked(pub); + } + + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.playlist_update_info) + .setView(dialogView) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new LoadingTask(context, false) { + @Override + protected Void doInBackground() throws Throwable { + String name = nameBox.getText().toString(); + String comment = commentBox.getText().toString(); + boolean isPublic = publicBox.isChecked(); + + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.updatePlaylist(playlist.getId(), name, comment, isPublic, context, null); + + playlist.setName(name); + playlist.setComment(comment); + playlist.setPublic(isPublic); + + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, context.getResources().getString(R.string.playlist_updated_info, playlist.getName())); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.playlist_updated_info_error, playlist.getName()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + }) + .setNegativeButton(R.string.common_cancel, null) + .show(); + } + + private void syncPlaylist(Playlist playlist) { + SyncUtil.addSyncedPlaylist(context, playlist.getId()); + downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true); + } + + private void stopSyncPlaylist(final Playlist playlist) { + SyncUtil.removeSyncedPlaylist(context, playlist.getId()); + + new LoadingTask(context, false) { + @Override + protected Void doInBackground() throws Throwable { + // Unpin all of the songs in playlist + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory root = musicService.getPlaylist(true, playlist.getId(), playlist.getName(), context, this); + for(MusicDirectory.Entry entry: root.getChildren()) { + DownloadFile file = new DownloadFile(context, entry, false); + file.unpin(); + } + + return null; + } + + @Override + protected void done(Void result) { + + } + }.execute(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java new file mode 100644 index 0000000..61ff449 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java @@ -0,0 +1,219 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.view.FastScroller; + +public abstract class SelectRecyclerFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SelectRecyclerFragment.class.getSimpleName(); + protected RecyclerView recyclerView; + protected FastScroller fastScroller; + protected SectionAdapter adapter; + protected UpdateTask currentTask; + protected List objects; + protected boolean serialize = true; + protected boolean largeAlbums = false; + protected boolean pullToRefresh = true; + protected boolean backgroundUpdate = true; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + if(bundle != null && serialize) { + objects = (List) bundle.getSerializable(Constants.FRAGMENT_LIST); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if(serialize) { + outState.putSerializable(Constants.FRAGMENT_LIST, (Serializable) objects); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + rootView = inflater.inflate(R.layout.abstract_recycler_fragment, container, false); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setOnRefreshListener(this); + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); + setupLayoutManager(); + + if(pullToRefresh) { + setupScrollList(recyclerView); + } else { + refreshLayout.setEnabled(false); + } + + if(objects == null) { + refresh(false); + } else { + recyclerView.setAdapter(adapter = getAdapter(objects)); + } + + return rootView; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + if(!primaryFragment) { + return; + } + + menuInflater.inflate(getOptionsMenu(), menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && adapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + protected void refresh(final boolean refresh) { + int titleRes = getTitleResource(); + if(titleRes != 0) { + setTitle(getTitleResource()); + } + if(backgroundUpdate) { + recyclerView.setVisibility(View.GONE); + } + + // Cancel current running task before starting another one + if(currentTask != null) { + currentTask.cancel(); + } + + currentTask = new UpdateTask(this, refresh); + + if(backgroundUpdate) { + currentTask.execute(); + } else { + objects = new ArrayList(); + + try { + objects = getObjects(null, refresh, null); + } catch (Exception x) { + Log.e(TAG, "Failed to load", x); + } + + currentTask.done(objects); + } + } + + public SectionAdapter getCurrentAdapter() { + return adapter; + } + + private void setupLayoutManager() { + setupLayoutManager(recyclerView, largeAlbums); + } + + public abstract int getOptionsMenu(); + public abstract SectionAdapter getAdapter(List objs); + public abstract List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception; + public abstract int getTitleResource(); + + public void onFinishRefresh() { + + } + + private class UpdateTask extends TabBackgroundTask> { + private boolean refresh; + + public UpdateTask(SubsonicFragment fragment, boolean refresh) { + super(fragment); + this.refresh = refresh; + } + + @Override + public List doInBackground() throws Exception { + MusicService musicService = MusicServiceFactory.getMusicService(context); + + objects = new ArrayList(); + + try { + objects = getObjects(musicService, refresh, this); + } catch (Exception x) { + Log.e(TAG, "Failed to load", x); + } + + return objects; + } + + @Override + public void done(List result) { + if (result != null && !result.isEmpty()) { + recyclerView.setAdapter(adapter = getAdapter(result)); + if(!fastScroller.isAttached()) { + fastScroller.attachRecyclerView(recyclerView); + } + + onFinishRefresh(); + recyclerView.setVisibility(View.VISIBLE); + } else { + setEmpty(true); + } + + currentTask = null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java new file mode 100644 index 0000000..054c4bc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java @@ -0,0 +1,88 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.BasicListAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.view.UpdateView; + +public class SelectYearFragment extends SelectRecyclerFragment { + + public SelectYearFragment() { + super(); + pullToRefresh = false; + serialize = false; + backgroundUpdate = false; + } + + @Override + public int getOptionsMenu() { + return R.menu.empty; + } + + @Override + public SectionAdapter getAdapter(List objs) { + return new BasicListAdapter(context, objs, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List decades = new ArrayList<>(); + for(int i = 2010; i >= 1800; i -= 10) { + decades.add(String.valueOf(i)); + } + + return decades; + } + + @Override + public int getTitleResource() { + return R.string.main_albums_year; + } + + @Override + public void onItemClicked(UpdateView updateView, String decade) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "years"); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20); + args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0); + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, decade); + fragment.setArguments(args); + + replaceFragment(fragment); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, String item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, String item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java new file mode 100644 index 0000000..a75cbd3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java @@ -0,0 +1,806 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.text.DecimalFormat; +import java.util.LinkedHashMap; +import java.util.Map; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.SyncUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.CacheLocationPreference; +import github.nvllsvm.audinaut.view.ErrorDialog; + +public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + private final static String TAG = SettingsFragment.class.getSimpleName(); + + private final Map serverSettings = new LinkedHashMap(); + private boolean testingConnection; + private ListPreference theme; + private ListPreference maxBitrateWifi; + private ListPreference maxBitrateMobile; + private ListPreference networkTimeout; + private CacheLocationPreference cacheLocation; + private ListPreference preloadCountWifi; + private ListPreference preloadCountMobile; + private ListPreference keepPlayedCount; + private ListPreference tempLoss; + private ListPreference pauseDisconnect; + private Preference addServerPreference; + private PreferenceCategory serversCategory; + private ListPreference songPressAction; + private ListPreference syncInterval; + private CheckBoxPreference syncEnabled; + private CheckBoxPreference syncWifi; + private CheckBoxPreference syncNotification; + private CheckBoxPreference syncMostRecent; + private CheckBoxPreference replayGain; + private ListPreference replayGainType; + private Preference replayGainBump; + private Preference replayGainUntagged; + private String internalSSID; + private String internalSSIDDisplay; + private EditTextPreference cacheSize; + + private int serverCount = 3; + private SharedPreferences settings; + private DecimalFormat megabyteFromat; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + int instance = this.getArguments().getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1); + if (instance != -1) { + PreferenceScreen preferenceScreen = expandServer(instance); + setPreferenceScreen(preferenceScreen); + + serverSettings.put(Integer.toString(instance), new ServerSettings(instance)); + onInitPreferences(preferenceScreen); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + SharedPreferences prefs = Util.getPreferences(context); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onStartNewFragment(String name) { + SettingsFragment newFragment = new SettingsFragment(); + Bundle args = new Bundle(); + + int xml = 0; + if("appearance".equals(name)) { + xml = R.xml.settings_appearance; + } else if("cache".equals(name)) { + xml = R.xml.settings_cache; + } else if("playback".equals(name)) { + xml = R.xml.settings_playback; + } else if("servers".equals(name)) { + xml = R.xml.settings_servers; + } + + if(xml != 0) { + args.putInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, xml); + newFragment.setArguments(args); + replaceFragment(newFragment); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + // Random error I have no idea how to reproduce + if(sharedPreferences == null) { + return; + } + + update(); + + if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) { + setHideMedia(sharedPreferences.getBoolean(key, false)); + } + else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) { + setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true)); + } + else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) { + setCacheLocation(sharedPreferences.getString(key, "")); + } + else if(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT.equals(key)) { + SyncUtil.removeMostRecentSyncFiles(context); + } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(key)) { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null) { + downloadService.reapplyVolume(); + } + } else if(Constants.PREFERENCES_KEY_START_ON_HEADPHONES.equals(key)) { + Intent serviceIntent = new Intent(); + serviceIntent.setClassName(context.getPackageName(), HeadphoneListenerService.class.getName()); + + if(sharedPreferences.getBoolean(key, false)) { + context.startService(serviceIntent); + } else { + context.stopService(serviceIntent); + } + } + + scheduleBackup(); + } + + @Override + protected void onInitPreferences(PreferenceScreen preferenceScreen) { + this.setTitle(preferenceScreen.getTitle()); + + internalSSID = Util.getSSID(context); + if (internalSSID == null) { + internalSSID = ""; + } + internalSSIDDisplay = context.getResources().getString(R.string.settings_server_local_network_ssid_hint, internalSSID); + + theme = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_THEME); + maxBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI); + maxBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE); + networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); + cacheLocation = (CacheLocationPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); + preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); + preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); + keepPlayedCount = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT); + tempLoss = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS); + pauseDisconnect = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT); + serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); + addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); + songPressAction = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION); + syncInterval = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL); + syncEnabled = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED); + syncWifi = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI); + syncNotification = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION); + syncMostRecent = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT); + replayGain = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN); + replayGainType = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE); + replayGainBump = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP); + replayGainUntagged = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED); + cacheSize = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE); + + settings = Util.getPreferences(context); + serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + + if(cacheSize != null) { + this.findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.confirmDialog(context, R.string.common_delete, R.string.common_confirm_message_cache, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new LoadingTask(context, false) { + @Override + protected Void doInBackground() throws Throwable { + FileUtil.deleteMusicDirectory(context); + FileUtil.deleteSerializedCache(context); + FileUtil.deleteArtworkCache(context); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, R.string.settings_cache_clear_complete); + } + + @Override + protected void error(Throwable error) { + Util.toast(context, getErrorMessage(error), false); + } + }.execute(); + } + }); + return false; + } + }); + } + + if(syncEnabled != null) { + this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Boolean syncEnabled = (Boolean) newValue; + + Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); + ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled); + + return true; + } + }); + syncInterval.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Integer syncInterval = Integer.parseInt(((String) newValue)); + + Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); + ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval); + + return true; + } + }); + } + + if(serversCategory != null) { + addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + serverCount++; + int instance = serverCount; + serversCategory.addPreference(addServer(serverCount)); + + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); + // Reset set folder ID + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, "http://yourhost"); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, getResources().getString(R.string.settings_server_unused)); + editor.commit(); + + ServerSettings ss = new ServerSettings(instance); + serverSettings.put(String.valueOf(instance), ss); + ss.update(); + + return true; + } + }); + + serversCategory.setOrderingAsAdded(false); + for (int i = 1; i <= serverCount; i++) { + serversCategory.addPreference(addServer(i)); + serverSettings.put(String.valueOf(i), new ServerSettings(i)); + } + } + + SharedPreferences prefs = Util.getPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + + update(); + } + + private void scheduleBackup() { + try { + Class managerClass = Class.forName("android.app.backup.BackupManager"); + Constructor managerConstructor = managerClass.getConstructor(Context.class); + Object manager = managerConstructor.newInstance(context); + Method m = managerClass.getMethod("dataChanged"); + m.invoke(manager); + } catch(ClassNotFoundException e) { + Log.e(TAG, "No backup manager found"); + } catch(Throwable t) { + Log.e(TAG, "Scheduling backup failed " + t); + t.printStackTrace(); + } + } + + private void update() { + if (testingConnection) { + return; + } + + if(theme != null) { + theme.setSummary(theme.getEntry()); + } + + if(cacheSize != null) { + maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); + maxBitrateMobile.setSummary(maxBitrateMobile.getEntry()); + networkTimeout.setSummary(networkTimeout.getEntry()); + cacheLocation.setSummary(cacheLocation.getText()); + preloadCountWifi.setSummary(preloadCountWifi.getEntry()); + preloadCountMobile.setSummary(preloadCountMobile.getEntry()); + + try { + if(megabyteFromat == null) { + megabyteFromat = new DecimalFormat(getResources().getString(R.string.util_bytes_format_megabyte)); + } + + cacheSize.setSummary(megabyteFromat.format((double) Integer.parseInt(cacheSize.getText())).replace(".00", "")); + } catch(Exception e) { + Log.e(TAG, "Failed to format cache size", e); + cacheSize.setSummary(cacheSize.getText()); + } + } + + if(keepPlayedCount != null) { + keepPlayedCount.setSummary(keepPlayedCount.getEntry()); + tempLoss.setSummary(tempLoss.getEntry()); + pauseDisconnect.setSummary(pauseDisconnect.getEntry()); + songPressAction.setSummary(songPressAction.getEntry()); + + if(replayGain.isChecked()) { + replayGainType.setEnabled(true); + replayGainBump.setEnabled(true); + replayGainUntagged.setEnabled(true); + } else { + replayGainType.setEnabled(false); + replayGainBump.setEnabled(false); + replayGainUntagged.setEnabled(false); + } + replayGainType.setSummary(replayGainType.getEntry()); + } + + if(syncEnabled != null) { + syncInterval.setSummary(syncInterval.getEntry()); + + if(syncEnabled.isChecked()) { + if(!syncInterval.isEnabled()) { + syncInterval.setEnabled(true); + syncWifi.setEnabled(true); + syncNotification.setEnabled(true); + syncMostRecent.setEnabled(true); + } + } else { + if(syncInterval.isEnabled()) { + syncInterval.setEnabled(false); + syncWifi.setEnabled(false); + syncNotification.setEnabled(false); + syncMostRecent.setEnabled(false); + } + } + } + + for (ServerSettings ss : serverSettings.values()) { + ss.update(); + } + } + public void checkForRemoved() { + for (ServerSettings ss : serverSettings.values()) { + if(!ss.update()) { + serversCategory.removePreference(ss.getScreen()); + serverCount--; + } + } + } + + private PreferenceScreen addServer(final int instance) { + final PreferenceScreen screen = this.getPreferenceManager().createPreferenceScreen(context); + screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance); + screen.setOrder(instance); + + screen.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SettingsFragment newFragment = new SettingsFragment(); + + Bundle args = new Bundle(); + args.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); + newFragment.setArguments(args); + + replaceFragment(newFragment); + return false; + } + }); + + return screen; + } + + private PreferenceScreen expandServer(final int instance) { + final PreferenceScreen screen = this.getPreferenceManager().createPreferenceScreen(context); + screen.setTitle(R.string.settings_server_unused); + screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance); + + final EditTextPreference serverNamePreference = new EditTextPreference(context); + serverNamePreference.setKey(Constants.PREFERENCES_KEY_SERVER_NAME + instance); + serverNamePreference.setDefaultValue(getResources().getString(R.string.settings_server_unused)); + serverNamePreference.setTitle(R.string.settings_server_name); + serverNamePreference.setDialogTitle(R.string.settings_server_name); + + if (serverNamePreference.getText() == null) { + serverNamePreference.setText(getResources().getString(R.string.settings_server_unused)); + } + + serverNamePreference.setSummary(serverNamePreference.getText()); + + final EditTextPreference serverUrlPreference = new EditTextPreference(context); + serverUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_URL + instance); + serverUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); + serverUrlPreference.setDefaultValue("http://yourhost"); + serverUrlPreference.setTitle(R.string.settings_server_address); + serverUrlPreference.setDialogTitle(R.string.settings_server_address); + + if (serverUrlPreference.getText() == null) { + serverUrlPreference.setText("http://yourhost"); + } + + serverUrlPreference.setSummary(serverUrlPreference.getText()); + screen.setSummary(serverUrlPreference.getText()); + + final EditTextPreference serverLocalNetworkSSIDPreference = new EditTextPreference(context) { + @Override + protected void onAddEditTextToDialogView(View dialogView, final EditText editText) { + super.onAddEditTextToDialogView(dialogView, editText); + ViewGroup root = (ViewGroup) ((ViewGroup) dialogView).getChildAt(0); + + Button defaultButton = new Button(getContext()); + defaultButton.setText(internalSSIDDisplay); + defaultButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + editText.setText(internalSSID); + } + }); + root.addView(defaultButton); + } + }; + serverLocalNetworkSSIDPreference.setKey(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); + serverLocalNetworkSSIDPreference.setTitle(R.string.settings_server_local_network_ssid); + serverLocalNetworkSSIDPreference.setDialogTitle(R.string.settings_server_local_network_ssid); + + final EditTextPreference serverInternalUrlPreference = new EditTextPreference(context); + serverInternalUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); + serverInternalUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); + serverInternalUrlPreference.setDefaultValue(""); + serverInternalUrlPreference.setTitle(R.string.settings_server_internal_address); + serverInternalUrlPreference.setDialogTitle(R.string.settings_server_internal_address); + serverInternalUrlPreference.setSummary(serverInternalUrlPreference.getText()); + + final EditTextPreference serverUsernamePreference = new EditTextPreference(context); + serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance); + serverUsernamePreference.setTitle(R.string.settings_server_username); + serverUsernamePreference.setDialogTitle(R.string.settings_server_username); + + final EditTextPreference serverPasswordPreference = new EditTextPreference(context); + serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance); + serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + serverPasswordPreference.setSummary("***"); + serverPasswordPreference.setTitle(R.string.settings_server_password); + + final Preference serverOpenBrowser = new Preference(context); + serverOpenBrowser.setKey(Constants.PREFERENCES_KEY_OPEN_BROWSER); + serverOpenBrowser.setPersistent(false); + serverOpenBrowser.setTitle(R.string.settings_server_open_browser); + serverOpenBrowser.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + openInBrowser(instance); + return true; + } + }); + + Preference serverRemoveServerPreference = new Preference(context); + serverRemoveServerPreference.setKey(Constants.PREFERENCES_KEY_SERVER_REMOVE + instance); + serverRemoveServerPreference.setPersistent(false); + serverRemoveServerPreference.setTitle(R.string.settings_servers_remove); + + serverRemoveServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.confirmDialog(context, R.string.common_delete, screen.getTitle().toString(), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Reset values to null so when we ask for them again they are new + serverNamePreference.setText(null); + serverUrlPreference.setText(null); + serverUsernamePreference.setText(null); + serverPasswordPreference.setText(null); + + // Don't use Util.getActiveServer since it is 0 if offline + int activeServer = Util.getPreferences(context).getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + for (int i = instance; i <= serverCount; i++) { + Util.removeInstanceName(context, i, activeServer); + } + + serverCount--; + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); + editor.commit(); + + removeCurrent(); + + SubsonicFragment parentFragment = context.getCurrentFragment(); + if(parentFragment instanceof SettingsFragment) { + SettingsFragment serverSelectionFragment = (SettingsFragment) parentFragment; + serverSelectionFragment.checkForRemoved(); + } + } + }); + + return true; + } + }); + + Preference serverTestConnectionPreference = new Preference(context); + serverTestConnectionPreference.setKey(Constants.PREFERENCES_KEY_TEST_CONNECTION + instance); + serverTestConnectionPreference.setPersistent(false); + serverTestConnectionPreference.setTitle(R.string.settings_test_connection_title); + serverTestConnectionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + testConnection(instance); + return false; + } + }); + + screen.addPreference(serverNamePreference); + screen.addPreference(serverUrlPreference); + screen.addPreference(serverInternalUrlPreference); + screen.addPreference(serverLocalNetworkSSIDPreference); + screen.addPreference(serverUsernamePreference); + screen.addPreference(serverPasswordPreference); + screen.addPreference(serverTestConnectionPreference); + screen.addPreference(serverOpenBrowser); + screen.addPreference(serverRemoveServerPreference); + + return screen; + } + + private void setHideMedia(boolean hide) { + File nomediaDir = new File(FileUtil.getSubsonicDirectory(context), ".nomedia"); + File musicNoMedia = new File(FileUtil.getMusicDirectory(context), ".nomedia"); + if (hide && !nomediaDir.exists()) { + try { + if (!nomediaDir.createNewFile()) { + Log.w(TAG, "Failed to create " + nomediaDir); + } + } catch(Exception e) { + Log.w(TAG, "Failed to create " + nomediaDir, e); + } + + try { + if(!musicNoMedia.createNewFile()) { + Log.w(TAG, "Failed to create " + musicNoMedia); + } + } catch(Exception e) { + Log.w(TAG, "Failed to create " + musicNoMedia, e); + } + } else if (!hide && nomediaDir.exists()) { + if (!nomediaDir.delete()) { + Log.w(TAG, "Failed to delete " + nomediaDir); + } + if(!musicNoMedia.delete()) { + Log.w(TAG, "Failed to delete " + musicNoMedia); + } + } + Util.toast(context, R.string.settings_hide_media_toast, false); + } + + private void setMediaButtonsEnabled(boolean enabled) { + if (enabled) { + Util.registerMediaButtonEventReceiver(context); + } else { + Util.unregisterMediaButtonEventReceiver(context); + } + } + + private void setCacheLocation(String path) { + File dir = new File(path); + if (!FileUtil.verifyCanWrite(dir)) { + Util.toast(context, R.string.settings_cache_location_error, false); + + // Reset it to the default. + String defaultPath = FileUtil.getDefaultMusicDirectory(context).getPath(); + if (!defaultPath.equals(path)) { + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); + editor.commit(); + + if(cacheLocation != null) { + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } + } + + // Clear download queue. + DownloadService downloadService = DownloadService.getInstance(); + downloadService.clear(); + } + } + + private void testConnection(final int instance) { + LoadingTask task = new LoadingTask(context) { + private int previousInstance; + + @Override + protected Boolean doInBackground() throws Throwable { + updateProgress(R.string.settings_testing_connection); + + previousInstance = Util.getActiveServer(context); + testingConnection = true; + MusicService musicService = MusicServiceFactory.getMusicService(context); + try { + musicService.setInstance(instance); + musicService.ping(context, this); + return true; + } finally { + musicService.setInstance(null); + testingConnection = false; + } + } + + @Override + protected void done(Boolean licenseValid) { + Util.toast(context, R.string.settings_testing_ok); + } + + @Override + public void cancel() { + super.cancel(); + Util.setActiveServer(context, previousInstance); + } + + @Override + protected void error(Throwable error) { + Log.w(TAG, error.toString(), error); + new ErrorDialog(context, getResources().getString(R.string.settings_connection_failure) + + " " + getErrorMessage(error), false); + } + }; + task.execute(); + } + + private void openInBrowser(final int instance) { + SharedPreferences prefs = Util.getPreferences(context); + String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + if(url == null) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return; + } + Uri uriServer = Uri.parse(url); + + Intent browserIntent = new Intent(Intent.ACTION_VIEW, uriServer); + startActivity(browserIntent); + } + + private class ServerSettings { + private int instance; + private EditTextPreference serverName; + private EditTextPreference serverUrl; + private EditTextPreference serverLocalNetworkSSID; + private EditTextPreference serverInternalUrl; + private EditTextPreference username; + private PreferenceScreen screen; + + private ServerSettings(int instance) { + this.instance = instance; + screen = (PreferenceScreen) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY + instance); + serverName = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance); + serverUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance); + serverLocalNetworkSSID = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); + serverInternalUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); + username = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_USERNAME + instance); + + if(serverName != null) { + serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + try { + String url = (String) value; + new URL(url); + if (url.contains(" ") || url.contains("@") || url.contains("_")) { + throw new Exception(); + } + } catch (Exception x) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return false; + } + return true; + } + }); + serverInternalUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + try { + String url = (String) value; + // Allow blank internal IP address + if ("".equals(url) || url == null) { + return true; + } + + new URL(url); + if (url.contains(" ") || url.contains("@") || url.contains("_")) { + throw new Exception(); + } + } catch (Exception x) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return false; + } + return true; + } + }); + + username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String username = (String) value; + if (username == null || !username.equals(username.trim())) { + new ErrorDialog(context, R.string.settings_invalid_username, false); + return false; + } + return true; + } + }); + } + } + + public PreferenceScreen getScreen() { + return screen; + } + + public boolean update() { + SharedPreferences prefs = Util.getPreferences(context); + + if(prefs.contains(Constants.PREFERENCES_KEY_SERVER_NAME + instance)) { + if (serverName != null) { + serverName.setSummary(serverName.getText()); + serverUrl.setSummary(serverUrl.getText()); + serverLocalNetworkSSID.setSummary(serverLocalNetworkSSID.getText()); + serverInternalUrl.setSummary(serverInternalUrl.getText()); + username.setSummary(username.getText()); + + setTitle(serverName.getText()); + } + + + String title = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + String summary = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + + if (title != null) { + screen.setTitle(title); + } else { + screen.setTitle(R.string.settings_server_unused); + } + if (summary != null) { + screen.setSummary(summary); + } + + return true; + } else { + return false; + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java new file mode 100644 index 0000000..f4baa44 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java @@ -0,0 +1,1633 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.StatFs; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MediaStoreService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.MenuUtil; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistEntryView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.GridSpacingDecoration; +import github.nvllsvm.audinaut.view.PlaylistSongView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.io.File; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; + +public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + private static final String TAG = SubsonicFragment.class.getSimpleName(); + private static int TAG_INC = 10; + private int tag; + + protected SubsonicActivity context; + protected CharSequence title = null; + protected CharSequence subtitle = null; + protected View rootView; + protected boolean primaryFragment = false; + protected boolean secondaryFragment = false; + protected boolean isOnlyVisible = true; + protected boolean alwaysFullscreen = false; + protected boolean alwaysStartFullscreen = false; + protected boolean invalidated = false; + protected static Random random = new Random(); + protected GestureDetector gestureScanner; + protected boolean artist = false; + protected boolean artistOverride = false; + protected SwipeRefreshLayout refreshLayout; + protected boolean firstRun; + protected MenuItem searchItem; + protected SearchView searchView; + + public SubsonicFragment() { + super(); + tag = TAG_INC++; + } + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + if(bundle != null) { + String name = bundle.getString(Constants.FRAGMENT_NAME); + if(name != null) { + title = name; + } + } + firstRun = true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if(title != null) { + outState.putString(Constants.FRAGMENT_NAME, title.toString()); + } + } + + @Override + public void onResume() { + super.onResume(); + if(firstRun) { + firstRun = false; + } else { + UpdateView.triggerUpdate(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + context = (SubsonicActivity)activity; + } + + public void setContext(SubsonicActivity context) { + this.context = context; + } + + protected void onFinishSetupOptionsMenu(final Menu menu) { + searchItem = menu.findItem(R.id.menu_global_search); + if(searchItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + SearchManager searchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(context.getComponentName()); + if(searchableInfo == null) { + Log.w(TAG, "Failed to get SearchableInfo"); + } else { + searchView.setSearchableInfo(searchableInfo); + } + + String currentQuery = getCurrentQuery(); + if(currentQuery != null) { + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchView.setQuery(getCurrentQuery(), false); + } + }); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_global_shuffle: + onShuffleRequested(); + return true; + case R.id.menu_refresh: + refresh(); + return true; + case R.id.menu_play_now: + playNow(false, false); + return true; + case R.id.menu_play_last: + playNow(false, true); + return true; + case R.id.menu_play_next: + playNow(false, true, true); + return true; + case R.id.menu_shuffle: + playNow(true, false); + return true; + case R.id.menu_download: + downloadBackground(false); + clearSelected(); + return true; + case R.id.menu_cache: + downloadBackground(true); + clearSelected(); + return true; + case R.id.menu_delete: + delete(); + clearSelected(); + return true; + case R.id.menu_add_playlist: + List songs = getSelectedEntries(); + addToPlaylist(songs); + clearSelected(); + return true; + } + + return false; + } + + public void onCreateContextMenuSupport(Menu menu, MenuInflater menuInflater, UpdateView updateView, Object selected) { + if(selected instanceof Entry) { + Entry entry = (Entry) selected; + if (entry.isDirectory()) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_album_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_album_context, menu); + } + } else { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_song_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_song_context, menu); + + + String songPressAction = Util.getSongPressAction(context); + if(!"next".equals(songPressAction) && !"last".equals(songPressAction)) { + menu.setGroupVisible(R.id.hide_play_now, false); + } + } + } + + if(!isShowArtistEnabled() || (!Util.isTagBrowsing(context) && entry.getParent() == null) || (Util.isTagBrowsing(context) && entry.getArtistId() == null)) { + menu.setGroupVisible(R.id.hide_show_artist, false); + } + } else if(selected instanceof Artist) { + Artist artist = (Artist) selected; + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_artist_context_offline, menu); + } else { + menuInflater.inflate(R.menu.select_artist_context, menu); + } + } + + MenuUtil.hideMenuItems(context, menu, updateView); + } + + protected void recreateContextMenu(Menu menu) { + List menuItems = new ArrayList(); + for(int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + if(item.isVisible()) { + menuItems.add(item); + } + } + menu.clear(); + for(int i = 0; i < menuItems.size(); i++) { + MenuItem item = menuItems.get(i); + menu.add(tag, item.getItemId(), Menu.NONE, item.getTitle()); + } + } + + // For reverting specific removals: https://github.com/daneren2005/Subsonic/commit/fbd1a68042dfc3601eaa0a9e37b3957bbdd51420 + public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) { + Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; + Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null; + if(selectedItem instanceof DownloadFile) { + entry = ((DownloadFile) selectedItem).getSong(); + } + List songs = new ArrayList(1); + songs.add(entry); + + switch (menuItem.getItemId()) { + case R.id.artist_menu_play_now: + downloadRecursively(artist.getId(), false, false, true, false, false); + break; + case R.id.artist_menu_play_shuffled: + downloadRecursively(artist.getId(), false, false, true, true, false); + break; + case R.id.artist_menu_play_next: + downloadRecursively(artist.getId(), false, true, false, false, false, true); + break; + case R.id.artist_menu_play_last: + downloadRecursively(artist.getId(), false, true, false, false, false); + break; + case R.id.artist_menu_download: + downloadRecursively(artist.getId(), false, true, false, false, true); + break; + case R.id.artist_menu_pin: + downloadRecursively(artist.getId(), true, true, false, false, true); + break; + case R.id.artist_menu_delete: + deleteRecursively(artist); + break; + case R.id.album_menu_play_now: + artistOverride = true; + downloadRecursively(entry.getId(), false, false, true, false, false); + break; + case R.id.album_menu_play_shuffled: + artistOverride = true; + downloadRecursively(entry.getId(), false, false, true, true, false); + break; + case R.id.album_menu_play_next: + artistOverride = true; + downloadRecursively(entry.getId(), false, true, false, false, false, true); + break; + case R.id.album_menu_play_last: + artistOverride = true; + downloadRecursively(entry.getId(), false, true, false, false, false); + break; + case R.id.album_menu_download: + artistOverride = true; + downloadRecursively(entry.getId(), false, true, false, false, true); + break; + case R.id.album_menu_pin: + artistOverride = true; + downloadRecursively(entry.getId(), true, true, false, false, true); + break; + case R.id.album_menu_delete: + deleteRecursively(entry); + break; + case R.id.album_menu_info: + displaySongInfo(entry); + break; + case R.id.album_menu_show_artist: + showAlbumArtist((Entry) selectedItem); + break; + case R.id.song_menu_play_now: + playNow(songs); + break; + case R.id.song_menu_play_next: + getDownloadService().download(songs, false, false, true, false); + break; + case R.id.song_menu_play_last: + getDownloadService().download(songs, false, false, false, false); + break; + case R.id.song_menu_download: + getDownloadService().downloadBackground(songs, false); + break; + case R.id.song_menu_pin: + getDownloadService().downloadBackground(songs, true); + break; + case R.id.song_menu_delete: + deleteSongs(songs); + break; + case R.id.song_menu_add_playlist: + addToPlaylist(songs); + break; + case R.id.song_menu_info: + displaySongInfo(entry); + break; + case R.id.song_menu_show_album: + showAlbum((Entry) selectedItem); + break; + case R.id.song_menu_show_artist: + showArtist((Entry) selectedItem); + break; + default: + return false; + } + + return true; + } + + public void replaceFragment(SubsonicFragment fragment) { + replaceFragment(fragment, true); + } + public void replaceFragment(SubsonicFragment fragment, boolean replaceCurrent) { + context.replaceFragment(fragment, fragment.getSupportTag(), secondaryFragment && replaceCurrent); + } + public void replaceExistingFragment(SubsonicFragment fragment) { + context.replaceExistingFragment(fragment, fragment.getSupportTag()); + } + public void removeCurrent() { + context.removeCurrent(); + } + + public int getRootId() { + return rootView.getId(); + } + + public void setSupportTag(int tag) { this.tag = tag; } + public void setSupportTag(String tag) { this.tag = Integer.parseInt(tag); } + public int getSupportTag() { + return tag; + } + + public void setPrimaryFragment(boolean primary) { + primaryFragment = primary; + if(primary) { + if(context != null && title != null) { + context.setTitle(title); + context.setSubtitle(subtitle); + } + if(invalidated) { + invalidated = false; + refresh(false); + } + } + } + public void setPrimaryFragment(boolean primary, boolean secondary) { + setPrimaryFragment(primary); + secondaryFragment = secondary; + } + public void setSecondaryFragment(boolean secondary) { + secondaryFragment = secondary; + } + public void setIsOnlyVisible(boolean isOnlyVisible) { + this.isOnlyVisible = isOnlyVisible; + } + public boolean isAlwaysFullscreen() { + return alwaysFullscreen; + } + public boolean isAlwaysStartFullscreen() { + return alwaysStartFullscreen; + } + + public void invalidate() { + if(primaryFragment) { + refresh(true); + } else { + invalidated = true; + } + } + + public DownloadService getDownloadService() { + return context != null ? context.getDownloadService() : null; + } + + protected void refresh() { + refresh(true); + } + protected void refresh(boolean refresh) { + + } + + @Override + public void onRefresh() { + refreshLayout.setRefreshing(false); + refresh(); + } + + public void setProgressVisible(boolean visible) { + View view = rootView.findViewById(R.id.tab_progress); + if (view != null) { + view.setVisibility(visible ? View.VISIBLE : View.GONE); + + if(visible) { + View progress = rootView.findViewById(R.id.tab_progress_spinner); + progress.setVisibility(View.VISIBLE); + } + } + } + + public void updateProgress(String message) { + TextView view = (TextView) rootView.findViewById(R.id.tab_progress_message); + if (view != null) { + view.setText(message); + } + } + + public void setEmpty(boolean empty) { + View view = rootView.findViewById(R.id.tab_progress); + if(empty) { + view.setVisibility(View.VISIBLE); + + View progress = view.findViewById(R.id.tab_progress_spinner); + progress.setVisibility(View.GONE); + + TextView text = (TextView) view.findViewById(R.id.tab_progress_message); + text.setText(R.string.common_empty); + } else { + view.setVisibility(View.GONE); + } + } + + protected synchronized ImageLoader getImageLoader() { + return context.getImageLoader(); + } + public synchronized static ImageLoader getStaticImageLoader(Context context) { + return SubsonicActivity.getStaticImageLoader(context); + } + + public void setTitle(CharSequence title) { + this.title = title; + context.setTitle(title); + } + public void setTitle(int title) { + this.title = context.getResources().getString(title); + context.setTitle(this.title); + } + public void setSubtitle(CharSequence title) { + this.subtitle = title; + context.setSubtitle(title); + } + public CharSequence getTitle() { + return this.title; + } + + protected void setupScrollList(final AbsListView listView) { + if(!context.isTouchscreen()) { + refreshLayout.setEnabled(false); + } else { + listView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int topRowVerticalPosition = (listView.getChildCount() == 0) ? 0 : listView.getChildAt(0).getTop(); + refreshLayout.setEnabled(topRowVerticalPosition >= 0 && listView.getFirstVisiblePosition() == 0); + } + }); + } + } + protected void setupScrollList(final RecyclerView recyclerView) { + if(!context.isTouchscreen()) { + refreshLayout.setEnabled(false); + } else { + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + refreshLayout.setEnabled(!recyclerView.canScrollVertically(-1)); + } + }); + } + } + + public void setupLayoutManager(RecyclerView recyclerView, boolean largeAlbums) { + recyclerView.setLayoutManager(getLayoutManager(recyclerView, largeAlbums)); + } + public RecyclerView.LayoutManager getLayoutManager(RecyclerView recyclerView, boolean largeCells) { + if(largeCells) { + return getGridLayoutManager(recyclerView); + } else { + return getLinearLayoutManager(); + } + } + public GridLayoutManager getGridLayoutManager(RecyclerView recyclerView) { + final int columns = getRecyclerColumnCount(); + GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); + + GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); + if(spanSizeLookup != null) { + gridLayoutManager.setSpanSizeLookup(spanSizeLookup); + } + RecyclerView.ItemDecoration itemDecoration = getItemDecoration(); + if(itemDecoration != null) { + recyclerView.addItemDecoration(itemDecoration); + } + return gridLayoutManager; + } + public LinearLayoutManager getLinearLayoutManager() { + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + return layoutManager; + } + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + int viewType = adapter.getItemViewType(position); + if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } else { + return 1; + } + } + }; + } + public RecyclerView.ItemDecoration getItemDecoration() { + return new GridSpacingDecoration(); + } + public int getRecyclerColumnCount() { + if(isOnlyVisible) { + return context.getResources().getInteger(R.integer.Grid_FullScreen_Columns); + } else { + return context.getResources().getInteger(R.integer.Grid_Columns); + } + } + + protected void warnIfStorageUnavailable() { + if (!Util.isExternalStoragePresent()) { + Util.toast(context, R.string.select_album_no_sdcard); + } + + try { + StatFs stat = new StatFs(FileUtil.getMusicDirectory(context).getPath()); + long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + if (bytesAvailableFs < 50000000L) { + Util.toast(context, context.getResources().getString(R.string.select_album_no_room, Util.formatBytes(bytesAvailableFs))); + } + } catch(Exception e) { + Log.w(TAG, "Error while checking storage space for music directory", e); + } + } + + protected void onShuffleRequested() { + if(Util.isOffline(context)) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); + context.openNowPlaying(); + return; + } + + View dialogView = context.getLayoutInflater().inflate(R.layout.shuffle_dialog, null); + final EditText startYearBox = (EditText)dialogView.findViewById(R.id.start_year); + final EditText endYearBox = (EditText)dialogView.findViewById(R.id.end_year); + final EditText genreBox = (EditText)dialogView.findViewById(R.id.genre); + final Button genreCombo = (Button)dialogView.findViewById(R.id.genre_combo); + + final SharedPreferences prefs = Util.getPreferences(context); + final String oldStartYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""); + final String oldEndYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, ""); + final String oldGenre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""); + + boolean _useCombo = false; + genreBox.setVisibility(View.GONE); + genreCombo.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + new LoadingTask>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getGenres(false, context, this); + } + + @Override + protected void done(final List genres) { + List names = new ArrayList(); + String blank = context.getResources().getString(R.string.select_genre_blank); + names.add(blank); + for(Genre genre: genres) { + names.add(genre.getName()); + } + final List finalNames = names; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.shuffle_pick_genre) + .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if(which == 0) { + genreCombo.setText(""); + } else { + genreCombo.setText(finalNames.get(which)); + } + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + }); + _useCombo = true; + final boolean useCombo = _useCombo; + + startYearBox.setText(oldStartYear); + endYearBox.setText(oldEndYear); + genreBox.setText(oldGenre); + genreCombo.setText(oldGenre); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.shuffle_title) + .setView(dialogView) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + String genre; + if (useCombo) { + genre = genreCombo.getText().toString(); + } else { + genre = genreBox.getText().toString(); + } + String startYear = startYearBox.getText().toString(); + String endYear = endYearBox.getText().toString(); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear); + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear); + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); + editor.commit(); + + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); + context.openNowPlaying(); + } + }) + .setNegativeButton(R.string.common_cancel, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { + downloadRecursively(id, "", true, save, append, autoplay, shuffle, background); + } + protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) { + downloadRecursively(id, "", true, save, append, autoplay, shuffle, background, playNext); + } + protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { + downloadRecursively(id, name, false, save, append, autoplay, shuffle, background); + } + + protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { + downloadRecursively(id, name, isDirectory, save, append, autoplay, shuffle, background, false); + } + + protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory root; + if(isDirectory && id != null) { + root = getMusicDirectory(id, name, false, musicService, this); + } else { + root = musicService.getPlaylist(true, id, name, context, this); + } + + boolean shuffleByAlbum = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_BY_ALBUM, true); + if(shuffle && shuffleByAlbum) { + Collections.shuffle(root.getChildren()); + } + + songs = new LinkedList(); + getSongsRecursively(root, songs); + + if(shuffle && !shuffleByAlbum) { + Collections.shuffle(songs); + } + + DownloadService downloadService = getDownloadService(); + boolean transition = false; + if (!songs.isEmpty() && downloadService != null) { + // Conditions for a standard play now operation + if(!append && !save && autoplay && !playNext && !shuffle && !background) { + playNowOverride = true; + return false; + } + + if (!append && !background) { + downloadService.clear(); + } + if(!background) { + downloadService.download(songs, save, autoplay, playNext, false); + if(!append) { + transition = true; + } + } + else { + downloadService.downloadBackground(songs, save); + } + } + artistOverride = false; + + return transition; + } + }.execute(); + } + + protected void downloadRecursively(final List albums, final boolean shuffle, final boolean append, final boolean playNext) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(context); + + if(shuffle) { + Collections.shuffle(albums); + } + + songs = new LinkedList(); + MusicDirectory root = new MusicDirectory(); + root.addChildren(albums); + getSongsRecursively(root, songs); + + DownloadService downloadService = getDownloadService(); + boolean transition = false; + if (!songs.isEmpty() && downloadService != null) { + // Conditions for a standard play now operation + if(!append && !shuffle) { + playNowOverride = true; + return false; + } + + if (!append) { + downloadService.clear(); + } + + downloadService.download(songs, false, true, playNext, false); + if(!append) { + transition = true; + } + } + artistOverride = false; + + return transition; + } + }.execute(); + } + + protected MusicDirectory getMusicDirectory(String id, String name, boolean refresh, MusicService service, ProgressListener listener) throws Exception { + return getMusicDirectory(id, name, refresh, false, service, listener); + } + protected MusicDirectory getMusicDirectory(String id, String name, boolean refresh, boolean forceArtist, MusicService service, ProgressListener listener) throws Exception { + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + if(artist && !artistOverride || forceArtist) { + return service.getArtist(id, name, refresh, context, listener); + } else { + return service.getAlbum(id, name, refresh, context, listener); + } + } else { + return service.getMusicDirectory(id, name, refresh, context, listener); + } + } + + protected void addToPlaylist(final List songs) { + Iterator it = songs.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + if(entry.isDirectory()) { + it.remove(); + } + } + + if(songs.isEmpty()) { + Util.toast(context, "No songs selected"); + return; + } + + new LoadingTask>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + List playlists = new ArrayList(); + playlists.addAll(musicService.getPlaylists(false, context, this)); + + // Iterate through and remove all non owned public playlists + Iterator it = playlists.iterator(); + while(it.hasNext()) { + Playlist playlist = it.next(); + if(playlist.getPublic() == true && playlist.getId().indexOf(".m3u") == -1 && !UserUtil.getCurrentUsername(context).equals(playlist.getOwner())) { + it.remove(); + } + } + + return playlists; + } + + @Override + protected void done(final List playlists) { + // Create adapter to show playlists + Playlist createNew = new Playlist("-1", context.getResources().getString(R.string.playlist_create_new)); + playlists.add(0, createNew); + ArrayAdapter playlistAdapter = new ArrayAdapter(context, R.layout.basic_count_item, playlists) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Playlist playlist = getItem(position); + + // Create new if not getting a convert view to use + PlaylistSongView view; + if(convertView instanceof PlaylistSongView) { + view = (PlaylistSongView) convertView; + } else { + view = new PlaylistSongView(context); + } + + view.setObject(playlist, songs); + + return view; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.playlist_add_to) + .setAdapter(playlistAdapter, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which > 0) { + addToPlaylist(playlists.get(which), songs); + } else { + createNewPlaylist(songs, false); + } + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + private void addToPlaylist(final Playlist playlist, final List songs) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.addToPlaylist(playlist.getId(), songs, context, null); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName())); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + protected void createNewPlaylist(final List songs, final boolean getSuggestion) { + View layout = context.getLayoutInflater().inflate(R.layout.save_playlist, null); + final EditText playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name); + final CheckBox overwriteCheckBox = (CheckBox) layout.findViewById(R.id.save_playlist_overwrite); + if(getSuggestion) { + String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null; + if (playlistName != null) { + playlistNameView.setText(playlistName); + try { + if(Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) { + overwriteCheckBox.setChecked(true); + overwriteCheckBox.setVisibility(View.VISIBLE); + } + } catch(Exception e) { + Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet"); + } + } else { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + playlistNameView.setText(dateFormat.format(new Date())); + } + } else { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + playlistNameView.setText(dateFormat.format(new Date())); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.download_playlist_title) + .setMessage(R.string.download_playlist_name) + .setView(layout) + .setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + String playlistName = String.valueOf(playlistNameView.getText()); + if(overwriteCheckBox.isChecked()) { + overwritePlaylist(songs, playlistName, getDownloadService().getSuggestedPlaylistId()); + } else { + createNewPlaylist(songs, playlistName); + + if(getSuggestion) { + DownloadService downloadService = getDownloadService(); + if(downloadService != null) { + downloadService.setSuggestedPlaylistName(playlistName, null); + } + } + } + } + }) + .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }) + .setCancelable(true); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + private void createNewPlaylist(final List songs, final String name) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.createPlaylist(null, name, songs, context, null); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, R.string.download_playlist_done); + } + + @Override + protected void error(Throwable error) { + String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); + Util.toast(context, msg); + } + }.execute(); + } + private void overwritePlaylist(final List songs, final String name, final String id) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory playlist = musicService.getPlaylist(true, id, name, context, null); + List toDelete = playlist.getChildren(); + musicService.overwritePlaylist(id, name, toDelete.size(), songs, context, null); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, R.string.download_playlist_done); + } + + @Override + protected void error(Throwable error) { + String msg; + if (error instanceof OfflineException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + public void displaySongInfo(final Entry song) { + Integer duration = null; + Integer bitrate = null; + String format = null; + long size = 0; + if(!song.isDirectory()) { + try { + DownloadFile downloadFile = new DownloadFile(context, song, false); + File file = downloadFile.getCompleteFile(); + if(file.exists()) { + MediaMetadataRetriever metadata = new MediaMetadataRetriever(); + metadata.setDataSource(file.getAbsolutePath()); + + String tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + duration = Integer.parseInt((tmp != null) ? tmp : "0") / 1000; + format = FileUtil.getExtension(file.getName()); + size = file.length(); + + // If no duration try to read bitrate tag + if(duration == null) { + tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE); + bitrate = Integer.parseInt((tmp != null) ? tmp : "0") / 1000; + } else { + // Otherwise do a calculation for it + // Divide by 1000 so in kbps + bitrate = (int) (size / duration) / 1000 * 8; + } + + if(Util.isOffline(context)) { + song.setGenre(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)); + String year = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR); + song.setYear(Integer.parseInt((year != null) ? year : "0")); + } + } + } catch(Exception e) { + Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver"); + } + } + if(duration == null) { + duration = song.getDuration(); + } + + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + if(!song.isDirectory()) { + headers.add(R.string.details_title); + details.add(song.getTitle()); + } + + if(song.getArtist() != null && !"".equals(song.getArtist())) { + headers.add(R.string.details_artist); + details.add(song.getArtist()); + } + if(song.getAlbum() != null && !"".equals(song.getAlbum())) { + headers.add(R.string.details_album); + details.add(song.getAlbum()); + } + if(song.getTrack() != null && song.getTrack() != 0) { + headers.add(R.string.details_track); + details.add(Integer.toString(song.getTrack())); + } + if(song.getGenre() != null && !"".equals(song.getGenre())) { + headers.add(R.string.details_genre); + details.add(song.getGenre()); + } + if(song.getYear() != null && song.getYear() != 0) { + headers.add(R.string.details_year); + details.add(Integer.toString(song.getYear())); + } + if(!Util.isOffline(context) && song.getSuffix() != null) { + headers.add(R.string.details_server_format); + details.add(song.getSuffix()); + + if(song.getBitRate() != null && song.getBitRate() != 0) { + headers.add(R.string.details_server_bitrate); + details.add(song.getBitRate() + " kbps"); + } + } + if(format != null && !"".equals(format)) { + headers.add(R.string.details_cached_format); + details.add(format); + } + if(bitrate != null && bitrate != 0) { + headers.add(R.string.details_cached_bitrate); + details.add(bitrate + " kbps"); + } + if(size != 0) { + headers.add(R.string.details_size); + details.add(Util.formatLocalizedBytes(size, context)); + } + if(duration != null && duration != 0) { + headers.add(R.string.details_length); + details.add(Util.formatDuration(duration)); + } + + try { + Long[] dates = SongDBHandler.getHandler(context).getLastPlayed(song); + if(dates != null && dates[0] != null && dates[0] > 0) { + headers.add(R.string.details_last_played); + details.add(Util.formatDate((dates[1] != null && dates[1] > dates[0]) ? dates[1] : dates[0])); + } + } catch(Exception e) { + Log.e(TAG, "Failed to get last played", e); + } + + int title; + if(song.isDirectory()) { + title = R.string.details_title_album; + } else { + title = R.string.details_title_song; + } + Util.showDetailsDialog(context, title, headers, details); + } + + + protected boolean entryExists(Entry entry) { + DownloadFile check = new DownloadFile(context, entry, false); + return check.isCompleteFileAvailable(); + } + + public void deleteRecursively(Artist artist) { + deleteRecursively(artist, FileUtil.getArtistDirectory(context, artist)); + } + + public void deleteRecursively(Entry album) { + deleteRecursively(album, FileUtil.getAlbumDirectory(context, album)); + } + + public void deleteRecursively(final Object remove, final File dir) { + if(dir == null) { + return; + } + + new LoadingTask(context) { + @Override + protected Void doInBackground() throws Throwable { + MediaStoreService mediaStore = new MediaStoreService(context); + FileUtil.recursiveDelete(dir, mediaStore); + return null; + } + + @Override + protected void done(Void result) { + if(Util.isOffline(context)) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + adapter.removeItem(remove); + } else { + refresh(); + } + } else { + UpdateView.triggerUpdate(); + } + } + }.execute(); + } + public void deleteSongs(final List songs) { + new LoadingTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().delete(songs); + return null; + } + + @Override + protected void done(Void result) { + if(Util.isOffline(context)) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + for(Entry song: songs) { + adapter.removeItem(song); + } + } else { + refresh(); + } + } else { + UpdateView.triggerUpdate(); + } + } + }.execute(); + } + + public void showAlbumArtist(Entry entry) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + if(Util.isTagBrowsing(context)) { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId()); + } else { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent()); + } + args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment, true); + } + public void showArtist(Entry entry) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + if(Util.isTagBrowsing(context)) { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId()); + } else { + if(entry.getGrandParent() == null) { + args.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getParent()); + } else { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getGrandParent()); + } + } + args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment, true); + } + + public void showAlbum(Entry entry) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + if(Util.isTagBrowsing(context)) { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getAlbumId()); + } else { + args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent()); + } + args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum()); + fragment.setArguments(args); + + replaceFragment(fragment, true); + } + + public GestureDetector getGestureDetector() { + return gestureScanner; + } + + protected void onSongPress(List entries, Entry entry) { + onSongPress(entries, entry, 0, true); + } + protected void onSongPress(List entries, Entry entry, boolean allowPlayAll) { + onSongPress(entries, entry, 0, allowPlayAll); + } + protected void onSongPress(List entries, Entry entry, int position, boolean allowPlayAll) { + List songs = new ArrayList(); + + String songPressAction = Util.getSongPressAction(context); + if("all".equals(songPressAction) && allowPlayAll) { + for(Entry song: entries) { + if(!song.isDirectory()) { + songs.add(song); + } + } + playNow(songs, entry, position); + } else if("next".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, true, false); + } else if("last".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, false, false); + } else { + songs.add(entry); + playNow(songs); + } + } + + protected void playNow(List entries) { + playNow(entries, null, null); + } + protected void playNow(final List entries, final String playlistName, final String playlistId) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, songs); + return null; + } + + @Override + protected void done(Boolean result) { + playNow(songs, 0, playlistName, playlistId); + } + }.execute(); + } + protected void playNow(List entries, int position) { + playNow(entries, position, null, null); + } + protected void playNow(List entries, int position, String playlistName, String playlistId) { + Entry selected = entries.isEmpty() ? null : entries.get(0); + playNow(entries, selected, position, playlistName, playlistId); + } + + protected void playNow(List entries, Entry song, int position) { + playNow(entries, song, position, null, null); + } + + protected void playNow(final List entries, final Entry song, final int position, final String playlistName, final String playlistId) { + new LoadingTask(context) { + @Override + protected Void doInBackground() throws Throwable { + playNowInTask(entries, song, position, playlistName, playlistId); + return null; + } + + @Override + protected void done(Void result) { + context.openNowPlaying(); + } + }.execute(); + } + protected void playNowInTask(final List entries, final Entry song, final int position) { + playNowInTask(entries, song, position, null, null); + } + protected void playNowInTask(final List entries, final Entry song, final int position, final String playlistName, final String playlistId) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } + + public SectionAdapter getCurrentAdapter() { return null; } + public void stopActionMode() { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + adapter.stopActionMode(); + } + } + protected void clearSelected() { + if(getCurrentAdapter() != null) { + getCurrentAdapter().clearSelected(); + } + } + protected List getSelectedEntries() { + return getCurrentAdapter().getSelected(); + } + + protected void playNow(final boolean shuffle, final boolean append) { + playNow(shuffle, append, false); + } + protected void playNow(final boolean shuffle, final boolean append, final boolean playNext) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + download(songs, append, false, !append, playNext, shuffle); + clearSelected(); + } + } + + protected void download(List entries, boolean append, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(entries, append, save, autoplay, playNext, shuffle, null, null); + } + protected void download(final List entries, final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle, final String playlistName, final String playlistId) { + final DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + warnIfStorageUnavailable(); + + // Conditions for using play now button + if(!append && !save && autoplay && !playNext && !shuffle) { + // Call playNow which goes through and tries to use information + playNow(entries, playlistName, playlistId); + return; + } + + RecursiveLoader onValid = new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + if (!append) { + getDownloadService().clear(); + } + getSongsRecursively(entries, songs); + + downloadService.download(songs, save, autoplay, playNext, shuffle); + if (playlistName != null) { + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } else { + downloadService.setSuggestedPlaylistName(null, null); + } + return null; + } + + @Override + protected void done(Boolean result) { + if (autoplay) { + context.openNowPlaying(); + } else if (save) { + Util.toast(context, + context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); + } else if (append) { + Util.toast(context, + context.getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size())); + } + } + }; + + executeOnValid(onValid); + } + protected void executeOnValid(RecursiveLoader onValid) { + onValid.execute(); + } + protected void downloadBackground(final boolean save) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + downloadBackground(save, songs); + } + } + + protected void downloadBackground(final boolean save, final List entries) { + if (getDownloadService() == null) { + return; + } + + warnIfStorageUnavailable(); + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, true); + getDownloadService().downloadBackground(songs, save); + return null; + } + + @Override + protected void done(Boolean result) { + Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); + } + }.execute(); + } + + protected void delete() { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + DownloadService downloadService = getDownloadService(); + if(downloadService != null) { + downloadService.delete(songs); + } + } + } + + protected boolean isShowArtistEnabled() { + return false; + } + + protected String getCurrentQuery() { + return null; + } + + public abstract class RecursiveLoader extends LoadingTask { + protected MusicService musicService; + protected static final int MAX_SONGS = 500; + protected boolean playNowOverride = false; + protected List songs = new ArrayList<>(); + + public RecursiveLoader(Activity context) { + super(context); + musicService = MusicServiceFactory.getMusicService(context); + } + + protected void getSiblingsRecursively(Entry entry) throws Exception { + MusicDirectory parent = new MusicDirectory(); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + parent.setId(entry.getAlbumId()); + } else { + parent.setId(entry.getParent()); + } + + if(parent.getId() == null) { + songs.add(entry); + } else { + MusicDirectory.Entry dir = new Entry(parent.getId()); + dir.setDirectory(true); + parent.addChild(dir); + getSongsRecursively(parent, songs); + } + } + protected void getSongsRecursively(List entry) throws Exception { + getSongsRecursively(entry, false); + } + protected void getSongsRecursively(List entry, boolean allowVideo) throws Exception { + getSongsRecursively(entry, songs, allowVideo); + } + protected void getSongsRecursively(List entry, List songs) throws Exception { + getSongsRecursively(entry, songs, false); + } + protected void getSongsRecursively(List entry, List songs, boolean allowVideo) throws Exception { + MusicDirectory dir = new MusicDirectory(); + dir.addChildren(entry); + getSongsRecursively(dir, songs, allowVideo); + } + + protected void getSongsRecursively(MusicDirectory parent, List songs) throws Exception { + getSongsRecursively(parent, songs, false); + } + protected void getSongsRecursively(MusicDirectory parent, List songs, boolean allowVideo) throws Exception { + if (songs.size() > MAX_SONGS) { + return; + } + + for (Entry dir : parent.getChildren(true, false)) { + MusicDirectory musicDirectory; + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this); + } else { + musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this); + } + getSongsRecursively(musicDirectory, songs); + } + + for (Entry song : parent.getChildren(false, true)) { + songs.add(song); + } + } + + @Override + protected void done(Boolean result) { + warnIfStorageUnavailable(); + + if(playNowOverride) { + playNow(songs); + return; + } + + if(result) { + context.openNowPlaying(); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java new file mode 100644 index 0000000..0739aa5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java @@ -0,0 +1,209 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Util; + +/** + * Provides search suggestions based on recent searches. + * + * @author Sindre Mehus + */ +public class AudinautSearchProvider extends ContentProvider { + private static final String TAG = AudinautSearchProvider.class.getSimpleName(); + + private static final String RESOURCE_PREFIX = "android.resource://github.nvllsvm.audinaut/"; + private static final String[] COLUMNS = {"_id", + SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_INTENT_DATA, + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, + SearchManager.SUGGEST_COLUMN_ICON_1}; + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if(selectionArgs[0].isEmpty()) { + return null; + } + + String query = selectionArgs[0] + "*"; + SearchResult searchResult = search(query); + return createCursor(selectionArgs[0], searchResult); + } + + private SearchResult search(String query) { + MusicService musicService = MusicServiceFactory.getMusicService(getContext()); + if (musicService == null) { + return null; + } + + try { + return musicService.search(new SearchCritera(query, 5, 10, 10), getContext(), null); + } catch (Exception e) { + return null; + } + } + + private Cursor createCursor(String query, SearchResult searchResult) { + MatrixCursor cursor = new MatrixCursor(COLUMNS); + if (searchResult == null) { + return cursor; + } + + // Add all results into one pot + List results = new ArrayList(); + results.addAll(searchResult.getArtists()); + results.addAll(searchResult.getAlbums()); + results.addAll(searchResult.getSongs()); + + // For each, calculate its string distance to the query + for(Object obj: results) { + if(obj instanceof Artist) { + Artist artist = (Artist) obj; + artist.setCloseness(Util.getStringDistance(query, artist.getName())); + } else { + MusicDirectory.Entry entry = (MusicDirectory.Entry) obj; + entry.setCloseness(Util.getStringDistance(query, entry.getTitle())); + } + } + + // Sort based on the closeness paramater + Collections.sort(results, new Comparator() { + @Override + public int compare(Object lhs, Object rhs) { + // Get the closeness of the two objects + int left, right; + boolean leftArtist = lhs instanceof Artist; + boolean rightArtist = rhs instanceof Artist; + if (leftArtist) { + left = ((Artist) lhs).getCloseness(); + } else { + left = ((MusicDirectory.Entry) lhs).getCloseness(); + } + if (rightArtist) { + right = ((Artist) rhs).getCloseness(); + } else { + right = ((MusicDirectory.Entry) rhs).getCloseness(); + } + + if (left == right) { + if(leftArtist && rightArtist) { + return 0; + } else if(leftArtist) { + return -1; + } else if(rightArtist) { + return 1; + } else { + return 0; + } + } else if (left > right) { + return 1; + } else { + return -1; + } + } + }); + + // Done sorting, add results to cursor + for(Object obj: results) { + if(obj instanceof Artist) { + Artist artist = (Artist) obj; + String icon = RESOURCE_PREFIX + R.drawable.ic_action_artist; + cursor.addRow(new Object[]{artist.getId().hashCode(), artist.getName(), null, "ar-" + artist.getId(), artist.getName(), icon}); + } else { + MusicDirectory.Entry entry = (MusicDirectory.Entry) obj; + + if(entry.isDirectory()) { + String icon = RESOURCE_PREFIX + R.drawable.ic_action_album; + cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), entry.getId(), entry.getTitle(), icon}); + } else { + String icon = RESOURCE_PREFIX + R.drawable.ic_action_song; + String id; + if(Util.isTagBrowsing(getContext())) { + id = entry.getAlbumId(); + } else { + id = entry.getParent(); + } + + String artistDisplay; + if(entry.getArtist() == null) { + if(entry.getAlbum() != null) { + artistDisplay = entry.getAlbumDisplay(); + } else { + artistDisplay = ""; + } + } else if(entry.getAlbum() != null) { + artistDisplay = entry.getArtist() + " - " + entry.getAlbumDisplay(); + } else { + artistDisplay = entry.getArtist(); + } + + cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), artistDisplay, "so-" + id, entry.getTitle(), icon}); + } + } + } + return cursor; + } + + @Override + public boolean onCreate() { + return false; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + return null; + } + + @Override + public int delete(Uri uri, String s, String[] strings) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { + return 0; + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.java new file mode 100644 index 0000000..5a81e2a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.java @@ -0,0 +1,28 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x1 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x1; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java new file mode 100644 index 0000000..df42c92 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java @@ -0,0 +1,28 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x2 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x2; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java new file mode 100644 index 0000000..ad9f6da --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java @@ -0,0 +1,28 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x3 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x3; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java new file mode 100644 index 0000000..8c7ef02 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java @@ -0,0 +1,28 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x4 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x4; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java new file mode 100644 index 0000000..30341c3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java @@ -0,0 +1,305 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Environment; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadServiceLifecycleSupport; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; + +/** + * Simple widget to show currently playing album art along + * with play/pause and next track buttons. + *

+ * Based on source code from the stock Android Music app. + * + * @author Sindre Mehus + */ +public class AudinautWidgetProvider extends AppWidgetProvider { + private static final String TAG = AudinautWidgetProvider.class.getSimpleName(); + private static AudinautWidget4x1 instance4x1; + private static AudinautWidget4x2 instance4x2; + private static AudinautWidget4x3 instance4x3; + private static AudinautWidget4x4 instance4x4; + + public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) { + if(instance4x1 == null) { + instance4x1 = new AudinautWidget4x1(); + } + if(instance4x2 == null) { + instance4x2 = new AudinautWidget4x2(); + } + if(instance4x3 == null) { + instance4x3 = new AudinautWidget4x3(); + } + if(instance4x4 == null) { + instance4x4 = new AudinautWidget4x4(); + } + + instance4x1.notifyChange(context, service, playing); + instance4x2.notifyChange(context, service, playing); + instance4x3.notifyChange(context, service, playing); + instance4x4.notifyChange(context, service, playing); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + defaultAppWidget(context, appWidgetIds); + } + + @Override + public void onEnabled(Context context) { + notifyInstances(context, DownloadService.getInstance(), false); + } + + protected int getLayout() { + return 0; + } + + /** + * Initialize given widgets to default state, where we launch Subsonic on default click + * and hide actions if service not running. + */ + private void defaultAppWidget(Context context, int[] appWidgetIds) { + final Resources res = context.getResources(); + final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout()); + + views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text)); + if(getLayout() == R.layout.appwidget4x2) { + views.setTextViewText(R.id.album, ""); + } + + linkButtons(context, views, false); + performUpdate(context, null, appWidgetIds, false); + } + + private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) { + // Update specific list of appWidgetIds if given, otherwise default to all + final AppWidgetManager manager = AppWidgetManager.getInstance(context); + if (appWidgetIds != null) { + manager.updateAppWidget(appWidgetIds, views); + } else { + manager.updateAppWidget(new ComponentName(context, this.getClass()), views); + } + } + + /** + * Handle a change notification coming over from {@link DownloadService} + */ + public void notifyChange(Context context, DownloadService service, boolean playing) { + if (hasInstances(context)) { + performUpdate(context, service, null, playing); + } + } + + /** + * Check against {@link AppWidgetManager} if there are any instances of this widget. + */ + private boolean hasInstances(Context context) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass())); + return (appWidgetIds.length > 0); + } + + /** + * Update all active widget instances by pushing changes + */ + private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) { + final Resources res = context.getResources(); + final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout()); + + if(playing) { + views.setViewVisibility(R.id.widget_root, View.VISIBLE); + } else { + // Hide widget + SharedPreferences prefs = Util.getPreferences(context); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_HIDE_WIDGET, false)) { + views.setViewVisibility(R.id.widget_root, View.GONE); + } + } + + // Get Entry from current playing DownloadFile + MusicDirectory.Entry currentPlaying = null; + if(service == null) { + // Deserialize from playling list to setup + try { + PlayerQueue state = FileUtil.deserialize(context, DownloadServiceLifecycleSupport.FILENAME_DOWNLOADS_SER, PlayerQueue.class); + if (state != null && state.currentPlayingIndex != -1) { + currentPlaying = state.songs.get(state.currentPlayingIndex); + } + } catch(Exception e) { + Log.e(TAG, "Failed to grab current playing", e); + } + } else { + currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong(); + } + + String title = currentPlaying == null ? null : currentPlaying.getTitle(); + CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist(); + CharSequence album = currentPlaying == null ? null : currentPlaying.getAlbum(); + CharSequence errorState = null; + + // Show error message? + String status = Environment.getExternalStorageState(); + if (status.equals(Environment.MEDIA_SHARED) || + status.equals(Environment.MEDIA_UNMOUNTED)) { + errorState = res.getText(R.string.widget_sdcard_busy); + } else if (status.equals(Environment.MEDIA_REMOVED)) { + errorState = res.getText(R.string.widget_sdcard_missing); + } else if (currentPlaying == null) { + errorState = res.getText(R.string.widget_initial_text); + } + + if (errorState != null) { + // Show error state to user + views.setTextViewText(R.id.title,null); + views.setTextViewText(R.id.artist, errorState); + views.setTextViewText(R.id.album, ""); + if(getLayout() != R.layout.appwidget4x1) { + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default); + } + } else { + // No error, so show normal titles + views.setTextViewText(R.id.title, title); + views.setTextViewText(R.id.artist, artist); + if(getLayout() != R.layout.appwidget4x1) { + views.setTextViewText(R.id.album, album); + } + } + + // Set correct drawable for pause state + if (playing) { + views.setImageViewResource(R.id.control_play, R.drawable.media_pause_dark); + } else { + views.setImageViewResource(R.id.control_play, R.drawable.media_start_dark); + } + + // Set the cover art + try { + boolean large = false; + if(getLayout() != R.layout.appwidget4x1 && getLayout() != R.layout.appwidget4x2) { + large = true; + } + ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context); + Bitmap bitmap = imageLoader == null ? null : imageLoader.getCachedImage(context, currentPlaying, large); + + if (bitmap == null) { + // Set default cover art + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown); + } else { + bitmap = getRoundedCornerBitmap(bitmap); + views.setImageViewBitmap(R.id.appwidget_coverart, bitmap); + } + } catch (Exception x) { + Log.e(TAG, "Failed to load cover art", x); + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown); + } + + // Link actions buttons to intents + linkButtons(context, views, currentPlaying != null); + + pushUpdate(context, appWidgetIds, views); + } + + /** + * Round the corners of a bitmap for the cover art image + */ + private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) { + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final float roundPx = 10; + + // Add extra width to the rect so the right side wont be rounded. + final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight()); + final RectF rectF = new RectF(rect); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawRoundRect(rectF, roundPx, roundPx, paint); + + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + + /** + * Link up various button actions using {@link PendingIntent}. + * + * @param playerActive @param playerActive True if player is active in background. Launch {@link github.nvllsvm.audinaut.activity.SubsonicFragmentActivity}. + */ + private void linkButtons(Context context, RemoteViews views, boolean playerActive) { + Intent intent = new Intent(context, SubsonicFragmentActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); + views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); + + // Emulate media button clicks. + intent = new Intent("Audinaut.PLAY_PAUSE"); + intent.setComponent(new ComponentName(context, DownloadService.class)); + intent.setAction(DownloadService.CMD_TOGGLEPAUSE); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_play, pendingIntent); + + intent = new Intent("Audinaut.NEXT"); // Use a unique action name to ensure a different PendingIntent to be created. + intent.setComponent(new ComponentName(context, DownloadService.class)); + intent.setAction(DownloadService.CMD_NEXT); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_next, pendingIntent); + + intent = new Intent("Audinaut.PREVIOUS"); // Use a unique action name to ensure a different PendingIntent to be created. + intent.setComponent(new ComponentName(context, DownloadService.class)); + intent.setAction(DownloadService.CMD_PREVIOUS); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/MostRecentStubProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/MostRecentStubProvider.java new file mode 100644 index 0000000..a8c0289 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/MostRecentStubProvider.java @@ -0,0 +1,61 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/** + * Created by Scott on 8/28/13. + */ + +public class MostRecentStubProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return ""; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/PlaylistStubProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/PlaylistStubProvider.java new file mode 100644 index 0000000..aa841ed --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/PlaylistStubProvider.java @@ -0,0 +1,61 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/** + * Created by Scott on 8/28/13. + */ + +public class PlaylistStubProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return ""; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java new file mode 100644 index 0000000..0009b53 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java @@ -0,0 +1,47 @@ +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import github.nvllsvm.audinaut.service.DownloadService; + +public class A2dpIntentReceiver extends BroadcastReceiver { + private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; + private String TAG = A2dpIntentReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "GOT INTENT " + intent); + + DownloadService downloadService = DownloadService.getInstance(); + + if (downloadService != null){ + + Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); + + avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration()); + avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition()); + avrcpIntent.putExtra("ListSize", (long) downloadService.getSongs().size()); + + switch (downloadService.getPlayerState()){ + case STARTED: + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + avrcpIntent.putExtra("playing", false); + break; + case PAUSED: + avrcpIntent.putExtra("playing", false); + break; + case COMPLETED: + avrcpIntent.putExtra("playing", false); + break; + default: + return; + } + + context.sendBroadcast(avrcpIntent); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java new file mode 100644 index 0000000..302598a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java @@ -0,0 +1,51 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.util.Log; + +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +public class AudioNoisyReceiver extends BroadcastReceiver { + private static final String TAG = AudioNoisyReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + DownloadService downloadService = DownloadService.getInstance(); + // Don't do anything if downloadService is not started + if(downloadService == null) { + return; + } + + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals (intent.getAction ())) { + if((downloadService.getPlayerState() == PlayerState.STARTED || downloadService.getPlayerState() == PlayerState.PAUSED_TEMP)) { + SharedPreferences prefs = Util.getPreferences(downloadService); + int pausePref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT, "0")); + if(pausePref == 0) { + downloadService.pause(); + } + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/BootReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/BootReceiver.java new file mode 100644 index 0000000..ba5915f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/BootReceiver.java @@ -0,0 +1,34 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.util.Util; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if(Util.shouldStartOnHeadphones(context)) { + Intent serviceIntent = new Intent(); + serviceIntent.setClassName(context.getPackageName(), HeadphoneListenerService.class.getName()); + context.startService(serviceIntent); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java new file mode 100644 index 0000000..a7c0e5c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java @@ -0,0 +1,40 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Util; + +public class HeadphonePlugReceiver extends BroadcastReceiver { + private static final String TAG = HeadphonePlugReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if(Intent.ACTION_HEADSET_PLUG.equals(intent.getAction())) { + int headphoneState = intent.getIntExtra("state", -1); + if(headphoneState == 1 && Util.shouldStartOnHeadphones(context)) { + Intent start = new Intent(context, DownloadService.class); + start.setAction(DownloadService.START_PLAY); + context.startService(start); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java new file mode 100644 index 0000000..a172322 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java @@ -0,0 +1,57 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; + +import github.nvllsvm.audinaut.service.DownloadService; + +/** + * @author Sindre Mehus + */ +public class MediaButtonIntentReceiver extends BroadcastReceiver { + + private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + if(DownloadService.getInstance() == null && (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_STOP || + event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) { + Log.w(TAG, "Ignore keycode event because downloadService is off"); + return; + } + Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); + + Intent serviceIntent = new Intent(context, DownloadService.class); + serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + context.startService(serviceIntent); + if (isOrderedBroadcast()) { + try { + abortBroadcast(); + } catch (Exception x) { + // Ignored. + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/PlayActionReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/PlayActionReceiver.java new file mode 100644 index 0000000..f05ed16 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/PlayActionReceiver.java @@ -0,0 +1,46 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; + +public class PlayActionReceiver extends BroadcastReceiver { + private static final String TAG = PlayActionReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if(intent.hasExtra(Constants.TASKER_EXTRA_BUNDLE)) { + Bundle data = intent.getBundleExtra(Constants.TASKER_EXTRA_BUNDLE); + Boolean startShuffled = data.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE); + + Intent start = new Intent(context, DownloadService.class); + start.setAction(DownloadService.START_PLAY); + start.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, startShuffled); + start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR)); + start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR)); + start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE)); + start.putExtra(Constants.PREFERENCES_KEY_OFFLINE, data.getInt(Constants.PREFERENCES_KEY_OFFLINE)); + context.startService(start); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java new file mode 100644 index 0000000..4badd07 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java @@ -0,0 +1,1129 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpResponse; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.TimeLimitedCache; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; + +/** + * @author Sindre Mehus + */ +public class CachedMusicService implements MusicService { + private static final String TAG = CachedMusicService.class.getSimpleName(); + + private static final int MUSIC_DIR_CACHE_SIZE = 20; + private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes + public static final int CACHE_UPDATE_LIST = 1; + public static final int CACHE_UPDATE_METADATA = 2; + private static final int CACHED_LAST_FM = 24 * 60; + + private final RESTMusicService musicService; + private final TimeLimitedCache cachedIndexes = new TimeLimitedCache(60 * 60, TimeUnit.SECONDS); + private final TimeLimitedCache> cachedPlaylists = new TimeLimitedCache>(3600, TimeUnit.SECONDS); + private final TimeLimitedCache> cachedMusicFolders = new TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS); + private String restUrl; + private String musicFolderId; + private boolean isTagBrowsing = false; + + public CachedMusicService(RESTMusicService musicService) { + this.musicService = musicService; + } + + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + musicService.ping(context, progressListener); + } + + @Override + public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + if (refresh) { + cachedMusicFolders.clear(); + } + List result = cachedMusicFolders.get(); + if (result == null) { + if(!refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class); + } + + if(result == null) { + result = musicService.getMusicFolders(refresh, context, progressListener); + FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "musicFolders")); + } + + MusicFolder.sort(result); + cachedMusicFolders.set(result); + } + return result; + } + + @Override + public void startRescan(Context context, ProgressListener listener) throws Exception { + musicService.startRescan(context, listener); + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + if (refresh) { + cachedIndexes.clear(); + cachedMusicFolders.clear(); + } + Indexes result = cachedIndexes.get(); + if (result == null) { + String name = Util.isTagBrowsing(context, musicService.getInstance(context)) ? "artists" : "indexes"; + name = getCacheName(context, name, musicFolderId); + if(!refresh) { + result = FileUtil.deserialize(context, name, Indexes.class); + } + + if(result == null) { + result = musicService.getIndexes(musicFolderId, refresh, context, progressListener); + FileUtil.serialize(context, result, name); + } + cachedIndexes.set(result); + } + return result; + } + + @Override + public MusicDirectory getMusicDirectory(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "directory", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + private boolean metadataUpdated; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getMusicDirectory(id, name, true, context, null); + updateAllSongs(context, refreshed); + metadataUpdated = cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh music directory", error); + } + }.execute(); + } + + if(dir == null) { + dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); + FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); + + // If a cached copy exists to check against, look for removes + deleteRemovedEntries(context, dir, cached); + } + dir.sortChildren(context, musicService.getInstance(context)); + + return dir; + } + + @Override + public MusicDirectory getArtist(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "artist", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getArtist(id, name, refresh, context, null); + cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "artist", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh getArtist", error); + } + }.execute(); + } + + if(dir == null) { + dir = musicService.getArtist(id, name, refresh, context, progressListener); + FileUtil.serialize(context, dir, getCacheName(context, "artist", id)); + + // If a cached copy exists to check against, look for removes + deleteRemovedEntries(context, dir, cached); + } + dir.sortChildren(context, musicService.getInstance(context)); + + return dir; + } + + @Override + public MusicDirectory getAlbum(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "album", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + private boolean metadataUpdated; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getAlbum(id, name, refresh, context, null); + updateAllSongs(context, refreshed); + metadataUpdated = cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "album", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh getAlbum", error); + } + }.execute(); + } + + if(dir == null) { + dir = musicService.getAlbum(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); + FileUtil.serialize(context, dir, getCacheName(context, "album", id)); + + // If a cached copy exists to check against, look for removes + deleteRemovedEntries(context, dir, cached); + } + dir.sortChildren(context, musicService.getInstance(context)); + + return dir; + } + + @Override + public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { + return musicService.search(criteria, context, progressListener); + } + + @Override + public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + MusicDirectory cachedPlaylist = FileUtil.deserialize(context, getCacheName(context, "playlist", id), MusicDirectory.class); + if(!refresh) { + dir = cachedPlaylist; + } + if(dir == null) { + dir = musicService.getPlaylist(refresh, id, name, context, progressListener); + updateAllSongs(context, dir); + FileUtil.serialize(context, dir, getCacheName(context, "playlist", id)); + + File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context, musicService.getInstance(context)), dir.getName()); + if(cachedPlaylist == null || !playlistFile.exists() || !cachedPlaylist.getChildren().equals(dir.getChildren())) { + FileUtil.writePlaylistFile(context, playlistFile, dir); + } + } + return dir; + } + + @Override + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + List result = refresh ? null : cachedPlaylists.get(); + if (result == null) { + if(!refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class); + } + + if(result == null) { + result = musicService.getPlaylists(refresh, context, progressListener); + FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "playlist")); + } + cachedPlaylists.set(result); + } + return result; + } + + @Override + public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { + cachedPlaylists.clear(); + Util.delete(new File(context.getCacheDir(), getCacheName(context, "playlist"))); + musicService.createPlaylist(id, name, entries, context, progressListener); + } + + @Override + public void deletePlaylist(final String id, Context context, ProgressListener progressListener) throws Exception { + musicService.deletePlaylist(id, context, progressListener); + + new PlaylistUpdater(context, id) { + @Override + public void updateResult(List objects, Playlist result) { + objects.remove(result); + cachedPlaylists.set(objects); + } + }.execute(); + } + + @Override + public void addToPlaylist(String id, final List toAdd, Context context, ProgressListener progressListener) throws Exception { + musicService.addToPlaylist(id, toAdd, context, progressListener); + + new MusicDirectoryUpdater(context, "playlist", id) { + @Override + public boolean checkResult(Entry check) { + return true; + } + + @Override + public void updateResult(List objects, Entry result) { + objects.addAll(toAdd); + } + }.execute(); + } + + @Override + public void removeFromPlaylist(final String id, final List toRemove, Context context, ProgressListener progressListener) throws Exception { + musicService.removeFromPlaylist(id, toRemove, context, progressListener); + + new MusicDirectoryUpdater(context, "playlist", id) { + @Override + public boolean checkResult(Entry check) { + return true; + } + + @Override + public void updateResult(List objects, Entry result) { + // Make sure this playlist is supposed to be synced + boolean supposedToUnpin = false; + + // Remove in reverse order so indexes are still correct as we iterate through + for(ListIterator iterator = toRemove.listIterator(toRemove.size()); iterator.hasPrevious(); ) { + int index = iterator.previous(); + if(supposedToUnpin) { + Entry entry = objects.get(index); + DownloadFile file = new DownloadFile(context, entry, true); + file.unpin(); + } + + objects.remove(index); + } + } + }.execute(); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, final List toAdd, Context context, ProgressListener progressListener) throws Exception { + musicService.overwritePlaylist(id, name, toRemove, toAdd, context, progressListener); + + new MusicDirectoryUpdater(context, "playlist", id) { + @Override + public boolean checkResult(Entry check) { + return true; + } + + @Override + public void updateResult(List objects, Entry result) { + objects.clear(); + objects.addAll(toAdd); + } + }.execute(); + } + + @Override + public void updatePlaylist(String id, final String name, final String comment, final boolean pub, Context context, ProgressListener progressListener) throws Exception { + musicService.updatePlaylist(id, name, comment, pub, context, progressListener); + + new PlaylistUpdater(context, id) { + @Override + public void updateResult(List objects, Playlist result) { + result.setName(name); + result.setComment(comment); + result.setPublic(pub); + + cachedPlaylists.set(objects); + } + }.execute(); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + try { + MusicDirectory dir = musicService.getAlbumList(type, size, offset, refresh, context, progressListener); + + // Do some serialization updates for changes to recently added + if ("newest".equals(type) && offset == 0) { + String recentlyAddedFile = getCacheName(context, type); + ArrayList recents = FileUtil.deserialize(context, recentlyAddedFile, ArrayList.class); + if (recents == null) { + recents = new ArrayList(); + } + + // Add any new items + final int instance = musicService.getInstance(context); + isTagBrowsing = Util.isTagBrowsing(context, instance); + for (final Entry album : dir.getChildren()) { + if (!recents.contains(album.getId())) { + recents.add(album.getId()); + + String cacheName, parent; + if (isTagBrowsing) { + cacheName = "artist"; + parent = album.getArtistId(); + } else { + cacheName = "directory"; + parent = album.getParent(); + } + + // Add album to artist + if (parent != null) { + new MusicDirectoryUpdater(context, cacheName, parent) { + private boolean changed = false; + + @Override + public boolean checkResult(Entry check) { + return true; + } + + @Override + public void updateResult(List objects, Entry result) { + // Only add if it doesn't already exist in it! + if (!objects.contains(album)) { + objects.add(album); + changed = true; + } + } + + @Override + public void save(ArrayList objects) { + // Only save if actually added to artist + if (changed) { + musicDirectory.replaceChildren(objects); + FileUtil.serialize(context, musicDirectory, cacheName); + } + } + }.execute(); + } else { + // If parent is null, then this is a root level album + final Artist artist = new Artist(); + artist.setId(album.getId()); + artist.setName(album.getTitle()); + + new IndexesUpdater(context, isTagBrowsing ? "artists" : "indexes") { + private boolean changed = false; + + @Override + public boolean checkResult(Artist check) { + return true; + } + + @Override + public void updateResult(List objects, Artist result) { + if (!objects.contains(artist)) { + objects.add(artist); + changed = true; + } + } + + @Override + public void save(ArrayList objects) { + if (changed) { + indexes.setArtists(objects); + FileUtil.serialize(context, indexes, cacheName); + cachedIndexes.set(indexes); + } + } + }.execute(); + } + } + } + + // Keep list from growing into infinity + while (recents.size() > 0) { + recents.remove(0); + } + FileUtil.serialize(context, recents, recentlyAddedFile); + } + + FileUtil.serialize(context, dir, getCacheName(context, type, Integer.toString(offset))); + return dir; + } catch(IOException e) { + Log.w(TAG, "Failed to refresh album list: ", e); + if(refresh) { + throw e; + } + + MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type, Integer.toString(offset)), MusicDirectory.class); + + if(dir == null) { + // If we are at start and no cache, throw error higher + if(offset == 0) { + throw e; + } else { + // Otherwise just pretend we are at the end of the list + return new MusicDirectory(); + } + } else { + return dir; + } + } + } + + @Override + public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + try { + MusicDirectory dir = musicService.getAlbumList(type, extra, size, offset, refresh, context, progressListener); + FileUtil.serialize(context, dir, getCacheName(context, type + extra, Integer.toString(offset))); + return dir; + } catch(IOException e) { + Log.w(TAG, "Failed to refresh album list: ", e); + if(refresh) { + throw e; + } + + MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type + extra, Integer.toString(offset)), MusicDirectory.class); + + if(dir == null) { + // If we are at start and no cache, throw error higher + if(offset == 0) { + throw e; + } else { + // Otherwise just pretend we are at the end of the list + return new MusicDirectory(); + } + } else { + return dir; + } + } + } + + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongList(type, size, offset, context, progressListener); + } + + @Override + public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { + return musicService.getRandomSongs(size, artistId, context, progressListener); + } + + @Override + public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener); + } + + @Override + public String getCoverArtUrl(Context context, Entry entry) throws Exception { + return musicService.getCoverArtUrl(context, entry); + } + + @Override + public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + return musicService.getCoverArt(context, entry, size, progressListener, task); + } + + @Override + public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task); + } + + @Override + public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception { + return musicService.getMusicUrl(context, song, maxBitrate); + } + + @Override + public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List result = null; + + if(!refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "genre"), ArrayList.class); + } + + if(result == null) { + result = musicService.getGenres(refresh, context, progressListener); + FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "genre")); + } + + return result; + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + try { + MusicDirectory dir = musicService.getSongsByGenre(genre, count, offset, context, progressListener); + FileUtil.serialize(context, dir, getCacheName(context, "genreSongs", Integer.toString(offset))); + + return dir; + } catch(IOException e) { + MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, "genreSongs", Integer.toString(offset)), MusicDirectory.class); + + if(dir == null) { + // If we are at start and no cache, throw error higher + if(offset == 0) { + throw e; + } else { + // Otherwise just pretend we are at the end of the list + return new MusicDirectory(); + } + } else { + return dir; + } + } + } + + @Override + public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception { + return musicService.getTopTrackSongs(artist, size, context, progressListener); + } + + @Override + public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { + User result = null; + + try { + result = musicService.getUser(refresh, username, context, progressListener); + FileUtil.serialize(context, result, getCacheName(context, "user-" + username)); + } catch(Exception e) { + // Don't care + } + + if(result == null && !refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "user-" + username), User.class); + } + + return result; + } + + @Override + public List getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List result = null; + + if(!refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "users"), ArrayList.class); + } + + if(result == null) { + result = musicService.getUsers(refresh, context, progressListener); + FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "users")); + } + + return result; + } + + @Override + public void createUser(final User user, Context context, ProgressListener progressListener) throws Exception { + musicService.createUser(user, context, progressListener); + + new UserUpdater(context, "") { + @Override + public boolean checkResult(User check) { + return true; + } + + @Override + public void updateResult(List users, User result) { + users.add(user); + } + }.execute(); + } + + @Override + public void updateUser(final User user, Context context, ProgressListener progressListener) throws Exception { + musicService.updateUser(user, context, progressListener); + + new UserUpdater(context, user.getUsername()) { + @Override + public void updateResult(List users, User result) { + result.setEmail(user.getEmail()); + result.setSettings(user.getSettings()); + } + }.execute(); + } + + @Override + public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception { + musicService.deleteUser(username, context, progressListener); + + new UserUpdater(context, username) { + @Override + public void updateResult(List users, User result) { + users.remove(result); + } + }.execute(); + } + + @Override + public void changeEmail(String username, final String email, Context context, ProgressListener progressListener) throws Exception { + musicService.changeEmail(username, email, context, progressListener); + + // Update cached email for user + new UserUpdater(context, username) { + @Override + public void updateResult(List users, User result) { + result.setEmail(email); + } + }.execute(); + } + + @Override + public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception { + musicService.changePassword(username, password, context, progressListener); + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + return musicService.getBitmap(url, size, context, progressListener, task); + } + +@Override + public void savePlayQueue(List songs, Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception { + musicService.savePlayQueue(songs, currentPlaying, position, context, progressListener); + } + + @Override + public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception { + return musicService.getPlayQueue(context, progressListener); + } + + @Override + public void setInstance(Integer instance) throws Exception { + musicService.setInstance(instance); + } + + private String getCacheName(Context context, String name, String id) { + String s = musicService.getRestUrl(context, null, false) + id; + return name + "-" + s.hashCode() + ".ser"; + } + private String getCacheName(Context context, String name) { + String s = musicService.getRestUrl(context, null, false); + return name + "-" + s.hashCode() + ".ser"; + } + + private void deleteRemovedEntries(Context context, MusicDirectory dir, MusicDirectory cached) { + if(cached != null) { + List oldList = new ArrayList(); + oldList.addAll(cached.getChildren()); + + // Remove all current items from old list + for(Entry entry: dir.getChildren()) { + oldList.remove(entry); + } + + // Anything remaining has been removed from server + MediaStoreService store = new MediaStoreService(context); + for(Entry entry: oldList) { + File file = FileUtil.getEntryFile(context, entry); + FileUtil.recursiveDelete(file, store); + } + } + } + + private abstract class SerializeUpdater { + final Context context; + final String cacheName; + final boolean singleUpdate; + + public SerializeUpdater(Context context, String cacheName) { + this(context, cacheName, true); + } + public SerializeUpdater(Context context, String cacheName, boolean singleUpdate) { + this.context = context; + this.cacheName = getCacheName(context, cacheName); + this.singleUpdate = singleUpdate; + } + public SerializeUpdater(Context context, String cacheName, String id) { + this(context, cacheName, id, true); + } + public SerializeUpdater(Context context, String cacheName, String id, boolean singleUpdate) { + this.context = context; + this.cacheName = getCacheName(context, cacheName, id); + this.singleUpdate = singleUpdate; + } + + public ArrayList getArrayList() { + return FileUtil.deserialize(context, cacheName, ArrayList.class); + } + public abstract boolean checkResult(T check); + public abstract void updateResult(List objects, T result); + public void save(ArrayList objects) { + FileUtil.serialize(context, objects, cacheName); + } + + public void execute() { + ArrayList objects = getArrayList(); + + // Only execute if something to check against + if(objects != null) { + List results = new ArrayList(); + for(T check: objects) { + if(checkResult(check)) { + results.add(check); + if(singleUpdate) { + break; + } + } + } + + // Iterate through and update each object matched + for(T result: results) { + updateResult(objects, result); + } + + // Only reserialize if at least one match was found + if(results.size() > 0) { + save(objects); + } + } + } + } + private abstract class UserUpdater extends SerializeUpdater { + String username; + + public UserUpdater(Context context, String username) { + super(context, "users"); + this.username = username; + } + + @Override + public boolean checkResult(User check) { + return username.equals(check.getUsername()); + } + } + private abstract class PlaylistUpdater extends SerializeUpdater { + String id; + + public PlaylistUpdater(Context context, String id) { + super(context, "playlist"); + this.id = id; + } + + @Override + public boolean checkResult(Playlist check) { + return id.equals(check.getId()); + } + } + private abstract class MusicDirectoryUpdater extends SerializeUpdater { + protected MusicDirectory musicDirectory; + + public MusicDirectoryUpdater(Context context, String cacheName, String id) { + super(context, cacheName, id, true); + } + public MusicDirectoryUpdater(Context context, String cacheName, String id, boolean singleUpdate) { + super(context, cacheName, id, singleUpdate); + } + + @Override + public ArrayList getArrayList() { + musicDirectory = FileUtil.deserialize(context, cacheName, MusicDirectory.class); + if(musicDirectory != null) { + return new ArrayList<>(musicDirectory.getChildren()); + } else { + return null; + } + } + public void save(ArrayList objects) { + musicDirectory.replaceChildren(objects); + FileUtil.serialize(context, musicDirectory, cacheName); + } + } + private abstract class PlaylistDirectoryUpdater { + Context context; + + public PlaylistDirectoryUpdater(Context context) { + this.context = context; + } + + public abstract boolean checkResult(Entry check); + public abstract void updateResult(Entry result); + + public void execute() { + List playlists = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class); + if(playlists == null) { + // No playlist list cache, nothing to update! + return; + } + + for(Playlist playlist: playlists) { + new MusicDirectoryUpdater(context, "playlist", playlist.getId(), false) { + @Override + public boolean checkResult(Entry check) { + return PlaylistDirectoryUpdater.this.checkResult(check); + } + + @Override + public void updateResult(List objects, Entry result) { + PlaylistDirectoryUpdater.this.updateResult(result); + } + }.execute(); + } + } + } + private abstract class GenericEntryUpdater { + Context context; + List entries; + + public GenericEntryUpdater(Context context, Entry entry) { + this.context = context; + this.entries = Arrays.asList(entry); + } + public GenericEntryUpdater(Context context, List entries) { + this.context = context; + this.entries = entries; + } + + public boolean checkResult(Entry entry, Entry check) { + return entry.getId().equals(check.getId()); + } + public abstract void updateResult(Entry result); + + public void execute() { + String cacheName, parent; + // Make sure it is up to date + isTagBrowsing = Util.isTagBrowsing(context, musicService.getInstance(context)); + + // Run through each entry, trying to update the directory it is in + final List songs = new ArrayList(); + for(final Entry entry: entries) { + if(isTagBrowsing) { + // If starring album, needs to reference artist instead + if(entry.isDirectory()) { + if(entry.isAlbum()) { + cacheName = "artist"; + parent = entry.getArtistId(); + } else { + cacheName = "artists"; + parent = null; + } + } else { + cacheName = "album"; + parent = entry.getAlbumId(); + } + } else { + if(entry.isDirectory() && !entry.isAlbum()) { + cacheName = "indexes"; + parent = null; + } else { + cacheName = "directory"; + parent = entry.getParent(); + } + } + + // Parent is only null when it is an artist + if(parent == null) { + new IndexesUpdater(context, cacheName) { + @Override + public boolean checkResult(Artist check) { + return GenericEntryUpdater.this.checkResult(entry, new Entry(check)); + } + + @Override + public void updateResult(List objects, Artist result) { + // Don't try to put anything here, as the Entry update method will not be called since it's a artist! + } + }.execute(); + } else { + new MusicDirectoryUpdater(context, cacheName, parent) { + @Override + public boolean checkResult(Entry check) { + return GenericEntryUpdater.this.checkResult(entry, check); + } + + @Override + public void updateResult(List objects, Entry result) { + GenericEntryUpdater.this.updateResult(result); + } + }.execute(); + } + + songs.add(entry); + } + + // Only run through playlists once and check each song against it + if(songs.size() > 0) { + new PlaylistDirectoryUpdater(context) { + @Override + public boolean checkResult(Entry check) { + for(Entry entry: songs) { + if(GenericEntryUpdater.this.checkResult(entry, check)) { + return true; + } + } + + return false; + } + + @Override + public void updateResult(Entry result) { + GenericEntryUpdater.this.updateResult(result); + } + }.execute(); + } + } + } + + private class StarUpdater extends GenericEntryUpdater { + public StarUpdater(Context context, List entries) { + super(context, entries); + } + + @Override + public boolean checkResult(Entry entry, Entry check) { + if (!entry.getId().equals(check.getId())) { + return false; + } + + return true; + } + + @Override + public void updateResult(Entry result) { + + } + }; + private abstract class IndexesUpdater extends SerializeUpdater { + Indexes indexes; + + IndexesUpdater(Context context, String name) { + super(context, name, Util.getSelectedMusicFolderId(context, musicService.getInstance(context))); + } + + @Override + public ArrayList getArrayList() { + indexes = FileUtil.deserialize(context, cacheName, Indexes.class); + if(indexes == null) { + return null; + } + + ArrayList artists = new ArrayList(); + artists.addAll(indexes.getArtists()); + artists.addAll(indexes.getShortcuts()); + return artists; + } + + public void save(ArrayList objects) { + indexes.setArtists(objects); + FileUtil.serialize(context, indexes, cacheName); + cachedIndexes.set(indexes); + } + } + + protected void updateAllSongs(Context context, MusicDirectory dir) { + List songs = dir.getSongs(); + if(!songs.isEmpty()) { + SongDBHandler.getHandler(context).addSongs(musicService.getInstance(context), songs); + } + } + + private void checkSettingsChanged(Context context) { + int instance = musicService.getInstance(context); + String newUrl = musicService.getRestUrl(context, null, false); + boolean newIsTagBrowsing = Util.isTagBrowsing(context, instance); + if (!Util.equals(newUrl, restUrl) || isTagBrowsing != newIsTagBrowsing) { + cachedMusicFolders.clear(); + cachedIndexes.clear(); + cachedPlaylists.clear(); + restUrl = newUrl; + isTagBrowsing = newIsTagBrowsing; + } + + String newMusicFolderId = Util.getSelectedMusicFolderId(context, instance); + if(!Util.equals(newMusicFolderId, musicFolderId)) { + cachedIndexes.clear(); + musicFolderId = newMusicFolderId; + } + } + + public RESTMusicService getMusicService() { + return musicService; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java new file mode 100644 index 0000000..1f8ed4c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java @@ -0,0 +1,633 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.util.Log; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.util.CacheCleaner; +import github.daneren2005.serverproxy.BufferFile; + +import org.apache.http.Header; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DownloadFile implements BufferFile { + private static final String TAG = DownloadFile.class.getSimpleName(); + private static final int MAX_FAILURES = 5; + private final Context context; + private final MusicDirectory.Entry song; + private final File partialFile; + private final File completeFile; + private final File saveFile; + + private final MediaStoreService mediaStoreService; + private DownloadTask downloadTask; + private boolean save; + private boolean failedDownload = false; + private int failed = 0; + private int bitRate; + private boolean isPlaying = false; + private boolean saveWhenDone = false; + private boolean completeWhenDone = false; + private Long contentLength = null; + private long currentSpeed = 0; + private boolean rateLimit = false; + + public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) { + this.context = context; + this.song = song; + this.save = save; + saveFile = FileUtil.getSongFile(context, song); + bitRate = getActualBitrate(); + partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + + ".partial." + FileUtil.getExtension(saveFile.getName())); + completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + + ".complete." + FileUtil.getExtension(saveFile.getName())); + mediaStoreService = new MediaStoreService(context); + } + + public MusicDirectory.Entry getSong() { + return song; + } + public boolean isSong() { + return song.isSong(); + } + + public Context getContext() { + return context; + } + + /** + * Returns the effective bit rate. + */ + public int getBitRate() { + if(!partialFile.exists()) { + bitRate = getActualBitrate(); + } + if (bitRate > 0) { + return bitRate; + } + return song.getBitRate() == null ? 160 : song.getBitRate(); + } + private int getActualBitrate() { + int br = Util.getMaxBitrate(context); + if(br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) { + if(song.getBitRate() != null) { + br = Math.min(320, song.getBitRate()); + } else { + br = 320; + } + } else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) { + // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs) + if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) { + br = song.getBitRate(); + } + } + + return br; + } + + public Long getContentLength() { + return contentLength; + } + + public long getCurrentSize() { + if(partialFile.exists()) { + return partialFile.length(); + } else { + File file = getCompleteFile(); + if(file.exists()) { + return file.length(); + } else { + return 0L; + } + } + } + + @Override + public long getEstimatedSize() { + if(contentLength != null) { + return contentLength; + } + + File file = getCompleteFile(); + if(file.exists()) { + return file.length(); + } else if(song.getDuration() == null) { + return 0; + } else { + int br = (getBitRate() * 1000) / 8; + int duration = song.getDuration(); + return br * duration; + } + } + + public long getBytesPerSecond() { + return currentSpeed; + } + + public synchronized void download() { + rateLimit = false; + preDownload(); + downloadTask.execute(); + } + public synchronized void downloadNow(MusicService musicService) { + rateLimit = true; + preDownload(); + downloadTask.setMusicService(musicService); + try { + downloadTask.doInBackground(); + } catch(InterruptedException e) { + // This should never be reached + } + } + private void preDownload() { + FileUtil.createDirectoryForParent(saveFile); + failedDownload = false; + if(!partialFile.exists()) { + bitRate = getActualBitrate(); + } + downloadTask = new DownloadTask(context); + } + + public synchronized void cancelDownload() { + if (downloadTask != null) { + downloadTask.cancel(); + } + } + + @Override + public File getFile() { + if (saveFile.exists()) { + return saveFile; + } else if (completeFile.exists()) { + return completeFile; + } else { + return partialFile; + } + } + + public File getCompleteFile() { + if (saveFile.exists()) { + return saveFile; + } + + if (completeFile.exists()) { + return completeFile; + } + + return saveFile; + } + public File getSaveFile() { + return saveFile; + } + + public File getPartialFile() { + return partialFile; + } + + public boolean isSaved() { + return saveFile.exists(); + } + + public synchronized boolean isCompleteFileAvailable() { + return saveFile.exists() || completeFile.exists(); + } + + @Override + public synchronized boolean isWorkDone() { + return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone; + } + + @Override + public void onStart() { + setPlaying(true); + } + + @Override + public void onStop() { + setPlaying(false); + } + + @Override + public synchronized void onResume() { + if(!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) { + download(); + } + } + + public synchronized boolean isDownloading() { + return downloadTask != null && downloadTask.isRunning(); + } + + public synchronized boolean isDownloadCancelled() { + return downloadTask != null && downloadTask.isCancelled(); + } + + public boolean shouldSave() { + return save; + } + + public boolean isFailed() { + return failedDownload; + } + public boolean isFailedMax() { + return failed > MAX_FAILURES; + } + + public void delete() { + cancelDownload(); + + // Remove from mediaStore BEFORE deleting file since it calls getCompleteFile + deleteFromStore(); + + // Delete all possible versions of the file + File parent = partialFile.getParentFile(); + Util.delete(partialFile); + Util.delete(completeFile); + Util.delete(saveFile); + FileUtil.deleteEmptyDir(parent); + } + + public void unpin() { + if (saveFile.exists()) { + // Delete old store entry before renaming to pinned file + saveFile.renameTo(completeFile); + renameInStore(saveFile, completeFile); + } + } + + public boolean cleanup() { + boolean ok = true; + if (completeFile.exists() || saveFile.exists()) { + ok = Util.delete(partialFile); + } + if (saveFile.exists()) { + ok &= Util.delete(completeFile); + } + return ok; + } + + // In support of LRU caching. + public void updateModificationDate() { + updateModificationDate(saveFile); + updateModificationDate(partialFile); + updateModificationDate(completeFile); + } + + private void updateModificationDate(File file) { + if (file.exists()) { + boolean ok = file.setLastModified(System.currentTimeMillis()); + if (!ok) { + Log.w(TAG, "Failed to set last-modified date on " + file); + } + } + } + + public void setPlaying(boolean isPlaying) { + try { + if(saveWhenDone && !isPlaying) { + Util.renameFile(completeFile, saveFile); + renameInStore(completeFile, saveFile); + saveWhenDone = false; + } else if(completeWhenDone && !isPlaying) { + if(save) { + Util.renameFile(partialFile, saveFile); + saveToStore(); + } else { + Util.renameFile(partialFile, completeFile); + saveToStore(); + } + completeWhenDone = false; + } + } catch(IOException ex) { + Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile, ex); + } + + this.isPlaying = isPlaying; + } + public void renamePartial() { + try { + Util.renameFile(partialFile, completeFile); + saveToStore(); + } catch(IOException ex) { + Log.w(TAG, "Failed to rename file " + partialFile + " to " + completeFile, ex); + } + } + public boolean getPlaying() { + return isPlaying; + } + + private void deleteFromStore() { + try { + mediaStoreService.deleteFromMediaStore(this); + } catch(Exception e) { + Log.w(TAG, "Failed to remove from store", e); + } + } + private void saveToStore() { + if(!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) { + try { + mediaStoreService.saveInMediaStore(this); + } catch(Exception e) { + Log.w(TAG, "Failed to save in media store", e); + } + } + } + private void renameInStore(File start, File end) { + try { + mediaStoreService.renameInMediaStore(start, end); + } catch(Exception e) { + Log.w(TAG, "Failed to rename in store", e); + } + } + + @Override + public String toString() { + return "DownloadFile (" + song + ")"; + } + + // Don't do this. Causes infinite loop if two instances of same song + /*@Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DownloadFile downloadFile = (DownloadFile) o; + return Util.equals(this.getSong(), downloadFile.getSong()); + }*/ + + private class DownloadTask extends SilentBackgroundTask { + private MusicService musicService; + + public DownloadTask(Context context) { + super(context); + } + + @Override + public Void doInBackground() throws InterruptedException { + InputStream in = null; + FileOutputStream out = null; + PowerManager.WakeLock wakeLock = null; + WifiManager.WifiLock wifiLock = null; + try { + + if (Util.isScreenLitOnDownload(context)) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString()); + wakeLock.acquire(); + } + + wifiLock = Util.createWifiLock(context, toString()); + wifiLock.acquire(); + + if (saveFile.exists()) { + Log.i(TAG, saveFile + " already exists. Skipping."); + checkDownloads(); + return null; + } + if (completeFile.exists()) { + if (save) { + if(isPlaying) { + saveWhenDone = true; + } else { + Util.renameFile(completeFile, saveFile); + renameInStore(completeFile, saveFile); + } + } else { + Log.i(TAG, completeFile + " already exists. Skipping."); + } + checkDownloads(); + return null; + } + + if(musicService == null) { + musicService = MusicServiceFactory.getMusicService(context); + } + + // Some devices seem to throw error on partial file which doesn't exist + boolean compare; + try { + compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length(); + } catch(Exception e) { + compare = true; + } + if(compare) { + // Attempt partial HTTP GET, appending to the file if it exists. + HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this); + Header contentLengthHeader = response.getFirstHeader("Content-Length"); + if(contentLengthHeader != null) { + String contentLengthString = contentLengthHeader.getValue(); + if(contentLengthString != null) { + Log.i(TAG, "Content Length: " + contentLengthString); + contentLength = Long.parseLong(contentLengthString); + } + } + in = response.getEntity().getContent(); + boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT; + if (partial) { + Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes"); + } + + out = new FileOutputStream(partialFile, partial); + long n = copy(in, out); + Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile); + out.flush(); + out.close(); + + if (isCancelled()) { + throw new Exception("Download of '" + song + "' was cancelled"); + } else if(partialFile.length() == 0) { + throw new Exception("Download of '" + song + "' failed. File is 0 bytes long."); + } + + downloadAndSaveCoverArt(musicService); + } + + if(isPlaying) { + completeWhenDone = true; + } else { + if(save) { + Util.renameFile(partialFile, saveFile); + } else { + Util.renameFile(partialFile, completeFile); + } + DownloadFile.this.saveToStore(); + } + + } catch(InterruptedException x) { + throw x; + } catch(FileNotFoundException x) { + Util.delete(completeFile); + Util.delete(saveFile); + if(!isCancelled()) { + failed = MAX_FAILURES + 1; + failedDownload = true; + Log.w(TAG, "Failed to download '" + song + "'.", x); + } + } catch(IOException x) { + Util.delete(completeFile); + Util.delete(saveFile); + if(!isCancelled()) { + failedDownload = true; + Log.w(TAG, "Failed to download '" + song + "'.", x); + } + } catch (Exception x) { + Util.delete(completeFile); + Util.delete(saveFile); + if (!isCancelled()) { + failed++; + failedDownload = true; + Log.w(TAG, "Failed to download '" + song + "'.", x); + } + } finally { + Util.close(in); + Util.close(out); + if (wakeLock != null) { + wakeLock.release(); + Log.i(TAG, "Released wake lock " + wakeLock); + } + if (wifiLock != null) { + wifiLock.release(); + } + } + + // Only run these if not interrupted, ie: cancelled + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !isCancelled()) { + new CacheCleaner(context, downloadService).cleanSpace(); + checkDownloads(); + } + + return null; + } + + private void checkDownloads() { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null) { + downloadService.checkDownloads(); + } + } + + @Override + public String toString() { + return "DownloadTask (" + song + ")"; + } + + public void setMusicService(MusicService musicService) { + this.musicService = musicService; + } + + private void downloadAndSaveCoverArt(MusicService musicService) throws Exception { + try { + if (song.getCoverArt() != null) { + // Check if album art already exists, don't want to needlessly load into memory + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + if(!albumArtFile.exists()) { + musicService.getCoverArt(context, song, 0, null, null); + } + } + } catch (Exception x) { + Log.e(TAG, "Failed to get cover art.", x); + } + } + + private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException { + + // Start a thread that will close the input stream if the task is + // cancelled, thus causing the copy() method to return. + new Thread("DownloadFile_copy") { + @Override + public void run() { + while (true) { + Util.sleepQuietly(3000L); + if (isCancelled()) { + Util.close(in); + return; + } + if (!isRunning()) { + return; + } + } + } + }.start(); + + byte[] buffer = new byte[1024 * 16]; + long count = 0; + int n; + long lastLog = System.currentTimeMillis(); + long lastCount = 0; + + boolean activeLimit = rateLimit; + while (!isCancelled() && (n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + count += n; + lastCount += n; + + long now = System.currentTimeMillis(); + if (now - lastLog > 3000L) { // Only every so often. + Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song); + currentSpeed = lastCount / ((now - lastLog) / 1000L); + lastLog = now; + lastCount = 0; + + // Re-establish every few seconds whether screen is on or not + if(rateLimit) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if(pm.isScreenOn()) { + activeLimit = true; + } else { + activeLimit = false; + } + } + } + + // If screen is on and rateLimit is true, stop downloading from exhausting bandwidth + if(activeLimit) { + Thread.sleep(10L); + } + } + return count; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java new file mode 100644 index 0000000..b8a377a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java @@ -0,0 +1,2271 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import static github.nvllsvm.audinaut.domain.PlayerState.COMPLETED; +import static github.nvllsvm.audinaut.domain.PlayerState.DOWNLOADING; +import static github.nvllsvm.audinaut.domain.PlayerState.IDLE; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED_TEMP; +import static github.nvllsvm.audinaut.domain.PlayerState.PREPARED; +import static github.nvllsvm.audinaut.domain.PlayerState.PREPARING; +import static github.nvllsvm.audinaut.domain.PlayerState.STARTED; +import static github.nvllsvm.audinaut.domain.PlayerState.STOPPED; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.audiofx.AudioEffectsController; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.receiver.MediaButtonIntentReceiver; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Notifications; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ShufflePlayBuffer; +import github.nvllsvm.audinaut.util.SimpleServiceBinder; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.util.tags.BastpUtil; +import github.nvllsvm.audinaut.view.UpdateView; +import github.daneren2005.serverproxy.BufferProxy; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import android.annotation.TargetApi; +import android.app.Service; +import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.PlaybackParams; +import android.media.audiofx.AudioEffect; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.support.v4.util.LruCache; +import android.view.KeyEvent; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DownloadService extends Service { + private static final String TAG = DownloadService.class.getSimpleName(); + + public static final String CMD_PLAY = "github.nvllsvm.audinaut.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "github.nvllsvm.audinaut.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "github.nvllsvm.audinaut.CMD_PAUSE"; + public static final String CMD_STOP = "github.nvllsvm.audinaut.CMD_STOP"; + public static final String CMD_PREVIOUS = "github.nvllsvm.audinaut.CMD_PREVIOUS"; + public static final String CMD_NEXT = "github.nvllsvm.audinaut.CMD_NEXT"; + public static final String CANCEL_DOWNLOADS = "github.nvllsvm.audinaut.CANCEL_DOWNLOADS"; + public static final String START_PLAY = "github.nvllsvm.audinaut.START_PLAYING"; + public static final int FAST_FORWARD = 30000; + public static final int REWIND = 10000; + private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L; + private static final double DELETE_CUTOFF = 0.84; + private static final int REQUIRED_ALBUM_MATCHES = 4; + private static final int REMOTE_PLAYLIST_TOTAL = 3; + private static final int SHUFFLE_MODE_NONE = 0; + private static final int SHUFFLE_MODE_ALL = 1; + private static final int SHUFFLE_MODE_ARTIST = 2; + + public static final int METADATA_UPDATED_ALL = 0; + public static final int METADATA_UPDATED_STAR = 1; + public static final int METADATA_UPDATED_COVER_ART = 8; + + private final IBinder binder = new SimpleServiceBinder<>(this); + private Looper mediaPlayerLooper; + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private int audioSessionId; + private boolean nextSetup = false; + private final List downloadList = new ArrayList(); + private final List backgroundDownloadList = new ArrayList(); + private final List toDelete = new ArrayList(); + private final Handler handler = new Handler(); + private Handler mediaPlayerHandler; + private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); + private ShufflePlayBuffer shufflePlayBuffer; + + private final LruCache downloadFileCache = new LruCache(100); + private final List cleanupCandidates = new ArrayList(); + private DownloadFile currentPlaying; + private int currentPlayingIndex = -1; + private DownloadFile nextPlaying; + private DownloadFile currentDownloading; + private SilentBackgroundTask bufferTask; + private SilentBackgroundTask nextPlayingTask; + private PlayerState playerState = IDLE; + private PlayerState nextPlayerState = IDLE; + private boolean removePlayed; + private boolean shufflePlay; + private final List onSongChangedListeners = new ArrayList<>(); + private long revision; + private static DownloadService instance; + private String suggestedPlaylistName; + private String suggestedPlaylistId; + private PowerManager.WakeLock wakeLock; + private WifiManager.WifiLock wifiLock; + private boolean keepScreenOn; + private int cachedPosition = 0; + private boolean downloadOngoing = false; + private float volume = 1.0f; + private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS; + + private AudioEffectsController effectsController; + private PositionCache positionCache; + private BufferProxy proxy; + + private boolean autoPlayStart = false; + private boolean runListenersOnInit = false; + + // Variables to manage getCurrentPosition sometimes starting from an arbitrary non-zero number + private long subtractNextPosition = 0; + private int subtractPosition = 0; + + @Override + public void onCreate() { + super.onCreate(); + + final SharedPreferences prefs = Util.getPreferences(this); + new Thread(new Runnable() { + public void run() { + Looper.prepare(); + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK); + + audioSessionId = -1; + Integer id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1); + if(id != -1) { + try { + audioSessionId = id; + mediaPlayer.setAudioSessionId(audioSessionId); + } catch (Throwable e) { + audioSessionId = -1; + } + } + + if(audioSessionId == -1) { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + try { + audioSessionId = mediaPlayer.getAudioSessionId(); + prefs.edit().putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId).commit(); + } catch (Throwable t) { + // Froyo or lower + } + } + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) { + handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")")); + return false; + } + }); + + /*try { + Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + }*/ + + effectsController = new AudioEffectsController(DownloadService.this, audioSessionId); + if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { + getEqualizerController(); + } + + mediaPlayerLooper = Looper.myLooper(); + mediaPlayerHandler = new Handler(mediaPlayerLooper); + + if(runListenersOnInit) { + onSongsChanged(); + onSongProgress(); + onStateUpdate(); + } + + Looper.loop(); + } + }, "DownloadService").start(); + + Util.registerMediaButtonEventReceiver(this); + + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "downloadServiceLock"); + + keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false); + + instance = this; + shufflePlayBuffer = new ShufflePlayBuffer(this); + lifecycleSupport.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + lifecycleSupport.onStart(intent); + return START_NOT_STICKY; + } + + @Override + public void onTrimMemory(int level) { + ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(this); + if(imageLoader != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + Log.i(TAG, "Memory Trim Level: " + level); + if (level < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + imageLoader.onLowMemory(0.75f); + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) { + imageLoader.onLowMemory(0.50f); + } else if(level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) { + imageLoader.onLowMemory(0.25f); + } + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + imageLoader.onLowMemory(0.25f); + } else if(level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + imageLoader.onLowMemory(0.75f); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + instance = null; + + if(currentPlaying != null) currentPlaying.setPlaying(false); + lifecycleSupport.onDestroy(); + + try { + Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + } + + mediaPlayer.release(); + if(nextMediaPlayer != null) { + nextMediaPlayer.release(); + } + mediaPlayerLooper.quit(); + shufflePlayBuffer.shutdown(); + effectsController.release(); + + if(bufferTask != null) { + bufferTask.cancel(); + bufferTask = null; + } + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + if(proxy != null) { + proxy.stop(); + proxy = null; + } + Notifications.hidePlayingNotification(this, this, handler); + Notifications.hideDownloadingNotification(this, this, handler); + } + + public static DownloadService getInstance() { + return instance; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + public void post(Runnable r) { + handler.post(r); + } + public void postDelayed(Runnable r, long millis) { + handler.postDelayed(r, millis); + } + + public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(songs, save, autoplay, playNext, shuffle, 0, 0); + } + public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start, int position) { + setShufflePlayEnabled(false); + int offset = 1; + boolean noNetwork = !Util.isOffline(this) && !Util.isNetworkConnected(this); + boolean warnNetwork = false; + + if (songs.isEmpty()) { + return; + } + + if (playNext) { + if (autoplay && getCurrentPlayingIndex() >= 0) { + offset = 0; + } + for (MusicDirectory.Entry song : songs) { + if(song != null) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + addToDownloadList(downloadFile, getCurrentPlayingIndex() + offset); + if(noNetwork && !warnNetwork) { + if(!downloadFile.isCompleteFileAvailable()) { + warnNetwork = true; + } + } + offset++; + } + } + + setNextPlaying(); + } else { + int size = size(); + int index = getCurrentPlayingIndex(); + for (MusicDirectory.Entry song : songs) { + if(song == null) { + continue; + } + + DownloadFile downloadFile = new DownloadFile(this, song, save); + addToDownloadList(downloadFile, -1); + if(noNetwork && !warnNetwork) { + if(!downloadFile.isCompleteFileAvailable()) { + warnNetwork = true; + } + } + } + if(!autoplay && (size - 1) == index) { + setNextPlaying(); + } + } + revision++; + onSongsChanged(); + updateRemotePlaylist(); + + if(shuffle) { + shuffle(); + } + if(warnNetwork) { + Util.toast(this, R.string.select_album_no_network); + } + + if (autoplay) { + play(start, true, position); + } else if(start != 0 || position != 0) { + play(start, false, position); + } else { + if (currentPlaying == null) { + currentPlaying = downloadList.get(0); + currentPlayingIndex = 0; + currentPlaying.setPlaying(true); + } else { + currentPlayingIndex = downloadList.indexOf(currentPlaying); + } + checkDownloads(); + } + lifecycleSupport.serializeDownloadQueue(); + } + private void addToDownloadList(DownloadFile file, int offset) { + if(offset == -1) { + downloadList.add(file); + } else { + downloadList.add(offset, file); + } + } + public synchronized void downloadBackground(List songs, boolean save) { + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + if(!downloadFile.isWorkDone() || (downloadFile.shouldSave() && !downloadFile.isSaved())) { + // Only add to list if there is work to be done + backgroundDownloadList.add(downloadFile); + } else if(downloadFile.isSaved() && !save) { + // Quickly unpin song instead of adding it to work to be done + downloadFile.unpin(); + } + } + revision++; + + if(!Util.isOffline(this) && !Util.isNetworkConnected(this)) { + Util.toast(this, R.string.select_album_no_network); + } + + checkDownloads(); + lifecycleSupport.serializeDownloadQueue(); + } + + private synchronized void updateRemotePlaylist() { + List playlist = new ArrayList<>(); + if(currentPlaying != null) { + int index = downloadList.indexOf(currentPlaying); + if(index == -1) { + index = 0; + } + + int size = size(); + int end = index + REMOTE_PLAYLIST_TOTAL; + for(int i = index; i < size && i < end; i++) { + playlist.add(downloadList.get(i)); + } + } + } + + public synchronized void restore(List songs, List toDelete, int currentPlayingIndex, int currentPlayingPosition) { + SharedPreferences prefs = Util.getPreferences(this); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, false)) { + removePlayed = true; + } + int startShufflePlay = prefs.getInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, SHUFFLE_MODE_NONE); + download(songs, false, false, false, false); + if(startShufflePlay != SHUFFLE_MODE_NONE) { + if(startShufflePlay == SHUFFLE_MODE_ALL) { + shufflePlay = true; + } + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, startShufflePlay); + editor.commit(); + } + if (currentPlayingIndex != -1) { + while(mediaPlayer == null) { + Util.sleepQuietly(50L); + } + + play(currentPlayingIndex, autoPlayStart, currentPlayingPosition); + autoPlayStart = false; + } + + if(toDelete != null) { + for(MusicDirectory.Entry entry: toDelete) { + this.toDelete.add(forSong(entry)); + } + } + + suggestedPlaylistName = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, null); + suggestedPlaylistId = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_ID, null); + } + + public boolean isInitialized() { + return lifecycleSupport != null && lifecycleSupport.isInitialized(); + } + + public synchronized Date getLastStateChanged() { + return lifecycleSupport.getLastChange(); + } + + public synchronized void setRemovePlayed(boolean enabled) { + removePlayed = enabled; + if(removePlayed) { + checkDownloads(); + lifecycleSupport.serializeDownloadQueue(); + } + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, enabled); + editor.commit(); + } + public boolean isRemovePlayed() { + return removePlayed; + } + + public synchronized void setShufflePlayEnabled(boolean enabled) { + shufflePlay = enabled; + if (shufflePlay) { + checkDownloads(); + } + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled ? SHUFFLE_MODE_ALL : SHUFFLE_MODE_NONE); + editor.commit(); + } + + public boolean isShufflePlayEnabled() { + return shufflePlay; + } + + public synchronized void shuffle() { + Collections.shuffle(downloadList); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + if (currentPlaying != null) { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, currentPlaying); + currentPlayingIndex = 0; + } + revision++; + onSongsChanged(); + lifecycleSupport.serializeDownloadQueue(); + updateRemotePlaylist(); + setNextPlaying(); + } + + public RepeatMode getRepeatMode() { + return Util.getRepeatMode(this); + } + + public void setRepeatMode(RepeatMode repeatMode) { + Util.setRepeatMode(this, repeatMode); + setNextPlaying(); + } + + public boolean getKeepScreenOn() { + return keepScreenOn; + } + + public void setKeepScreenOn(boolean keepScreenOn) { + this.keepScreenOn = keepScreenOn; + + SharedPreferences prefs = Util.getPreferences(this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, keepScreenOn); + editor.commit(); + } + + public synchronized DownloadFile forSong(MusicDirectory.Entry song) { + DownloadFile returnFile = null; + for (DownloadFile downloadFile : downloadList) { + if (downloadFile.getSong().equals(song)) { + if(((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) { + // If downloading, return immediately + return downloadFile; + } else { + // Otherwise, check to make sure there isn't a background download going on first + returnFile = downloadFile; + } + } + } + for (DownloadFile downloadFile : backgroundDownloadList) { + if (downloadFile.getSong().equals(song)) { + return downloadFile; + } + } + + if(returnFile != null) { + return returnFile; + } + + DownloadFile downloadFile = downloadFileCache.get(song); + if (downloadFile == null) { + downloadFile = new DownloadFile(this, song, false); + downloadFileCache.put(song, downloadFile); + } + return downloadFile; + } + + public synchronized void clearBackground() { + if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + backgroundDownloadList.clear(); + revision++; + Notifications.hideDownloadingNotification(this, this, handler); + } + + public synchronized void clearIncomplete() { + Iterator iterator = downloadList.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (!downloadFile.isCompleteFileAvailable()) { + iterator.remove(); + + // Reset if the current playing song has been removed + if(currentPlaying == downloadFile) { + reset(); + } + + currentPlayingIndex = downloadList.indexOf(currentPlaying); + } + } + lifecycleSupport.serializeDownloadQueue(); + updateRemotePlaylist(); + onSongsChanged(); + } + + public void setOnline(final boolean online) { + if(shufflePlay) { + setShufflePlayEnabled(false); + } + + lifecycleSupport.post(new Runnable() { + @Override + public void run() { + if (online) { + checkDownloads(); + } else { + clearIncomplete(); + } + } + }); + } + + public synchronized int size() { + return downloadList.size(); + } + + public synchronized void clear() { + clear(true); + } + public synchronized void clear(boolean serialize) { + // Delete podcast if fully listened to + int position = getPlayerPosition(); + int duration = getPlayerDuration(); + boolean cutoff = isPastCutoff(position, duration, true); + for(DownloadFile podcast: toDelete) { + podcast.delete(); + } + toDelete.clear(); + + reset(); + downloadList.clear(); + onSongsChanged(); + if (currentDownloading != null && !backgroundDownloadList.contains(currentDownloading)) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + setCurrentPlaying(null, false); + + if (serialize) { + lifecycleSupport.serializeDownloadQueue(); + } + updateRemotePlaylist(); + setNextPlaying(); + if(proxy != null) { + proxy.stop(); + proxy = null; + } + + suggestedPlaylistName = null; + suggestedPlaylistId = null; + + setShufflePlayEnabled(false); + checkDownloads(); + } + + public synchronized void remove(int which) { + downloadList.remove(which); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + } + + public synchronized void remove(DownloadFile downloadFile) { + if (downloadFile == currentDownloading) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + if (downloadFile == currentPlaying) { + reset(); + setCurrentPlaying(null, false); + } + downloadList.remove(downloadFile); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + backgroundDownloadList.remove(downloadFile); + revision++; + onSongsChanged(); + lifecycleSupport.serializeDownloadQueue(); + updateRemotePlaylist(); + if(downloadFile == nextPlaying) { + setNextPlaying(); + } + + checkDownloads(); + } + public synchronized void removeBackground(DownloadFile downloadFile) { + if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + + backgroundDownloadList.remove(downloadFile); + revision++; + checkDownloads(); + } + + public synchronized void delete(List songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).delete(); + } + } + + public synchronized void unpin(List songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).unpin(); + } + } + + synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) { + try { + setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification); + } catch (IndexOutOfBoundsException x) { + // Ignored + } + } + + synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { + if(this.currentPlaying != null) { + this.currentPlaying.setPlaying(false); + } + this.currentPlaying = currentPlaying; + if(currentPlaying == null) { + currentPlayingIndex = -1; + setPlayerState(IDLE); + } else { + currentPlayingIndex = downloadList.indexOf(currentPlaying); + } + + if (currentPlaying != null && currentPlaying.getSong() != null) { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + } else { + Util.broadcastNewTrackInfo(this, null); + Notifications.hidePlayingNotification(this, this, handler); + } + onSongChanged(); + } + + synchronized void setNextPlaying() { + SharedPreferences prefs = Util.getPreferences(DownloadService.this); + + boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); + if (!gaplessPlayback) { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } + setNextPlayerState(IDLE); + + int index = getNextPlayingIndex(); + + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + resetNext(); + + if(index < size() && index != -1 && index != currentPlayingIndex) { + nextPlaying = downloadList.get(index); + + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.execute(); + } else { + nextPlaying = null; + } + } + + public int getCurrentPlayingIndex() { + return currentPlayingIndex; + } + private int getNextPlayingIndex() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + RepeatMode repeatMode = getRepeatMode(); + switch (repeatMode) { + case OFF: + index = index + 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + + index = checkNextIndexValid(index, repeatMode); + } + return index; + } + private int checkNextIndexValid(int index, RepeatMode repeatMode) { + int startIndex = index; + int size = size(); + if(index < size && index != -1) { + if(!Util.isAllowedToDownload(this)){ + DownloadFile next = downloadList.get(index); + while(!next.isCompleteFileAvailable()) { + index++; + + if (index >= size) { + if(repeatMode == RepeatMode.ALL) { + index = 0; + } else { + return -1; + } + } else if(index == startIndex) { + handler.post(new Runnable() { + @Override + public void run() { + Util.toast(DownloadService.this, R.string.download_playerstate_mobile_disabled); + } + }); + return -1; + } + + next = downloadList.get(index); + } + } + } + + return index; + } + + public DownloadFile getCurrentPlaying() { + return currentPlaying; + } + + public DownloadFile getCurrentDownloading() { + return currentDownloading; + } + + public DownloadFile getNextPlaying() { + return nextPlaying; + } + + public List getSongs() { + return downloadList; + } + + public List getToDelete() { return toDelete; } + + public synchronized List getDownloads() { + List temp = new ArrayList(); + temp.addAll(downloadList); + temp.addAll(backgroundDownloadList); + return temp; + } + + public List getBackgroundDownloads() { + return backgroundDownloadList; + } + + /** Plays either the current song (resume) or the first/next one in queue. */ + public synchronized void play() + { + int current = getCurrentPlayingIndex(); + if (current == -1) { + play(0); + } else { + play(current); + } + } + + public synchronized void play(int index) { + play(index, true); + } + public synchronized void play(DownloadFile downloadFile) { + play(downloadList.indexOf(downloadFile)); + } + private synchronized void play(int index, boolean start) { + play(index, start, 0); + } + private synchronized void play(int index, boolean start, int position) { + int size = this.size(); + cachedPosition = 0; + if (index < 0 || index >= size) { + reset(); + if(index >= size && size != 0) { + setCurrentPlaying(0, false); + Notifications.hidePlayingNotification(this, this, handler); + } else { + setCurrentPlaying(null, false); + } + lifecycleSupport.serializeDownloadQueue(); + } else { + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + setCurrentPlaying(index, start); + bufferAndPlay(position, start); + checkDownloads(); + setNextPlaying(); + } + } + private synchronized void playNext() { + if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) { + if(!nextSetup) { + playNext(true); + } else { + nextSetup = false; + playNext(false); + } + } else { + onSongCompleted(); + } + } + private synchronized void playNext(boolean start) { + Util.broadcastPlaybackStatusChange(this, currentPlaying.getSong(), PlayerState.PREPARED); + + // Swap the media players since nextMediaPlayer is ready to play + subtractPosition = 0; + if(start) { + nextMediaPlayer.start(); + } else if(!nextMediaPlayer.isPlaying()) { + Log.w(TAG, "nextSetup lied about it's state!"); + nextMediaPlayer.start(); + } else { + Log.i(TAG, "nextMediaPlayer already playing"); + + // Next time the cachedPosition is updated, use that as position 0 + subtractNextPosition = System.currentTimeMillis(); + } + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying, true); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false, start); + setNextPlaying(); + + // Proxy should not be being used here since the next player was already setup to play + if(proxy != null) { + proxy.stop(); + proxy = null; + } + checkDownloads(); + updateRemotePlaylist(); + } + + /** Plays or resumes the playback, depending on the current player state. */ + public synchronized void togglePlayPause() { + if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) { + start(); + } else if (playerState == STOPPED || playerState == IDLE) { + autoPlayStart = true; + play(); + } else if (playerState == STARTED) { + pause(); + } + } + + public synchronized void seekTo(int position) { + if(position < 0) { + position = 0; + } + + try { + if(proxy != null && currentPlaying.isCompleteFileAvailable()) { + doPlay(currentPlaying, position, playerState == STARTED); + return; + } + + mediaPlayer.seekTo(position); + subtractPosition = 0; + cachedPosition = position; + + onSongProgress(); + if(playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + } catch (Exception x) { + handleError(x); + } + } + public synchronized int rewind() { + return seekToWrapper(-REWIND); + } + public synchronized int fastForward() { + return seekToWrapper(FAST_FORWARD); + } + protected int seekToWrapper(int difference) { + int msPlayed = Math.max(0, getPlayerPosition()); + Integer duration = getPlayerDuration(); + int msTotal = duration == null ? 0 : duration; + + int seekTo; + if(msPlayed + difference > msTotal) { + seekTo = msTotal; + } else { + seekTo = msPlayed + difference; + } + seekTo(seekTo); + + return seekTo; + } + + public synchronized void previous() { + int index = getCurrentPlayingIndex(); + if (index == -1) { + return; + } + + // If only one song, just skip within song + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + rewind(); + return; + } + + + // Restart song if played more than five seconds. + if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) { + seekTo(0); + } else { + if(index == 0) { + index = size(); + } + + play(index - 1, playerState != PAUSED && playerState != STOPPED && playerState != IDLE); + } + } + + public synchronized void next() { + next(false); + } + public synchronized void next(boolean forceCutoff) { + next(forceCutoff, false); + } + public synchronized void next(boolean forceCutoff, boolean forceStart) { + // If only one song, just skip within song + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + fastForward(); + return; + } else if(playerState == PREPARING || playerState == PREPARED) { + return; + } + + // Delete podcast if fully listened to + int position = getPlayerPosition(); + int duration = getPlayerDuration(); + boolean cutoff; + if(forceCutoff) { + cutoff = true; + } else { + cutoff = isPastCutoff(position, duration); + } + + int index = getCurrentPlayingIndex(); + int nextPlayingIndex = getNextPlayingIndex(); + // Make sure to actually go to next when repeat song is on + if(index == nextPlayingIndex) { + nextPlayingIndex++; + } + if (index != -1 && nextPlayingIndex < size()) { + play(nextPlayingIndex, playerState != PAUSED && playerState != STOPPED && playerState != IDLE || forceStart); + } + } + + public void onSongCompleted() { + setPlayerStateCompleted(); + postPlayCleanup(); + play(getNextPlayingIndex()); + } + public void onNextStarted(DownloadFile nextPlaying) { + setPlayerStateCompleted(); + postPlayCleanup(); + setCurrentPlaying(nextPlaying, true); + setPlayerState(STARTED); + setNextPlayerState(IDLE); + } + + public synchronized void pause() { + pause(false); + } + public synchronized void pause(boolean temp) { + try { + if (playerState == STARTED) { + mediaPlayer.pause(); + setPlayerState(temp ? PAUSED_TEMP : PAUSED); + } else if(playerState == PAUSED_TEMP) { + setPlayerState(temp ? PAUSED_TEMP : PAUSED); + } + } catch (Exception x) { + handleError(x); + } + } + + public synchronized void stop() { + try { + if (playerState == STARTED) { + mediaPlayer.pause(); + setPlayerState(STOPPED); + } else if(playerState == PAUSED) { + setPlayerState(STOPPED); + } + } catch(Exception x) { + handleError(x); + } + } + + public synchronized void start() { + try { + // Only start if done preparing + if(playerState != PREPARING) { + mediaPlayer.start(); + } else { + // Otherwise, we need to set it up to start when done preparing + autoPlayStart = true; + } + setPlayerState(STARTED); + } catch (Exception x) { + handleError(x); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public synchronized void reset() { + if (bufferTask != null) { + bufferTask.cancel(); + bufferTask = null; + } + try { + setPlayerState(IDLE); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.setOnCompletionListener(null); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && nextSetup) { + mediaPlayer.setNextMediaPlayer(null); + nextSetup = false; + } + mediaPlayer.reset(); + subtractPosition = 0; + } catch (Exception x) { + handleError(x); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public synchronized void resetNext() { + try { + if (nextMediaPlayer != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && nextSetup) { + mediaPlayer.setNextMediaPlayer(null); + } + nextSetup = false; + + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.setOnErrorListener(null); + nextMediaPlayer.reset(); + nextMediaPlayer.release(); + nextMediaPlayer = null; + } else if(nextSetup) { + nextSetup = false; + } + } catch (Exception e) { + Log.w(TAG, "Failed to reset next media player"); + } + } + + public int getPlayerPosition() { + try { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { + return 0; + } + return Math.max(0, cachedPosition - subtractPosition); + } catch (Exception x) { + handleError(x); + return 0; + } + } + + public synchronized int getPlayerDuration() { + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { + int duration = 0; + try { + duration = mediaPlayer.getDuration(); + } catch (Exception x) { + duration = 0; + } + + if(duration != 0) { + return duration; + } + } + + if (currentPlaying != null) { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) { + return duration * 1000; + } + } + + return 0; + } + + public PlayerState getPlayerState() { + return playerState; + } + + public PlayerState getNextPlayerState() { + return nextPlayerState; + } + + public synchronized void setPlayerState(final PlayerState playerState) { + Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); + + if (playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + + boolean show = playerState == PlayerState.STARTED; + boolean pause = playerState == PlayerState.PAUSED; + boolean hide = playerState == PlayerState.STOPPED; + Util.broadcastPlaybackStatusChange(this, (currentPlaying != null) ? currentPlaying.getSong() : null, playerState); + + this.playerState = playerState; + + if(playerState == STARTED) { + Util.requestAudioFocus(this); + } + + if (show) { + Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else if (pause) { + SharedPreferences prefs = Util.getPreferences(this); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) { + Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else { + Notifications.hidePlayingNotification(this, this, handler); + } + } else if(hide) { + Notifications.hidePlayingNotification(this, this, handler); + } + if(playerState == STARTED && positionCache == null) { + positionCache = new LocalPositionCache(); + Thread thread = new Thread(positionCache, "PositionCache"); + thread.start(); + } else if(playerState != STARTED && positionCache != null) { + positionCache.stop(); + positionCache = null; + } + + + onStateUpdate(); + } + + public void setPlayerStateCompleted() { + // Acquire a temporary wakelock + acquireWakelock(); + + Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")"); + this.playerState = PlayerState.COMPLETED; + if(positionCache != null) { + positionCache.stop(); + positionCache = null; + } + + onStateUpdate(); + } + + private class PositionCache implements Runnable { + boolean isRunning = true; + + public void stop() { + isRunning = false; + } + + @Override + public void run() { + // Stop checking position before the song reaches completion + while(isRunning) { + try { + onSongProgress(); + Thread.sleep(delayUpdateProgress); + } + catch(Exception e) { + isRunning = false; + positionCache = null; + } + } + } + } + private class LocalPositionCache extends PositionCache { + boolean isRunning = true; + + public void stop() { + isRunning = false; + } + + @Override + public void run() { + // Stop checking position before the song reaches completion + while(isRunning) { + try { + if(mediaPlayer != null && playerState == STARTED) { + int newPosition = mediaPlayer.getCurrentPosition(); + + // If sudden jump in position, something is wrong + if(subtractNextPosition == 0 && newPosition > (cachedPosition + 5000)) { + // Only 1 second should have gone by, subtract the rest + subtractPosition += (newPosition - cachedPosition) - 1000; + } + + cachedPosition = newPosition; + + if(subtractNextPosition > 0) { + // Subtraction amount is current position - how long ago onCompletionListener was called + subtractPosition = cachedPosition - (int) (System.currentTimeMillis() - subtractNextPosition); + if(subtractPosition < 0) { + subtractPosition = 0; + } + subtractNextPosition = 0; + } + } + onSongProgress(cachedPosition < 2000 ? true: false); + Thread.sleep(delayUpdateProgress); + } + catch(Exception e) { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + public synchronized void setNextPlayerState(PlayerState playerState) { + Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")"); + this.nextPlayerState = playerState; + } + + public void setSuggestedPlaylistName(String name, String id) { + this.suggestedPlaylistName = name; + this.suggestedPlaylistId = id; + + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, name); + editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_ID, id); + editor.commit(); + } + + public String getSuggestedPlaylistName() { + return suggestedPlaylistName; + } + + public String getSuggestedPlaylistId() { + return suggestedPlaylistId; + } + + public boolean getEqualizerAvailable() { + return effectsController.isAvailable(); + } + + public EqualizerController getEqualizerController() { + EqualizerController controller = null; + try { + controller = effectsController.getEqualizerController(); + if(controller.getEqualizer() == null) { + throw new Exception("Failed to get EQ"); + } + } catch(Exception e) { + Log.w(TAG, "Failed to start EQ, retrying with new mediaPlayer: " + e); + + // If we failed, we are going to try to reinitialize the MediaPlayer + boolean playing = playerState == STARTED; + int pos = getPlayerPosition(); + mediaPlayer.pause(); + Util.sleepQuietly(10L); + reset(); + + try { + // Resetup media player + mediaPlayer.setAudioSessionId(audioSessionId); + mediaPlayer.setDataSource(currentPlaying.getFile().getCanonicalPath()); + + controller = effectsController.getEqualizerController(); + if(controller.getEqualizer() == null) { + throw new Exception("Failed to get EQ"); + } + } catch(Exception e2) { + Log.w(TAG, "Failed to setup EQ even after reinitialization"); + // Don't try again, just resetup media player and continue on + controller = null; + } + + // Restart from same position and state we left off in + play(getCurrentPlayingIndex(), false, pos); + } + + return controller; + } + + public boolean isSeekable() { + return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING; + } + + public void updateRemoteVolume(boolean up) { + AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); + audioManager.adjustVolume(up ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI); + } + + private synchronized void bufferAndPlay() { + bufferAndPlay(0); + } + private synchronized void bufferAndPlay(int position) { + bufferAndPlay(position, true); + } + private synchronized void bufferAndPlay(int position, boolean start) { + if(!currentPlaying.isCompleteFileAvailable()) { + if(Util.isAllowedToDownload(this)) { + reset(); + + bufferTask = new BufferTask(currentPlaying, position, start); + bufferTask.execute(); + } else { + next(false, start); + } + } else { + doPlay(currentPlaying, position, start); + } + } + + private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { + try { + subtractPosition = 0; + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.setOnPreparedListener(null); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.reset(); + setPlayerState(IDLE); + try { + mediaPlayer.setAudioSessionId(audioSessionId); + } catch(Throwable e) { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + + String dataSource; + boolean isPartial = false; + downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + isPartial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + dataSource = file.getAbsolutePath(); + if (isPartial && !Util.isOffline(this)) { + if (proxy == null) { + proxy = new BufferProxy(this); + proxy.start(); + } + proxy.setBufferFile(downloadFile); + dataSource = proxy.getPrivateAddress(dataSource); + Log.i(TAG, "Data Source: " + dataSource); + } else if (proxy != null) { + proxy.stop(); + proxy = null; + } + + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + Log.i(TAG, "Buffered " + percent + "%"); + if (percent == 100) { + mediaPlayer.setOnBufferingUpdateListener(null); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setPlayerState(PREPARED); + + synchronized (DownloadService.this) { + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); + } + cachedPosition = position; + + applyReplayGain(mediaPlayer, downloadFile); + + if (start || autoPlayStart) { + mediaPlayer.start(); + setPlayerState(STARTED); + + // Disable autoPlayStart after done + autoPlayStart = false; + } else { + setPlayerState(PAUSED); + onSongProgress(); + } + + updateRemotePlaylist(); + } + + // Only call when starting, setPlayerState(PAUSED) already calls this + if(start) { + lifecycleSupport.serializeDownloadQueue(); + } + } catch (Exception x) { + handleError(x); + } + } + }); + + setupHandlers(downloadFile, isPartial, start); + + mediaPlayer.prepareAsync(); + } catch (Exception x) { + handleError(x); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private synchronized void setupNext(final DownloadFile downloadFile) { + try { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + resetNext(); + + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK); + try { + nextMediaPlayer.setAudioSessionId(audioSessionId); + } catch(Throwable e) { + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + // Changed to different while preparing so ignore + if(nextMediaPlayer != mp) { + return; + } + + try { + setNextPlayerState(PREPARED); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) { + mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } + + applyReplayGain(nextMediaPlayer, downloadFile); + } catch (Exception x) { + handleErrorNext(x); + } + } + }); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile); + return true; + } + }); + + nextMediaPlayer.prepareAsync(); + } catch (Exception x) { + handleErrorNext(x); + } + } + + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial, final boolean isPlaying) { + final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile); + int pos = getPlayerPosition(); + reset(); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + playNext(); + } else { + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, isPlaying); + downloadFile.setPlaying(true); + } + return true; + } + }); + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + setPlayerStateCompleted(); + + int pos = getPlayerPosition(); + Log.i(TAG, "Ending position " + pos + " of " + duration); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000)) || nextSetup) { + playNext(); + postPlayCleanup(downloadFile); + } else { + // If file is not completely downloaded, restart the playback from the current position. + synchronized (DownloadService.this) { + if (downloadFile.isWorkDone()) { + // Complete was called early even though file is fully buffered + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } else { + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + bufferTask = new BufferTask(downloadFile, pos, true); + bufferTask.execute(); + } + } + checkDownloads(); + } + } + }); + } + + public void setVolume(float volume) { + if(mediaPlayer != null && (playerState == STARTED || playerState == PAUSED || playerState == STOPPED)) { + try { + this.volume = volume; + reapplyVolume(); + } catch(Exception e) { + Log.w(TAG, "Failed to set volume"); + } + } + } + + public void reapplyVolume() { + applyReplayGain(mediaPlayer, currentPlaying); + } + + public synchronized void swap(boolean mainList, DownloadFile from, DownloadFile to) { + List list = mainList ? downloadList : backgroundDownloadList; + swap(mainList, list.indexOf(from), list.indexOf(to)); + } + public synchronized void swap(boolean mainList, int from, int to) { + List list = mainList ? downloadList : backgroundDownloadList; + int max = list.size(); + if(to >= max) { + to = max - 1; + } + else if(to < 0) { + to = 0; + } + + DownloadFile movedSong = list.remove(from); + list.add(to, movedSong); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + if(mainList) { + // Moving next playing, current playing, or moving a song to be next playing + if(movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to) { + setNextPlaying(); + } + } + } + + public synchronized void serializeQueue() { + serializeQueue(true); + } + public synchronized void serializeQueue(boolean serializeRemote) { + if(playerState == PlayerState.PAUSED) { + lifecycleSupport.serializeDownloadQueue(serializeRemote); + } + } + + private void handleError(Exception x) { + Log.w(TAG, "Media player error: " + x, x); + if(mediaPlayer != null) { + try { + mediaPlayer.reset(); + } catch(Exception e) { + Log.e(TAG, "Failed to reset player in error handler"); + } + } + setPlayerState(IDLE); + } + private void handleErrorNext(Exception x) { + Log.w(TAG, "Next Media player error: " + x, x); + try { + nextMediaPlayer.reset(); + } catch(Exception e) { + Log.e(TAG, "Failed to reset next media player", x); + } + setNextPlayerState(IDLE); + } + + public synchronized void checkDownloads() { + if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { + return; + } + + if(removePlayed) { + checkRemovePlayed(); + } + if (shufflePlay) { + checkShufflePlay(); + } + + if (!Util.isAllowedToDownload(this)) { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { + return; + } + + // Need to download current playing? + if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + + currentDownloading = currentPlaying; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + } + + // Find a suitable target for download. + else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) { + currentDownloading = null; + int n = size(); + + int preloaded = 0; + + if(n != 0) { + int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if(start == -1) { + start = 0; + } + int i = start; + do { + DownloadFile downloadFile = downloadList.get(i); + if (!downloadFile.isWorkDone() && !downloadFile.isFailedMax()) { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + if(i == (start + 1)) { + setNextPlayerState(DOWNLOADING); + } + break; + } + } else if (currentPlaying != downloadFile) { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) { + for(int i = 0; i < backgroundDownloadList.size(); i++) { + DownloadFile downloadFile = backgroundDownloadList.get(i); + if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()) || downloadFile.isFailedMax()) { + // Don't need to keep list like active song list + backgroundDownloadList.remove(i); + revision++; + i--; + } else { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + break; + } + } + } + } + + if(!backgroundDownloadList.isEmpty()) { + Notifications.showDownloadingNotification(this, this, handler, currentDownloading, backgroundDownloadList.size()); + downloadOngoing = true; + } else if(backgroundDownloadList.isEmpty() && downloadOngoing) { + Notifications.hideDownloadingNotification(this, this, handler); + downloadOngoing = false; + } + + // Delete obsolete .partial and .complete files. + cleanup(); + } + + private synchronized void checkRemovePlayed() { + boolean changed = false; + SharedPreferences prefs = Util.getPreferences(this); + int keepCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT, "0")); + while(currentPlayingIndex > keepCount) { + downloadList.remove(0); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + changed = true; + } + + if(changed) { + revision++; + onSongsChanged(); + } + } + + private synchronized void checkShufflePlay() { + + // Get users desired random playlist size + SharedPreferences prefs = Util.getPreferences(this); + int listSize = Math.max(1, Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"))); + boolean wasEmpty = downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = size(); + if (size < listSize) { + for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) { + DownloadFile downloadFile = new DownloadFile(this, song, false); + downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) { + downloadList.add(new DownloadFile(this, song, false)); + downloadList.get(0).cancelDownload(); + downloadList.remove(0); + revision++; + } + } + currentPlayingIndex = downloadList.indexOf(currentPlaying); + + if (revisionBefore != revision) { + onSongsChanged(); + updateRemotePlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) { + play(0); + } + } + + public long getDownloadListUpdateRevision() { + return revision; + } + + private synchronized void cleanup() { + Iterator iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != currentPlaying && downloadFile != currentDownloading) { + if (downloadFile.cleanup()) { + iterator.remove(); + } + } + } + } + + public void postPlayCleanup() { + postPlayCleanup(currentPlaying); + } + public void postPlayCleanup(DownloadFile downloadFile) { + if(downloadFile == null) { + return; + } + } + + private boolean isPastCutoff() { + return isPastCutoff(getPlayerPosition(), getPlayerDuration()); + } + private boolean isPastCutoff(int position, int duration) { + return isPastCutoff(position, duration, false); + } + private boolean isPastCutoff(int position, int duration, boolean allowSkipping) { + if(currentPlaying == null) { + return false; + } + + // Make cutoff a maximum of 10 minutes + int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000); + boolean isPastCutoff = duration > 0 && position > cutoffPoint; + + return isPastCutoff; + } + + private void applyReplayGain(MediaPlayer mediaPlayer, DownloadFile downloadFile) { + if(currentPlaying == null) { + return; + } + + SharedPreferences prefs = Util.getPreferences(this); + try { + float adjust = 0f; + if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) { + float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ + boolean singleAlbum = false; + + String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1"); + // 1 => Smart replay gain + if("1".equals(replayGainType)) { + // Check if part of at least consequetive songs of the same album + + int index = downloadList.indexOf(downloadFile); + if(index != -1) { + String albumName = downloadFile.getSong().getAlbum(); + int matched = 0; + + // Check forwards + for(int i = index + 1; i < downloadList.size() && matched < REQUIRED_ALBUM_MATCHES; i++) { + if(albumName.equals(downloadList.get(i).getSong().getAlbum())) { + matched++; + } else { + break; + } + } + + // Check backwards + for(int i = index - 1; i >= 0 && matched < REQUIRED_ALBUM_MATCHES; i--) { + if(albumName.equals(downloadList.get(i).getSong().getAlbum())) { + matched++; + } else { + break; + } + } + + if(matched >= REQUIRED_ALBUM_MATCHES) { + singleAlbum = true; + } + } + } + // 2 => Use album tags + else if("2".equals(replayGainType)) { + singleAlbum = true; + } + // 3 => Use track tags + // Already false, no need to do anything here + + + // If playing a single album or no track gain, use album gain + if((singleAlbum || rg[0] == 0) && rg[1] != 0) { + adjust = rg[1]; + } else { + // Otherwise, give priority to track gain + adjust = rg[0]; + } + + if (adjust == 0) { + /* No RG value found: decrease volume for untagged song if requested by user */ + int untagged = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED, "0")); + adjust = (untagged - 150) / 10f; + } else { + int bump = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP, "150")); + adjust += (bump - 150) / 10f; + } + } + + float rg_result = ((float) Math.pow(10, (adjust / 20))) * volume; + if (rg_result > 1.0f) { + rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */ + } else if (rg_result < 0.0f) { + rg_result = 0.0f; + } + mediaPlayer.setVolume(rg_result, rg_result); + } catch(IOException e) { + Log.w(TAG, "Failed to apply replay gain values", e); + } + } + + private synchronized boolean isNextPlayingSameAlbum() { + return isNextPlayingSameAlbum(currentPlaying, nextPlaying); + } + private synchronized boolean isNextPlayingSameAlbum(DownloadFile currentPlaying, DownloadFile nextPlaying) { + if(currentPlaying == null || nextPlaying == null) { + return false; + } else { + return currentPlaying.getSong().getAlbum().equals(nextPlaying.getSong().getAlbum()); + } + } + + public void acquireWakelock() { + acquireWakelock(30000); + } + public void acquireWakelock(int ms) { + wakeLock.acquire(ms); + } + + public void handleKeyEvent(KeyEvent keyEvent) { + lifecycleSupport.handleKeyEvent(keyEvent); + } + + public void addOnSongChangedListener(OnSongChangedListener listener) { + addOnSongChangedListener(listener, false); + } + public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index == -1) { + onSongChangedListeners.add(listener); + } + } + + if(run) { + if(mediaPlayerHandler != null) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongsChanged(); + onSongProgress(); + onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); + } + }); + } else { + runListenersOnInit = true; + } + } + } + public void removeOnSongChangeListener(OnSongChangedListener listener) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index != -1) { + onSongChangedListeners.remove(index); + } + } + } + + private void onSongChanged() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongChanged(currentPlaying, currentPlayingIndex); + + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + } + } + }); + } + + if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongProgress(); + } + }); + } + } + } + private void onSongsChanged() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + } + } + }); + } + } + } + + private void onSongProgress() { + onSongProgress(true); + } + private synchronized void onSongProgress(boolean manual) { + final long atRevision = revision; + final Integer duration = getPlayerDuration(); + final boolean isSeekable = isSeekable(); + final int position = getPlayerPosition(); + + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongProgress(currentPlaying, position, duration, isSeekable); + } + } + }); + } + } + + if(manual) { + handler.post(new Runnable() { + @Override + public void run() { + } + }); + } + } + private void onStateUpdate() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onStateUpdate(currentPlaying, playerState); + } + } + }); + } + } + } + public void onMetadataUpdate() { + onMetadataUpdate(METADATA_UPDATED_ALL); + } + public void onMetadataUpdate(final int updateType) { + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (instance != null) { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } + } + }); + } + } + + handler.post(new Runnable() { + @Override + public void run() { + } + }); + } + + private class BufferTask extends SilentBackgroundTask { + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + private final boolean start; + + public BufferTask(DownloadFile downloadFile, int position, boolean start) { + super(instance); + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + this.start = start; + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024L / 8L * 5L); + + // Find out how large the file should grow before resuming playback. + Log.i(TAG, "Buffering from position " + position + " and bitrate " + bitRate); + expectedFileSize = (position * bitRate / 8) + byteCount; + } + + @Override + public Void doInBackground() throws InterruptedException { + setPlayerState(DOWNLOADING); + + while (!bufferComplete()) { + Thread.sleep(1000L); + if (isCancelled() || downloadFile.isFailedMax()) { + return null; + } else if(!downloadFile.isFailedMax() && !downloadFile.isDownloading()) { + checkDownloads(); + } + } + doPlay(downloadFile, position, start); + + return null; + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + long size = partialFile.length(); + + Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() { + return "BufferTask (" + downloadFile + ")"; + } + } + + private class CheckCompletionTask extends SilentBackgroundTask { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) { + super(instance); + this.downloadFile = downloadFile; + if(downloadFile != null) { + partialFile = downloadFile.getPartialFile(); + } else { + partialFile = null; + } + } + + @Override + public Void doInBackground() throws InterruptedException { + if(downloadFile == null) { + return null; + } + + // Do an initial sleep so this prepare can't compete with main prepare + Thread.sleep(5000L); + while (!bufferComplete()) { + Thread.sleep(5000L); + if (isCancelled()) { + return null; + } + } + + // Start the setup of the next media player + mediaPlayerHandler.post(new Runnable() { + public void run() { + if(!CheckCompletionTask.this.isCancelled()) { + setupNext(downloadFile); + } + } + }); + return null; + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + "): " + completeFileAvailable); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() { + return "CheckCompletionTask (" + downloadFile + ")"; + } + } + + public interface OnSongChangedListener { + void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex); + void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex); + void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable); + void onStateUpdate(DownloadFile downloadFile, PlayerState playerState); + void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java new file mode 100644 index 0000000..3b9b075 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java @@ -0,0 +1,430 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.KeyEvent; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.util.CacheCleaner; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.domain.PlayerState.PREPARING; + +/** + * @author Sindre Mehus + */ +public class DownloadServiceLifecycleSupport { + private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); + public static final String FILENAME_DOWNLOADS_SER = "downloadstate2.ser"; + private static final int DEBOUNCE_TIME = 200; + + private final DownloadService downloadService; + private Looper eventLooper; + private Handler eventHandler; + private BroadcastReceiver ejectEventReceiver; + private PhoneStateListener phoneStateListener; + private boolean externalStorageAvailable= true; + private ReentrantLock lock = new ReentrantLock(); + private final AtomicBoolean setup = new AtomicBoolean(false); + private long lastPressTime = 0; + private SilentBackgroundTask currentSavePlayQueueTask = null; + private Date lastChange = null; + + /** + * This receiver manages the intent that could come from other applications. + */ + private BroadcastReceiver intentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + eventHandler.post(new Runnable() { + @Override + public void run() { + String action = intent.getAction(); + Log.i(TAG, "intentReceiver.onReceive: " + action); + if (DownloadService.CMD_PLAY.equals(action)) { + downloadService.play(); + } else if (DownloadService.CMD_NEXT.equals(action)) { + downloadService.next(); + } else if (DownloadService.CMD_PREVIOUS.equals(action)) { + downloadService.previous(); + } else if (DownloadService.CMD_TOGGLEPAUSE.equals(action)) { + downloadService.togglePlayPause(); + } else if (DownloadService.CMD_PAUSE.equals(action)) { + downloadService.pause(); + } else if (DownloadService.CMD_STOP.equals(action)) { + downloadService.pause(); + downloadService.seekTo(0); + } + } + }); + } + }; + + + public DownloadServiceLifecycleSupport(DownloadService downloadService) { + this.downloadService = downloadService; + } + + public void onCreate() { + new Thread(new Runnable() { + @Override + public void run() { + Looper.prepare(); + eventLooper = Looper.myLooper(); + eventHandler = new Handler(eventLooper); + + // Deserialize queue before starting looper + try { + lock.lock(); + deserializeDownloadQueueNow(); + + // Wait until PREPARING is done to mark lifecycle as ready to receive events + while(downloadService.getPlayerState() == PREPARING) { + Util.sleepQuietly(50L); + } + + setup.set(true); + } finally { + lock.unlock(); + } + + Looper.loop(); + } + }, "DownloadServiceLifecycleSupport").start(); + + // Stop when SD card is ejected. + ejectEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); + if (!externalStorageAvailable) { + Log.i(TAG, "External media is ejecting. Stopping playback."); + downloadService.reset(); + } else { + Log.i(TAG, "External media is available."); + } + } + }; + IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); + ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + ejectFilter.addDataScheme("file"); + downloadService.registerReceiver(ejectEventReceiver, ejectFilter); + + // React to media buttons. + Util.registerMediaButtonEventReceiver(downloadService); + + // Pause temporarily on incoming phone calls. + phoneStateListener = new MyPhoneStateListener(); + + // Android 6.0 removes requirement for android.Manifest.permission.READ_PHONE_STATE; + TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + + // Register the handler for outside intents. + IntentFilter commandFilter = new IntentFilter(); + commandFilter.addAction(DownloadService.CMD_PLAY); + commandFilter.addAction(DownloadService.CMD_TOGGLEPAUSE); + commandFilter.addAction(DownloadService.CMD_PAUSE); + commandFilter.addAction(DownloadService.CMD_STOP); + commandFilter.addAction(DownloadService.CMD_PREVIOUS); + commandFilter.addAction(DownloadService.CMD_NEXT); + commandFilter.addAction(DownloadService.CANCEL_DOWNLOADS); + downloadService.registerReceiver(intentReceiver, commandFilter); + + new CacheCleaner(downloadService, downloadService).clean(); + } + + public boolean isInitialized() { + return setup.get(); + } + + public void onStart(final Intent intent) { + if (intent != null) { + final String action = intent.getAction(); + + if(eventHandler == null) { + Util.sleepQuietly(100L); + } + if(eventHandler == null) { + return; + } + + eventHandler.post(new Runnable() { + @Override + public void run() { + if(!setup.get()) { + lock.lock(); + lock.unlock(); + } + + if(DownloadService.START_PLAY.equals(action)) { + int offlinePref = intent.getIntExtra(Constants.PREFERENCES_KEY_OFFLINE, 0); + if(offlinePref != 0) { + boolean offline = (offlinePref == 2); + Util.setOffline(downloadService, offline); + if (offline) { + downloadService.clearIncomplete(); + } else { + downloadService.checkDownloads(); + } + } + + if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) { + // Add shuffle parameters + SharedPreferences.Editor editor = Util.getPreferences(downloadService).edit(); + String startYear = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR); + if(startYear != null) { + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear); + } + + String endYear = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR); + if(endYear != null) { + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear); + } + + String genre = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_GENRE); + if(genre != null) { + editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); + } + editor.commit(); + + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); + } else { + downloadService.start(); + } + } else if(DownloadService.CMD_TOGGLEPAUSE.equals(action)) { + downloadService.togglePlayPause(); + } else if(DownloadService.CMD_NEXT.equals(action)) { + downloadService.next(); + } else if(DownloadService.CMD_PREVIOUS.equals(action)) { + downloadService.previous(); + } else if(DownloadService.CANCEL_DOWNLOADS.equals(action)) { + downloadService.clearBackground(); + } else if(intent.getExtras() != null) { + final KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + if (event != null) { + handleKeyEvent(event); + } + } + } + }); + } + } + + public void onDestroy() { + serializeDownloadQueue(); + eventLooper.quit(); + downloadService.unregisterReceiver(ejectEventReceiver); + downloadService.unregisterReceiver(intentReceiver); + + TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + } + + public boolean isExternalStorageAvailable() { + return externalStorageAvailable; + } + + public void serializeDownloadQueue() { + serializeDownloadQueue(true); + } + public void serializeDownloadQueue(final boolean serializeRemote) { + if(!setup.get()) { + return; + } + + final List songs = new ArrayList(downloadService.getSongs()); + eventHandler.post(new Runnable() { + @Override + public void run() { + if(lock.tryLock()) { + try { + serializeDownloadQueueNow(songs, serializeRemote); + } finally { + lock.unlock(); + } + } + } + }); + } + + public void serializeDownloadQueueNow(List songs, boolean serializeRemote) { + final PlayerQueue state = new PlayerQueue(); + for (DownloadFile downloadFile : songs) { + state.songs.add(downloadFile.getSong()); + } + for (DownloadFile downloadFile : downloadService.getToDelete()) { + state.toDelete.add(downloadFile.getSong()); + } + state.currentPlayingIndex = downloadService.getCurrentPlayingIndex(); + state.currentPlayingPosition = downloadService.getPlayerPosition(); + + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + if(currentPlaying != null) { + state.renameCurrent = currentPlaying.isWorkDone() && !currentPlaying.isCompleteFileAvailable(); + } + state.changed = lastChange = new Date(); + + Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); + } + + public void post(Runnable runnable) { + eventHandler.post(runnable); + } + + private void deserializeDownloadQueueNow() { + PlayerQueue state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER, PlayerQueue.class); + if (state == null) { + return; + } + Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + + // Rename first thing before anything else starts + if(state.renameCurrent && state.currentPlayingIndex != -1 && state.currentPlayingIndex < state.songs.size()) { + DownloadFile currentPlaying = new DownloadFile(downloadService, state.songs.get(state.currentPlayingIndex), false); + currentPlaying.renamePartial(); + } + + downloadService.restore(state.songs, state.toDelete, state.currentPlayingIndex, state.currentPlayingPosition); + + if(state != null) { + lastChange = state.changed; + } + } + + public Date getLastChange() { + return lastChange; + } + + public void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.fastForward(); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.rewind(); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if(lastPressTime < (System.currentTimeMillis() - 500)) { + lastPressTime = System.currentTimeMillis(); + downloadService.togglePlayPause(); + } else { + downloadService.next(false, true); + } + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.previous(); + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + downloadService.rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + downloadService.fastForward(); + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + downloadService.stop(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if(downloadService.getPlayerState() != PlayerState.STARTED) { + downloadService.start(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + downloadService.pause(); + default: + break; + } + } + } + + /** + * Logic taken from packages/apps/Music. Will pause when an incoming + * call rings or if a call (incoming or outgoing) is connected. + */ + private class MyPhoneStateListener extends PhoneStateListener { + private boolean resumeAfterCall; + + @Override + public void onCallStateChanged(final int state, String incomingNumber) { + eventHandler.post(new Runnable() { + @Override + public void run() { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + case TelephonyManager.CALL_STATE_OFFHOOK: + if (downloadService.getPlayerState() == PlayerState.STARTED) { + resumeAfterCall = true; + downloadService.pause(true); + } + break; + case TelephonyManager.CALL_STATE_IDLE: + if (resumeAfterCall) { + resumeAfterCall = false; + if(downloadService.getPlayerState() == PlayerState.PAUSED_TEMP) { + downloadService.start(); + } + } + break; + default: + break; + } + } + }); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/HeadphoneListenerService.java b/app/src/main/java/github/nvllsvm/audinaut/service/HeadphoneListenerService.java new file mode 100644 index 0000000..6bcdec6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/HeadphoneListenerService.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service; + +import android.app.Service; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; + +import github.nvllsvm.audinaut.receiver.HeadphonePlugReceiver; +import github.nvllsvm.audinaut.util.Util; + +/** + * Created by Scott on 4/6/2015. + */ +public class HeadphoneListenerService extends Service { + private HeadphonePlugReceiver receiver; + + @Override + public void onCreate() { + super.onCreate(); + + receiver = new HeadphonePlugReceiver(); + registerReceiver(receiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if(!Util.shouldStartOnHeadphones(this)) { + stopSelf(); + } + + return Service.START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + try { + if(receiver != null) { + unregisterReceiver(receiver); + } + } catch(Exception e) { + // Don't care + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java b/app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java new file mode 100644 index 0000000..94fa79b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java @@ -0,0 +1,151 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.io.File; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.Log; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class MediaStoreService { + + private static final String TAG = MediaStoreService.class.getSimpleName(); + private static final Uri ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart"); + + private final Context context; + + public MediaStoreService(Context context) { + this.context = context; + } + + public void saveInMediaStore(DownloadFile downloadFile) { + MusicDirectory.Entry song = downloadFile.getSong(); + File songFile = downloadFile.getCompleteFile(); + + // Delete existing row in case the song has been downloaded before. + deleteFromMediaStore(downloadFile); + + ContentResolver contentResolver = context.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.TITLE, song.getTitle()); + values.put(MediaStore.MediaColumns.DATA, songFile.getAbsolutePath()); + values.put(MediaStore.Audio.AudioColumns.ARTIST, song.getArtist()); + values.put(MediaStore.Audio.AudioColumns.ALBUM, song.getAlbum()); + if (song.getDuration() != null) { + values.put(MediaStore.Audio.AudioColumns.DURATION, song.getDuration() * 1000L); + } + if (song.getTrack() != null) { + values.put(MediaStore.Audio.AudioColumns.TRACK, song.getTrack()); + } + if (song.getYear() != null) { + values.put(MediaStore.Audio.AudioColumns.YEAR, song.getYear()); + } + if(song.getTranscodedContentType() != null) { + values.put(MediaStore.MediaColumns.MIME_TYPE, song.getTranscodedContentType()); + } else { + values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType()); + } + values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, 1); + + Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values); + + // Look up album, and add cover art if found. + Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Audio.AudioColumns.ALBUM_ID}, null, null, null); + if (cursor.moveToFirst()) { + int albumId = cursor.getInt(0); + insertAlbumArt(albumId, downloadFile); + } + + cursor.close(); + } + + public void deleteFromMediaStore(DownloadFile downloadFile) { + ContentResolver contentResolver = context.getContentResolver(); + MusicDirectory.Entry song = downloadFile.getSong(); + File file = downloadFile.getCompleteFile(); + + Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + + int n = contentResolver.delete(uri, + MediaStore.MediaColumns.DATA + "=?", + new String[]{file.getAbsolutePath()}); + if (n > 0) { + Log.i(TAG, "Deleting media store row for " + song); + } + } + + public void deleteFromMediaStore(File file) { + ContentResolver contentResolver = context.getContentResolver(); + + Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + + int n = contentResolver.delete(uri, + MediaStore.MediaColumns.DATA + "=?", + new String[]{file.getAbsolutePath()}); + if (n > 0) { + Log.i(TAG, "Deleting media store row for " + file); + } + } + + public void renameInMediaStore(File start, File end) { + ContentResolver contentResolver = context.getContentResolver(); + + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, end.getAbsolutePath()); + + int n = contentResolver.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + values, + MediaStore.MediaColumns.DATA + "=?", + new String[]{start.getAbsolutePath()}); + if (n > 0) { + Log.i(TAG, "Rename media store row for " + start + " to " + end); + } + } + + private void insertAlbumArt(int albumId, DownloadFile downloadFile) { + ContentResolver contentResolver = context.getContentResolver(); + + Cursor cursor = contentResolver.query(Uri.withAppendedPath(ALBUM_ART_URI, String.valueOf(albumId)), null, null, null, null); + if (!cursor.moveToFirst()) { + + // No album art found, add it. + File albumArtFile = FileUtil.getAlbumArtFile(context, downloadFile.getSong()); + if (albumArtFile.exists()) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId); + values.put(MediaStore.MediaColumns.DATA, albumArtFile.getPath()); + contentResolver.insert(ALBUM_ART_URI, values); + Log.i(TAG, "Added album art: " + albumArtFile); + } + } + cursor.close(); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java new file mode 100644 index 0000000..b672681 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java @@ -0,0 +1,123 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.util.List; + +import org.apache.http.HttpResponse; + +import android.content.Context; +import android.graphics.Bitmap; + +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.Version; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public interface MusicService { + + void ping(Context context, ProgressListener progressListener) throws Exception; + + List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + void startRescan(Context context, ProgressListener listener) throws Exception; + + Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception; + + List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception; + + void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception; + + void addToPlaylist(String id, List toAdd, Context context, ProgressListener progressListener) throws Exception; + + void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception; + + void overwritePlaylist(String id, String name, int toRemove, List toAdd, Context context, ProgressListener progressListener) throws Exception; + + void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; + + String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception; + + Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; + + HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception; + + String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception; + + List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception; + + User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception; + + List getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + void createUser(User user, Context context, ProgressListener progressListener) throws Exception; + + void updateUser(User user, Context context, ProgressListener progressListener) throws Exception; + + void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception; + + void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception; + + void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception; + + Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; + + void savePlayQueue(List songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception; + + PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception; + + void setInstance(Integer instance) throws Exception; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java b/app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java new file mode 100644 index 0000000..7a30acc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java @@ -0,0 +1,36 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import android.content.Context; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class MusicServiceFactory { + + private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService()); + private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService(); + + public static MusicService getMusicService(Context context) { + return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/OfflineException.java b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineException.java new file mode 100644 index 0000000..d0c7b18 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineException.java @@ -0,0 +1,32 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +/** + * Thrown by service methods that are not available in offline mode. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class OfflineException extends Exception { + + public OfflineException(String message) { + super(message); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java new file mode 100644 index 0000000..c6e9bde --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java @@ -0,0 +1,638 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.io.File; +import java.io.Reader; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.util.Log; + +import org.apache.http.HttpResponse; + +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.Util; +import java.io.*; +import java.util.Comparator; +import java.util.SortedSet; + +/** + * @author Sindre Mehus + */ +public class OfflineMusicService implements MusicService { + private static final String TAG = OfflineMusicService.class.getSimpleName(); + private static final String ERRORMSG = "Not available in offline mode"; + private static final Random random = new Random(); + + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List artists = new ArrayList(); + List entries = new ArrayList<>(); + File root = FileUtil.getMusicDirectory(context); + for (File file : FileUtil.listFiles(root)) { + if (file.isDirectory()) { + Artist artist = new Artist(); + artist.setId(file.getPath()); + artist.setIndex(file.getName().substring(0, 1)); + artist.setName(file.getName()); + artists.add(artist); + } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { + entries.add(createEntry(context, file)); + } + } + + Indexes indexes = new Indexes(0L, Collections.emptyList(), artists, entries); + return indexes; + } + + @Override + public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + return getMusicDirectory(id, artistName, refresh, context, progressListener, false); + } + private MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener, boolean isPodcast) throws Exception { + File dir = new File(id); + MusicDirectory result = new MusicDirectory(); + result.setName(dir.getName()); + + Set names = new HashSet(); + + for (File file : FileUtil.listMediaFiles(dir)) { + String name = getName(file); + if (name != null & !names.contains(name)) { + names.add(name); + result.addChild(createEntry(context, file, name, true, isPodcast)); + } + } + result.sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true)); + return result; + } + + @Override + public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + private String getName(File file) { + String name = file.getName(); + if (file.isDirectory()) { + return name; + } + + if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) { + return null; + } + + name = name.replace(".complete", ""); + return FileUtil.getBaseName(name); + } + + private Entry createEntry(Context context, File file) { + return createEntry(context, file, getName(file)); + } + private Entry createEntry(Context context, File file, String name) { + return createEntry(context, file, name, true); + } + private Entry createEntry(Context context, File file, String name, boolean load) { + return createEntry(context, file, name, load, false); + } + private Entry createEntry(Context context, File file, String name, boolean load, boolean isPodcast) { + Entry entry; + entry = new Entry(); + entry.setDirectory(file.isDirectory()); + entry.setId(file.getPath()); + entry.setParent(file.getParent()); + entry.setSize(file.length()); + String root = FileUtil.getMusicDirectory(context).getPath(); + if(!file.getParentFile().getParentFile().getPath().equals(root)) { + entry.setGrandParent(file.getParentFile().getParent()); + } + entry.setPath(file.getPath().replaceFirst("^" + root + "/" , "")); + String title = name; + if (file.isFile()) { + File artistFolder = file.getParentFile().getParentFile(); + File albumFolder = file.getParentFile(); + if(artistFolder.getPath().equals(root)) { + entry.setArtist(albumFolder.getName()); + } else { + entry.setArtist(artistFolder.getName()); + } + entry.setAlbum(albumFolder.getName()); + + int index = name.indexOf('-'); + if(index != -1) { + try { + entry.setTrack(Integer.parseInt(name.substring(0, index))); + title = title.substring(index + 1); + } catch(Exception e) { + // Failed parseInt, just means track filled out + } + } + + if(load) { + entry.loadMetadata(file); + } + } + + entry.setTitle(title); + entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); + + File albumArt = FileUtil.getAlbumArtFile(context, entry); + if (albumArt.exists()) { + entry.setCoverArt(albumArt.getPath()); + } + return entry; + } + + @Override + public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + try { + return FileUtil.getAlbumArtBitmap(context, entry, size); + } catch(Exception e) { + return null; + } + } + + @Override + public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void startRescan(Context context, ProgressListener listener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { + List artists = new ArrayList(); + List albums = new ArrayList(); + List songs = new ArrayList(); + File root = FileUtil.getMusicDirectory(context); + int closeness = 0; + for (File artistFile : FileUtil.listFiles(root)) { + String artistName = artistFile.getName(); + if (artistFile.isDirectory()) { + if((closeness = matchCriteria(criteria, artistName)) > 0) { + Artist artist = new Artist(); + artist.setId(artistFile.getPath()); + artist.setIndex(artistFile.getName().substring(0, 1)); + artist.setName(artistName); + artist.setCloseness(closeness); + artists.add(artist); + } + + recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs); + } + } + + Collections.sort(artists, new Comparator() { + public int compare(Artist lhs, Artist rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + Collections.sort(albums, new Comparator() { + public int compare(Entry lhs, Entry rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + Collections.sort(songs, new Comparator() { + public int compare(Entry lhs, Entry rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + + // Respect counts in search criteria + int artistCount = Math.min(artists.size(), criteria.getArtistCount()); + int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); + int songCount = Math.min(songs.size(), criteria.getSongCount()); + artists = artists.subList(0, artistCount); + albums = albums.subList(0, albumCount); + songs = songs.subList(0, songCount); + + return new SearchResult(artists, albums, songs); + } + + private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List albums, List songs) { + int closeness; + for(File albumFile : FileUtil.listMediaFiles(file)) { + if(albumFile.isDirectory()) { + String albumName = getName(albumFile); + if((closeness = matchCriteria(criteria, albumName)) > 0) { + Entry album = createEntry(context, albumFile, albumName); + album.setArtist(artistName); + album.setCloseness(closeness); + albums.add(album); + } + + for(File songFile : FileUtil.listMediaFiles(albumFile)) { + String songName = getName(songFile); + if(songName == null) { + continue; + } + + if(songFile.isDirectory()) { + recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs); + } + else if((closeness = matchCriteria(criteria, songName)) > 0){ + Entry song = createEntry(context, albumFile, songName); + song.setArtist(artistName); + song.setAlbum(albumName); + song.setCloseness(closeness); + songs.add(song); + } + } + } + else { + String songName = getName(albumFile); + if((closeness = matchCriteria(criteria, songName)) > 0) { + Entry song = createEntry(context, albumFile, songName); + song.setArtist(artistName); + song.setAlbum(songName); + song.setCloseness(closeness); + songs.add(song); + } + } + } + } + private int matchCriteria(SearchCritera criteria, String name) { + if (criteria.getPattern().matcher(name).matches()) { + return Util.getStringDistance( + criteria.getQuery().toLowerCase(), + name.toLowerCase()); + } else { + return 0; + } + } + + @Override + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List playlists = new ArrayList(); + File root = FileUtil.getPlaylistDirectory(context); + String lastServer = null; + boolean removeServer = true; + for (File folder : FileUtil.listFiles(root)) { + if(folder.isDirectory()) { + String server = folder.getName(); + SortedSet fileList = FileUtil.listFiles(folder); + for(File file: fileList) { + if(FileUtil.isPlaylistFile(file)) { + String id = file.getName(); + String filename = FileUtil.getBaseName(id); + String name = server + ": " + filename; + Playlist playlist = new Playlist(server, name); + playlist.setComment(filename); + + Reader reader = null; + BufferedReader buffer = null; + int songCount = 0; + try { + reader = new FileReader(file); + buffer = new BufferedReader(reader); + + String line = buffer.readLine(); + while( (line = buffer.readLine()) != null ){ + // No matter what, end file can't have .complete in it + line = line.replace(".complete", ""); + File entryFile = new File(line); + + // Don't add file to playlist if it doesn't exist as cached or pinned! + File checkFile = entryFile; + if(!checkFile.exists()) { + // If normal file doens't exist, check if .complete version does + checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName()) + + ".complete." + FileUtil.getExtension(entryFile.getName())); + } + + String entryName = getName(entryFile); + if(checkFile.exists() && entryName != null){ + songCount++; + } + } + + playlist.setSongCount(Integer.toString(songCount)); + } catch(Exception e) { + Log.w(TAG, "Failed to count songs in playlist", e); + } finally { + Util.close(buffer); + Util.close(reader); + } + + if(songCount > 0) { + playlists.add(playlist); + } + } + } + + if(!server.equals(lastServer) && fileList.size() > 0) { + if(lastServer != null) { + removeServer = false; + } + lastServer = server; + } + } else { + // Delete legacy playlist files + try { + folder.delete(); + } catch(Exception e) { + Log.w(TAG, "Failed to delete old playlist file: " + folder.getName()); + } + } + } + + if(removeServer) { + for(Playlist playlist: playlists) { + playlist.setName(playlist.getName().substring(playlist.getId().length() + 2)); + } + } + return playlists; + } + + @Override + public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { + DownloadService downloadService = DownloadService.getInstance(); + if (downloadService == null) { + return new MusicDirectory(); + } + + Reader reader = null; + BufferedReader buffer = null; + try { + int firstIndex = name.indexOf(id); + if(firstIndex != -1) { + name = name.substring(id.length() + 2); + } + + File playlistFile = FileUtil.getPlaylistFile(context, id, name); + reader = new FileReader(playlistFile); + buffer = new BufferedReader(reader); + + MusicDirectory playlist = new MusicDirectory(); + String line = buffer.readLine(); + if(!"#EXTM3U".equals(line)) return playlist; + + while( (line = buffer.readLine()) != null ){ + // No matter what, end file can't have .complete in it + line = line.replace(".complete", ""); + File entryFile = new File(line); + + // Don't add file to playlist if it doesn't exist as cached or pinned! + File checkFile = entryFile; + if(!checkFile.exists()) { + // If normal file doens't exist, check if .complete version does + checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName()) + + ".complete." + FileUtil.getExtension(entryFile.getName())); + } + + String entryName = getName(entryFile); + if(checkFile.exists() && entryName != null){ + playlist.addChild(createEntry(context, entryFile, entryName, false)); + } + } + + return playlist; + } finally { + Util.close(buffer); + Util.close(reader); + } + } + + @Override + public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void addToPlaylist(String id, List toAdd, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List toAdd, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + File root = FileUtil.getMusicDirectory(context); + List children = new LinkedList(); + listFilesRecursively(root, children); + MusicDirectory result = new MusicDirectory(); + + if (children.isEmpty()) { + return result; + } + for (int i = 0; i < size; i++) { + File file = children.get(random.nextInt(children.size())); + result.addChild(createEntry(context, file, getName(file))); + } + + return result; + } + + @Override + public String getCoverArtUrl(Context context, Entry entry) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public List getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void createUser(User user, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void savePlayQueue(List songs, Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void setInstance(Integer instance) throws Exception{ + throw new OfflineException(ERRORMSG); + } + + private void listFilesRecursively(File parent, List children) { + for (File file : FileUtil.listMediaFiles(parent)) { + if (file.isFile()) { + children.add(file); + } else { + listFilesRecursively(file, children); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java new file mode 100644 index 0000000..b742292 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java @@ -0,0 +1,1366 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.scheme.SocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Looper; +import android.util.Log; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.*; +import github.nvllsvm.audinaut.fragments.MainFragment; +import github.nvllsvm.audinaut.service.parser.EntryListParser; +import github.nvllsvm.audinaut.service.parser.ErrorParser; +import github.nvllsvm.audinaut.service.parser.GenreParser; +import github.nvllsvm.audinaut.service.parser.IndexesParser; +import github.nvllsvm.audinaut.service.parser.MusicDirectoryParser; +import github.nvllsvm.audinaut.service.parser.MusicFoldersParser; +import github.nvllsvm.audinaut.service.parser.PlayQueueParser; +import github.nvllsvm.audinaut.service.parser.PlaylistParser; +import github.nvllsvm.audinaut.service.parser.PlaylistsParser; +import github.nvllsvm.audinaut.service.parser.RandomSongsParser; +import github.nvllsvm.audinaut.service.parser.ScanStatusParser; +import github.nvllsvm.audinaut.service.parser.SearchResult2Parser; +import github.nvllsvm.audinaut.service.parser.SearchResultParser; +import github.nvllsvm.audinaut.service.parser.TopSongsParser; +import github.nvllsvm.audinaut.service.parser.UserParser; +import github.nvllsvm.audinaut.service.ssl.SSLSocketFactory; +import github.nvllsvm.audinaut.service.ssl.TrustSelfSignedStrategy; +import github.nvllsvm.audinaut.util.BackgroundTask; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.Util; +import java.io.*; +import java.util.zip.GZIPInputStream; + +/** + * @author Sindre Mehus + */ +public class RESTMusicService implements MusicService { + + private static final String TAG = RESTMusicService.class.getSimpleName(); + + private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000; + private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000; + private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000; + private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000; + private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000; + + // Allow 20 seconds extra timeout per MB offset. + private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0; + + private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5; + private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L; + + private final DefaultHttpClient httpClient; + private long redirectionLastChecked; + private int redirectionNetworkType = -1; + private String redirectFrom; + private String redirectTo; + private final ThreadSafeClientConnManager connManager; + private Integer instance; + + public RESTMusicService() { + + // Create and initialize default HTTP parameters + HttpParams params = new BasicHttpParams(); + ConnManagerParams.setMaxTotalConnections(params, 20); + ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20)); + HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT); + + // Turn off stale checking. Our connections break all the time anyway, + // and it's not worth it to pay the penalty of checking every time. + HttpConnectionParams.setStaleCheckingEnabled(params, false); + + // Create and initialize scheme registry + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443)); + + // Create an HttpClient with the ThreadSafeClientConnManager. + // This connection manager must be used if more than one thread will + // be using the HttpClient. + connManager = new ThreadSafeClientConnManager(params, schemeRegistry); + httpClient = new DefaultHttpClient(connManager, params); + } + + private SocketFactory createSSLSocketFactory() { + try { + return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + } catch (Throwable x) { + Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x); + return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory(); + } + } + + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "ping", null); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getMusicFolders", null); + try { + return new MusicFoldersParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void startRescan(Context context, ProgressListener listener) throws Exception { + Reader reader = getReader(context, listener, "startRescan", null); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + + // Now check if still running + boolean done = false; + while(!done) { + reader = getReader(context, null, "scanstatus", null); + try { + boolean running = new ScanStatusParser(context, getInstance(context)).parse(reader, listener); + if(running) { + // Don't run system ragged trying to query too much + Thread.sleep(100L); + } else { + done = true; + } + } catch(Exception e) { + done = true; + } finally { + Util.close(reader); + } + } + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + if (musicFolderId != null) { + parameterNames.add("musicFolderId"); + parameterValues.add(musicFolderId); + } + + Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", null, parameterNames, parameterValues); + try { + return new IndexesParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { + String search = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(search, 1, 1, 0); + SearchResult result = searchNew(critera, context, progressListener); + if(result.getArtists().size() == 1) { + id = result.getArtists().get(0).getId(); + } else if(result.getAlbums().size() == 1) { + id = result.getAlbums().get(0).getId(); + } + } + + MusicDirectory dir = null; + int index, start = 0; + while((index = id.indexOf(';', start)) != -1) { + MusicDirectory extra = getMusicDirectoryImpl(id.substring(start, index), name, refresh, context, progressListener); + if(dir == null) { + dir = extra; + } else { + dir.addChildren(extra.getChildren()); + } + + start = index + 1; + } + MusicDirectory extra = getMusicDirectoryImpl(id.substring(start), name, refresh, context, progressListener); + if(dir == null) { + dir = extra; + } else { + dir.addChildren(extra.getChildren()); + } + + return dir; + } + + private MusicDirectory getMusicDirectoryImpl(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id); + try { + return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getArtist", null, "id", id); + try { + return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getAlbum", null, "id", id); + try { + return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + return searchNew(critera, context, progressListener); + } + + /** + * Search using the "search" REST method. + */ + private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = Arrays.asList("any", "songCount"); + List parameterValues = Arrays.asList(critera.getQuery(), critera.getSongCount()); + Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues); + try { + return new SearchResultParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + /** + * Search using the "search2" REST method, available in 1.4.0 and later. + */ + private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount"); + List parameterValues = Arrays.asList(critera.getQuery(), critera.getArtistCount(), critera.getAlbumCount(), critera.getSongCount()); + + int instance = getInstance(context); + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "search3"; + } else { + method = "search2"; + } + Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); + try { + return new SearchResult2Parser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST); + + Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id); + try { + return new PlaylistParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getPlaylists", null); + try { + return new PlaylistsParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new LinkedList(); + List parameterValues = new LinkedList(); + + if (id != null) { + parameterNames.add("playlistId"); + parameterValues.add(id); + } + if (name != null) { + parameterNames.add("name"); + parameterValues.add(name); + } + for (MusicDirectory.Entry entry : entries) { + parameterNames.add("songId"); + parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener)); + } + + Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void addToPlaylist(String id, List toAdd, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + names.add("playlistId"); + values.add(id); + for(MusicDirectory.Entry song: toAdd) { + names.add("songIdToAdd"); + values.add(getOfflineSongId(song.getId(), context, progressListener)); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + names.add("playlistId"); + values.add(id); + for(Integer song: toRemove) { + names.add("songIndexToRemove"); + values.add(song); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List toAdd, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + names.add("playlistId"); + values.add(id); + names.add("name"); + values.add(name); + for(MusicDirectory.Entry song: toAdd) { + names.add("songIdToAdd"); + values.add(song.getId()); + } + for(int i = 0; i < toRemove; i++) { + names.add("songIndexToRemove"); + values.add(i); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.asList(id, name, comment, pub)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("type"); + values.add(type); + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + // Add folder if it was set and is non null + int instance = getInstance(context); + if(Util.getAlbumListsPerFolder(context, instance)) { + String folderId = Util.getSelectedMusicFolderId(context, instance); + if(folderId != null) { + names.add("musicFolderId"); + values.add(folderId); + } + } + + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "getAlbumList2"; + } else { + method = "getAlbumList"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("size"); + names.add("offset"); + + values.add(size); + values.add(offset); + + int instance = getInstance(context); + if("genres".equals(type)) { + names.add("type"); + values.add("byGenre"); + + names.add("genre"); + values.add(extra); + } else if("years".equals(type)) { + names.add("type"); + values.add("byYear"); + + names.add("fromYear"); + names.add("toYear"); + + int decade = Integer.parseInt(extra); + // Reverse chronological order only supported in 5.3+ + values.add(decade + 9); + values.add(decade); + } + + // Add folder if it was set and is non null + if(Util.getAlbumListsPerFolder(context, instance)) { + String folderId = Util.getSelectedMusicFolderId(context, instance); + if(folderId != null) { + names.add("musicFolderId"); + values.add(folderId); + } + } + + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "getAlbumList2"; + } else { + method = "getAlbumList"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, instance).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + String method; + switch(type) { + case MainFragment.SONGS_NEWEST: + method = "getNewaddedSongs"; + break; + case MainFragment.SONGS_TOP_PLAYED: + method = "getTopplayedSongs"; + break; + case MainFragment.SONGS_RECENT: + method = "getLastplayedSongs"; + break; + case MainFragment.SONGS_FREQUENT: + method = "getMostplayedSongs"; + break; + default: + method = "getNewaddedSongs"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("id"); + names.add("count"); + + values.add(artistId); + values.add(size); + + int instance = getInstance(context); + String method; + if (Util.isTagBrowsing(context, instance)) { + method = "getSimilarSongs2"; + } else { + method = "getSimilarSongs"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values); + try { + return new RandomSongsParser(context, instance).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("size"); + values.add(size); + + if (musicFolderId != null && !"".equals(musicFolderId) && !Util.isTagBrowsing(context, getInstance(context))) { + names.add("musicFolderId"); + values.add(musicFolderId); + } + if(genre != null && !"".equals(genre)) { + names.add("genre"); + values.add(genre); + } + if(startYear != null && !"".equals(startYear)) { + // Check to make sure user isn't doing 2015 -> 2010 since Subsonic will return no results + if(endYear != null && !"".equals(endYear)) { + try { + int startYearInt = Integer.parseInt(startYear); + int endYearInt = Integer.parseInt(endYear); + + if(startYearInt > endYearInt) { + String tmp = startYear; + startYear = endYear; + endYear = tmp; + } + } catch(Exception e) { + Log.w(TAG, "Failed to convert start/end year into ints", e); + } + } + + names.add("fromYear"); + values.add(startYear); + } + if(endYear != null && !"".equals(endYear)) { + names.add("toYear"); + values.add(endYear); + } + + Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values); + try { + return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception { + StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt")); + builder.append("&id=").append(entry.getCoverArt()); + String url = builder.toString(); + url = rewriteUrlWithRedirect(context, url); + return url; + } + + @Override + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + + // Synchronize on the entry so that we don't download concurrently for the same song. + synchronized (entry) { + + // Use cached file, if existing. + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); + if (bitmap != null) { + return bitmap; + } + + String url = getRestUrl(context, "getCoverArt"); + + InputStream in = null; + try { + List parameterNames = Arrays.asList("id"); + List parameterValues = Arrays.asList(entry.getCoverArt()); + HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task); + + in = entity.getContent(); + Header contentEncoding = entity.getContentEncoding(); + if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + in = new GZIPInputStream(in); + } + + // If content type is XML, an error occured. Get it. + String contentType = Util.getContentType(entity); + if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + return null; // Never reached. + } + + byte[] bytes = Util.toByteArray(in); + + // Handle case where partial was downloaded before being cancelled + if(task != null && task.isCancelled()) { + return null; + } + + OutputStream out = null; + try { + out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry)); + out.write(bytes); + } finally { + Util.close(out); + } + + // Size == 0 -> only want to download + if(size == 0) { + return null; + } else { + return FileUtil.getSampledBitmap(bytes, size); + } + } finally { + Util.close(in); + } + } + } + + @Override + public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + + String url = getRestUrl(context, "stream"); + + // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is + // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server. + // In that case, the server uses a long time before sending any data, causing the client to time out. + HttpParams params = new BasicHttpParams(); + int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE); + HttpConnectionParams.setSoTimeout(params, timeout); + + // Add "Range" header if offset is given. + List
headers = new ArrayList
(); + if (offset > 0) { + headers.add(new BasicHeader("Range", "bytes=" + offset + "-")); + } + + List parameterNames = new ArrayList(); + parameterNames.add("id"); + parameterNames.add("maxBitRate"); + + List parameterValues = new ArrayList(); + parameterValues.add(song.getId()); + parameterValues.add(maxBitrate); + + HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task, false); + + // If content type is XML, an error occurred. Get it. + String contentType = Util.getContentType(response.getEntity()); + if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { + InputStream in = response.getEntity().getContent(); + Header contentEncoding = response.getEntity().getContentEncoding(); + if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + in = new GZIPInputStream(in); + } + try { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + } finally { + Util.close(in); + } + } + + return response; + } + + @Override + public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception { + StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); + builder.append("&id=").append(song.getId()); + + // Allow user to specify to stream raw formats if available + builder.append("&maxBitRate=").append(maxBitrate); + + String url = builder.toString(); + url = rewriteUrlWithRedirect(context, url); + Log.i(TAG, "Using music URL: " + stripUrlInfo(url)); + return url; + } + + @Override + public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getGenres", null); + try { + return new GenreParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + parameterNames.add("genre"); + parameterValues.add(genre); + parameterNames.add("count"); + parameterValues.add(count); + parameterNames.add("offset"); + parameterValues.add(offset); + + // Add folder if it was set and is non null + int instance = getInstance(context); + if(Util.getAlbumListsPerFolder(context, instance)) { + String folderId = Util.getSelectedMusicFolderId(context, instance); + if(folderId != null) { + parameterNames.add("musicFolderId"); + parameterValues.add(folderId); + } + } + + Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues, true); + try { + return new RandomSongsParser(context, instance).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + parameterNames.add("artist"); + parameterValues.add(artist); + parameterNames.add("size"); + parameterValues.add(size); + + String method = "getTopSongs"; + Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); + try { + return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getUser", null, Arrays.asList("username"), Arrays.asList(username)); + try { + List users = new UserParser(context, getInstance(context)).parse(reader, progressListener); + if(users.size() > 0) { + // Should only have returned one anyways + return users.get(0); + } else { + return null; + } + } finally { + Util.close(reader); + } + } + + @Override + public List getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getUsers", null); + try { + return new UserParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void createUser(User user, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("username"); + values.add(user.getUsername()); + names.add("email"); + values.add(user.getEmail()); + names.add("password"); + values.add(user.getPassword()); + + for(User.Setting setting: user.getSettings()) { + names.add(setting.getName()); + values.add(setting.getValue()); + } + + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + Reader reader = getReader(context, progressListener, "createUser", null, names, values); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("username"); + values.add(user.getUsername()); + + for(User.Setting setting: user.getSettings()) { + if(setting.getName().indexOf("Role") != -1) { + names.add(setting.getName()); + values.add(setting.getValue()); + } + } + + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + Reader reader = getReader(context, progressListener, "updateUser", null, names, values); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "deleteUser", null, Arrays.asList("username"), Arrays.asList(username)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "updateUser", null, Arrays.asList("username", "email"), Arrays.asList(username, email)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "changePassword", null, Arrays.asList("username", "password"), Arrays.asList(username, password)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + // Synchronize on the url so that we don't download concurrently + synchronized (url) { + // Use cached file, if existing. + Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size); + if(bitmap != null) { + return bitmap; + } + + InputStream in = null; + try { + HttpEntity entity = getEntityForURL(context, url, null, null, null, progressListener, task); + in = entity.getContent(); + Header contentEncoding = entity.getContentEncoding(); + if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + in = new GZIPInputStream(in); + } + + // If content type is XML, an error occurred. Get it. + String contentType = Util.getContentType(entity); + if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + return null; // Never reached. + } + + byte[] bytes = Util.toByteArray(in); + if(task != null && task.isCancelled()) { + // Handle case where partial is downloaded and cancelled + return null; + } + + OutputStream out = null; + try { + out = new FileOutputStream(FileUtil.getMiscFile(context, url)); + out.write(bytes); + } finally { + Util.close(out); + } + + return FileUtil.getSampledBitmap(bytes, size, false); + } + finally { + Util.close(in); + } + } + } + + @Override + public void savePlayQueue(List songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new LinkedList(); + List parameterValues = new LinkedList(); + + for(MusicDirectory.Entry song: songs) { + parameterNames.add("id"); + parameterValues.add(song.getId()); + } + + parameterNames.add("current"); + parameterValues.add(currentPlaying.getId()); + + parameterNames.add("position"); + parameterValues.add(position); + + Reader reader = getReader(context, progressListener, "savePlayQueue", null, parameterNames, parameterValues); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getPlayQueue", null); + try { + return new PlayQueueParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + private String getOfflineSongId(String id, Context context, ProgressListener progressListener) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { + Pair cachedSongId = SongDBHandler.getHandler(context).getIdFromPath(Util.getRestUrlHash(context, getInstance(context)), id); + if(cachedSongId != null) { + id = cachedSongId.getSecond(); + } else { + String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); + SearchResult result = searchNew(critera, context, progressListener); + if (result.getSongs().size() == 1) { + id = result.getSongs().get(0).getId(); + } + } + } + + return id; + } + + @Override + public void setInstance(Integer instance) throws Exception { + this.instance = instance; + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception { + return getReader(context, progressListener, method, requestParams, false); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams, boolean throwsError) throws Exception { + return getReader(context, progressListener, method, requestParams, Collections.emptyList(), Collections.emptyList(), throwsError); + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, String parameterName, Object parameterValue) throws Exception { + return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.asList(parameterValue)); + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, List parameterNames, List parameterValues) throws Exception { + return getReader(context, progressListener, method, requestParams, parameterNames, parameterValues, false); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, List parameterNames, List parameterValues, boolean throwErrors) throws Exception { + + if (progressListener != null) { + progressListener.updateProgress(R.string.service_connecting); + } + + String url = getRestUrl(context, method); + return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors); + } + + private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List parameterValues, ProgressListener progressListener) throws Exception { + return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, true); + } + private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { + HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors); + if (entity == null) { + throw new RuntimeException("No entity received for URL " + url); + } + + InputStream in = entity.getContent(); + Header contentEncoding = entity.getContentEncoding(); + if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + in = new GZIPInputStream(in); + } + return new InputStreamReader(in, Constants.UTF_8); + } + + private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { + + return getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, null, throwErrors); + } + + private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List parameterValues, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, false).getEntity(); + } + private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List parameterValues, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsError) throws Exception { + return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, throwsError).getEntity(); + } + + private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams, + List parameterNames, List parameterValues, + List
headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsErrors) throws Exception { + // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being + // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus + // loosing its entity. + if (parameterNames != null && parameterNames.size() < 10) { + StringBuilder builder = new StringBuilder(url); + for (int i = 0; i < parameterNames.size(); i++) { + builder.append("&").append(parameterNames.get(i)).append("="); + String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"); + part = part.replaceAll("\\%27", "'"); + builder.append(part); + } + url = builder.toString(); + parameterNames = null; + parameterValues = null; + } + + String rewrittenUrl = rewriteUrlWithRedirect(context, url); + return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task, throwsErrors); + } + + private HttpResponse executeWithRetry(final Context context, String url, String originalUrl, HttpParams requestParams, + List parameterNames, List parameterValues, + List
headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwErrors) throws Exception { + // Strip out sensitive information from log + if(url.indexOf("scanstatus") == -1) { + Log.i(TAG, stripUrlInfo(url)); + } + + SharedPreferences prefs = Util.getPreferences(context); + int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); + HttpParams newParams = httpClient.getParams(); + HttpConnectionParams.setSoTimeout(newParams, networkTimeout); + httpClient.setParams(newParams); + + final AtomicReference isCancelled = new AtomicReference(false); + int attempts = 0; + while (true) { + attempts++; + HttpContext httpContext = new BasicHttpContext(); + final HttpRequestBase request = (url.indexOf("rest") == -1) ? new HttpGet(url) : new HttpPost(url); + + if (task != null) { + // Attempt to abort the HTTP request if the task is cancelled. + task.setOnCancelListener(new BackgroundTask.OnCancelListener() { + @Override + public void onCancel() { + try { + isCancelled.set(true); + if(Thread.currentThread() == Looper.getMainLooper().getThread()) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + request.abort(); + return null; + } + }.execute(); + } else { + request.abort(); + } + } catch(Exception e) { + Log.e(TAG, "Failed to stop http task", e); + } + } + }); + } + + if (parameterNames != null && request instanceof HttpPost) { + List params = new ArrayList(); + for (int i = 0; i < parameterNames.size(); i++) { + params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i)))); + } + ((HttpPost) request).setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8)); + } + + if (requestParams != null) { + request.setParams(requestParams); + } + + if (headers != null) { + for (Header header : headers) { + request.addHeader(header); + } + } + if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1) { + request.addHeader("Accept-Encoding", "gzip"); + } + request.addHeader("User-Agent", Constants.REST_CLIENT_ID); + + // Set credentials to get through apache proxies that require authentication. + int instance = getInstance(context); + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), + new UsernamePasswordCredentials(username, password)); + + try { + HttpResponse response = httpClient.execute(request, httpContext); + detectRedirect(originalUrl, context, httpContext); + return response; + } catch (IOException x) { + request.abort(); + if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || isCancelled.get() || throwErrors) { + throw x; + } + if (progressListener != null) { + String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); + progressListener.updateProgress(msg); + } + Log.w(TAG, "Got IOException " + x + " (" + attempts + "), will retry"); + increaseTimeouts(requestParams); + Thread.sleep(2000L); + } + } + } + + private void increaseTimeouts(HttpParams requestParams) { + if (requestParams != null) { + int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams); + if (connectTimeout != 0) { + HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F)); + } + int readTimeout = HttpConnectionParams.getSoTimeout(requestParams); + if (readTimeout != 0) { + HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F)); + } + } + } + + private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) throws Exception { + HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST); + HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST); + + // Sometimes the request doesn't contain the "http://host" part + String redirectedUrl; + if (request.getURI().getScheme() == null) { + redirectedUrl = host.toURI() + request.getURI(); + } else { + redirectedUrl = request.getURI().toString(); + } + + if(redirectedUrl != null && "http://subsonic.org/pages/".equals(redirectedUrl)) { + throw new Exception("Invalid url, redirects to http://subsonic.org/pages/"); + } + + int fromIndex = originalUrl.indexOf("/rest/"); + int toIndex = redirectedUrl.indexOf("/rest/"); + if(fromIndex != -1 && toIndex != -1 && !Util.equals(originalUrl, redirectedUrl)) { + redirectFrom = originalUrl.substring(0, fromIndex); + redirectTo = redirectedUrl.substring(0, toIndex); + + if (redirectFrom.compareTo(redirectTo) != 0) { + Log.i(TAG, redirectFrom + " redirects to " + redirectTo); + } + redirectionLastChecked = System.currentTimeMillis(); + redirectionNetworkType = getCurrentNetworkType(context); + } + } + + private String rewriteUrlWithRedirect(Context context, String url) { + + // Only cache for a certain time. + if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) { + return url; + } + + // Ignore cache if network type has changed. + if (redirectionNetworkType != getCurrentNetworkType(context)) { + return url; + } + + if (redirectFrom == null || redirectTo == null) { + return url; + } + + return url.replace(redirectFrom, redirectTo); + } + + private String stripUrlInfo(String url) { + return url.substring(0, url.indexOf("?u=") + 1) + url.substring(url.indexOf("&v=") + 1); + } + + private int getCurrentNetworkType(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + return networkInfo == null ? -1 : networkInfo.getType(); + } + + public int getInstance(Context context) { + if(instance == null) { + return Util.getActiveServer(context); + } else { + return instance; + } + } + public String getRestUrl(Context context, String method) { + return getRestUrl(context, method, true); + } + public String getRestUrl(Context context, String method, boolean allowAltAddress) { + if(instance == null) { + return Util.getRestUrl(context, method, allowAltAddress); + } else { + return Util.getRestUrl(context, method, instance, allowAltAddress); + } + } + + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java new file mode 100644 index 0000000..2007d6f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java @@ -0,0 +1,159 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import java.io.IOException; +import java.io.Reader; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.Log; +import android.util.Xml; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Version; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public abstract class AbstractParser { + private static final String TAG = AbstractParser.class.getSimpleName(); + private static final String SUBSONIC_RESPONSE = "subsonic-response"; + private static final String SUBSONIC = "subsonic"; + + protected final Context context; + protected final int instance; + private XmlPullParser parser; + private boolean rootElementFound; + + public AbstractParser(Context context, int instance) { + this.context = context; + this.instance = instance; + } + + protected Context getContext() { + return context; + } + + protected void handleError() throws Exception { + int code = getInteger("code"); + String message; + switch (code) { + case 0: + message = context.getResources().getString(R.string.parser_server_error, get("message")); + break; + case 20: + message = context.getResources().getString(R.string.parser_upgrade_client); + break; + case 30: + message = context.getResources().getString(R.string.parser_upgrade_server); + break; + case 40: + message = context.getResources().getString(R.string.parser_not_authenticated); + break; + case 41: + Util.setBlockTokenUse(context, instance, true); + + // Throw IOException so RESTMusicService knows to retry + throw new IOException(); + case 50: + message = context.getResources().getString(R.string.parser_not_authorized); + break; + default: + message = get("message"); + break; + } + throw new SubsonicRESTException(code, message); + } + + protected void updateProgress(ProgressListener progressListener, int messageId) { + if (progressListener != null) { + progressListener.updateProgress(messageId); + } + } + + protected void updateProgress(ProgressListener progressListener, String message) { + if (progressListener != null) { + progressListener.updateProgress(message); + } + } + + protected String getText() { + return parser.getText(); + } + + protected String get(String name) { + return parser.getAttributeValue(null, name); + } + + protected boolean getBoolean(String name) { + return "true".equals(get(name)); + } + + protected Integer getInteger(String name) { + String s = get(name); + try { + return (s == null || "".equals(s)) ? null : Integer.valueOf(s); + } catch(Exception e) { + Log.w(TAG, "Failed to parse " + s + " into integer"); + return null; + } + } + + protected Long getLong(String name) { + String s = get(name); + return s == null ? null : Long.valueOf(s); + } + + protected Float getFloat(String name) { + String s = get(name); + return s == null ? null : Float.valueOf(s); + } + + protected void init(Reader reader) throws Exception { + parser = Xml.newPullParser(); + parser.setInput(reader); + rootElementFound = false; + } + + protected int nextParseEvent() throws Exception { + try { + return parser.next(); + } catch(Exception e) { + throw e; + } + } + + protected String getElementName() { + String name = parser.getName(); + if (SUBSONIC_RESPONSE.equals(name)) { + rootElementFound = true; + String version = get("version"); + } + return name; + } + + protected void validate() throws Exception { + if (!rootElementFound) { + throw new Exception(context.getResources().getString(R.string.background_task_parse_error)); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.java new file mode 100644 index 0000000..d49240c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class EntryListParser extends MusicDirectoryEntryParser { + + public EntryListParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("album".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + if(get("isDir") == null) { + entry.setDirectory(true); + } + dir.addChild(entry); + } else if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + dir.addChild(entry); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/ErrorParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ErrorParser.java new file mode 100644 index 0000000..0c1ab2c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ErrorParser.java @@ -0,0 +1,49 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class ErrorParser extends AbstractParser { + + public ErrorParser(Context context, int instance) { + super(context, instance); + } + + public void parse(Reader reader) throws Exception { + + init(reader); + + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG && "error".equals(getElementName())) { + handleError(); + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/GenreParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/GenreParser.java new file mode 100644 index 0000000..ee43fa6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/GenreParser.java @@ -0,0 +1,122 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import android.text.Html; +import android.util.Log; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.util.ProgressListener; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Joshua Bahnsen + */ +public class GenreParser extends AbstractParser { + private static final String TAG = GenreParser.class.getSimpleName(); + + public GenreParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + List result = new ArrayList(); + StringReader sr = null; + + try { + BufferedReader br = new BufferedReader(reader); + String xml = null; + String line = null; + + while ((line = br.readLine()) != null) { + if (xml == null) { + xml = line; + } else { + xml += line; + } + } + br.close(); + + // Replace double escaped ampersand (&apos;) + xml = xml.replaceAll("(?:&)(amp;|lt;|gt;|#37;|apos;)", "&$1"); + + // Replace unescaped ampersand + xml = xml.replaceAll("&(?!amp;|lt;|gt;|#37;|apos;)", "&"); + + // Replace unescaped percent symbol + // No replacements for <> at this time + xml = xml.replaceAll("%", "%"); + + xml = xml.replaceAll("'", "'"); + + sr = new StringReader(xml); + } catch (IOException ioe) { + Log.e(TAG, "Error parsing Genre XML", ioe); + } + + if (sr == null) { + Log.w(TAG, "Unable to parse Genre XML, returning empty list"); + return result; + } + + init(sr); + + Genre genre = null; + + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("genre".equals(name)) { + genre = new Genre(); + genre.setSongCount(getInteger("songCount")); + genre.setAlbumCount(getInteger("albumCount")); + } else if ("error".equals(name)) { + handleError(); + } else { + genre = null; + } + } else if (eventType == XmlPullParser.TEXT) { + if (genre != null) { + String value = getText(); + if (genre != null) { + genre.setName(Html.fromHtml(value).toString()); + genre.setIndex(value.substring(0, 1)); + result.add(genre); + genre = null; + } + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return Genre.GenreComparator.sort(result); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java new file mode 100644 index 0000000..72a5a08 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java @@ -0,0 +1,129 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import java.io.Reader; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.content.SharedPreferences; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import android.util.Log; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class IndexesParser extends MusicDirectoryEntryParser { + private static final String TAG = IndexesParser.class.getSimpleName(); + + public IndexesParser(Context context, int instance) { + super(context, instance); + } + + public Indexes parse(Reader reader, ProgressListener progressListener) throws Exception { + long t0 = System.currentTimeMillis(); + init(reader); + + List artists = new ArrayList(); + List shortcuts = new ArrayList(); + List entries = new ArrayList(); + Long lastModified = null; + int eventType; + String index = "#"; + String ignoredArticles = null; + boolean changed = false; + Map artistList = new HashMap(); + + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("indexes".equals(name) || "artists".equals(name)) { + changed = true; + lastModified = getLong("lastModified"); + ignoredArticles = get("ignoredArticles"); + } else if ("index".equals(name)) { + index = get("name"); + + } else if ("artist".equals(name)) { + Artist artist = new Artist(); + artist.setId(get("id")); + artist.setName(get("name")); + artist.setIndex(index); + + // Combine the id's for the two artists + if(artistList.containsKey(artist.getName())) { + Artist originalArtist = artistList.get(artist.getName()); + originalArtist.setId(originalArtist.getId() + ";" + artist.getId()); + } else { + artistList.put(artist.getName(), artist); + artists.add(artist); + } + + if (artists.size() % 10 == 0) { + String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size()); + updateProgress(progressListener, msg); + } + } else if ("shortcut".equals(name)) { + Artist shortcut = new Artist(); + shortcut.setId(get("id")); + shortcut.setName(get("name")); + shortcut.setIndex("*"); + shortcuts.add(shortcut); + } else if("child".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entries.add(entry); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + if(ignoredArticles != null) { + SharedPreferences.Editor prefs = Util.getPreferences(context).edit(); + prefs.putString(Constants.CACHE_KEY_IGNORE, ignoredArticles); + prefs.commit(); + } + + if (!changed) { + return null; + } + + long t1 = System.currentTimeMillis(); + Log.d(TAG, "Got " + artists.size() + " artist(s) in " + (t1 - t0) + "ms."); + + String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size()); + updateProgress(progressListener, msg); + + return new Indexes(lastModified == null ? 0L : lastModified, shortcuts, artists, entries); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java new file mode 100644 index 0000000..796714e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java @@ -0,0 +1,79 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import github.nvllsvm.audinaut.domain.MusicDirectory; + +/** + * @author Sindre Mehus + */ +public class MusicDirectoryEntryParser extends AbstractParser { + public MusicDirectoryEntryParser(Context context, int instance) { + super(context, instance); + } + + protected MusicDirectory.Entry parseEntry(String artist) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + entry.setId(get("id")); + entry.setParent(get("parent")); + entry.setArtistId(get("artistId")); + entry.setTitle(get("title")); + if(entry.getTitle() == null) { + entry.setTitle(get("name")); + } + entry.setDirectory(getBoolean("isDir")); + entry.setCoverArt(get("coverArt")); + entry.setArtist(get("artist")); + entry.setYear(getInteger("year")); + entry.setGenre(get("genre")); + entry.setAlbum(get("album")); + + if (!entry.isDirectory()) { + entry.setAlbumId(get("albumId")); + entry.setTrack(getInteger("track")); + entry.setContentType(get("contentType")); + entry.setSuffix(get("suffix")); + entry.setTranscodedContentType(get("transcodedContentType")); + entry.setTranscodedSuffix(get("transcodedSuffix")); + entry.setSize(getLong("size")); + entry.setDuration(getInteger("duration")); + entry.setBitRate(getInteger("bitRate")); + entry.setPath(get("path")); + entry.setDiscNumber(getInteger("discNumber")); + + String type = get("type"); + } else if(!"".equals(artist)) { + entry.setPath(artist + "/" + entry.getTitle()); + } + return entry; + } + + protected MusicDirectory.Entry parseArtist() { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + + entry.setId(get("id")); + entry.setTitle(get("name")); + entry.setPath(entry.getTitle()); + entry.setDirectory(true); + + return entry; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java new file mode 100644 index 0000000..eb6ca6e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java @@ -0,0 +1,107 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import android.util.Log; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.*; + +/** + * @author Sindre Mehus + */ +public class MusicDirectoryParser extends MusicDirectoryEntryParser { + + private static final String TAG = MusicDirectoryParser.class.getSimpleName(); + + public MusicDirectoryParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(String artist, Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + boolean isArtist = false; + Map titleMap = new HashMap(); + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("child".equals(name) || "song".equals(name)) { + Entry entry = parseEntry(artist); + entry.setGrandParent(dir.getParent()); + + // Only check for songs + if(!entry.isDirectory()) { + // Check if duplicates + String disc = (entry.getDiscNumber() != null) ? Integer.toString(entry.getDiscNumber()) : ""; + String track = (entry.getTrack() != null) ? Integer.toString(entry.getTrack()) : ""; + String duplicateId = disc + "-" + track + "-" + entry.getTitle(); + + Entry duplicate = titleMap.get(duplicateId); + if (duplicate != null) { + // Check if the first already has been rebased or not + if (duplicate.getTitle().equals(entry.getTitle())) { + duplicate.rebaseTitleOffPath(); + } + + // Rebase if this is the second instance of this title found + entry.rebaseTitleOffPath(); + } else { + titleMap.put(duplicateId, entry); + } + } + + dir.addChild(entry); + } else if ("directory".equals(name) || "artist".equals(name) || ("album".equals(name) && !isArtist)) { + dir.setName(get("name")); + dir.setId(get("id")); + if(Util.isTagBrowsing(context, instance)) { + dir.setParent(get("artistId")); + } else { + dir.setParent(get("parent")); + } + isArtist = true; + } else if("album".equals(name)) { + Entry entry = parseEntry(artist); + entry.setDirectory(true); + dir.addChild(entry); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java new file mode 100644 index 0000000..dc1898b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java @@ -0,0 +1,65 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public class MusicFoldersParser extends AbstractParser { + + public MusicFoldersParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List result = new ArrayList(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String tag = getElementName(); + if ("musicFolder".equals(tag)) { + String id = get("id"); + String name = get("name"); + result.add(new MusicFolder(id, name)); + } else if ("error".equals(tag)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return result; + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java new file mode 100644 index 0000000..a3dac5c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class PlayQueueParser extends MusicDirectoryEntryParser { + private static final String TAG = PlayQueueParser.class.getSimpleName(); + + public PlayQueueParser(Context context, int instance) { + super(context, instance); + } + + public PlayerQueue parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + PlayerQueue state = new PlayerQueue(); + String currentId = null; + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if("playQueue".equals(name)) { + currentId = get("current"); + state.currentPlayingPosition = getInteger("position"); + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + state.changed = dateFormat.parse(get("changed")); + } catch (ParseException e) { + state.changed = null; + } + } else if ("entry".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + // Only add songs + state.songs.add(entry); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + if(currentId != null) { + for (MusicDirectory.Entry entry : state.songs) { + if (entry.getId().equals(currentId)) { + state.currentPlayingIndex = state.songs.indexOf(entry); + } + } + } else { + state.currentPlayingIndex = 0; + state.currentPlayingPosition = 0; + } + + validate(); + return state; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistParser.java new file mode 100644 index 0000000..62da232 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistParser.java @@ -0,0 +1,63 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class PlaylistParser extends MusicDirectoryEntryParser { + + public PlaylistParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("entry".equals(name)) { + dir.addChild(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } else if ("playlist".equals(name)) { + dir.setName(get("name")); + dir.setId(get("id")); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.java new file mode 100644 index 0000000..36fb942 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.java @@ -0,0 +1,71 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Sindre Mehus + */ +public class PlaylistsParser extends AbstractParser { + + public PlaylistsParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List result = new ArrayList(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String tag = getElementName(); + if ("playlist".equals(tag)) { + String id = get("id"); + String name = get("name"); + String owner = get("owner"); + String comment = get("comment"); + String songCount = get("songCount"); + String pub = get("public"); + String created = get("created"); + String changed = get("changed"); + Integer duration = getInteger("duration"); + result.add(new Playlist(id, name, owner, comment, songCount, pub, created, changed, duration)); + } else if ("error".equals(tag)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return Playlist.PlaylistComparator.sort(result); + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java new file mode 100644 index 0000000..2e3453c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java @@ -0,0 +1,60 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class RandomSongsParser extends MusicDirectoryEntryParser { + + public RandomSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + dir.addChild(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/ScanStatusParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ScanStatusParser.java new file mode 100644 index 0000000..e16db38 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ScanStatusParser.java @@ -0,0 +1,56 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class ScanStatusParser extends AbstractParser { + + public ScanStatusParser(Context context, int instance) { + super(context, instance); + } + + public boolean parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + Boolean started = null; + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if("status".equals(name)) { + started = getBoolean("started"); + + String msg = context.getResources().getString(R.string.parser_scan_count, getInteger("count")); + progressListener.updateProgress(msg); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return started != null && started; + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResult2Parser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResult2Parser.java new file mode 100644 index 0000000..24d29d7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResult2Parser.java @@ -0,0 +1,75 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.List; +import java.util.ArrayList; + +/** + * @author Sindre Mehus + */ +public class SearchResult2Parser extends MusicDirectoryEntryParser { + + public SearchResult2Parser(Context context, int instance) { + super(context, instance); + } + + public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List artists = new ArrayList(); + List albums = new ArrayList(); + List songs = new ArrayList(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("artist".equals(name)) { + Artist artist = new Artist(); + artist.setId(get("id")); + artist.setName(get("name")); + artists.add(artist); + } else if ("album".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setDirectory(true); + albums.add(entry); + } else if ("song".equals(name)) { + songs.add(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return new SearchResult(artists, albums, songs); + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResultParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResultParser.java new file mode 100644 index 0000000..41ee05d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResultParser.java @@ -0,0 +1,65 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +/** + * @author Sindre Mehus + */ +public class SearchResultParser extends MusicDirectoryEntryParser { + + public SearchResultParser(Context context, int instance) { + super(context, instance); + } + + public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List songs = new ArrayList(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("match".equals(name)) { + songs.add(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return new SearchResult(Collections.emptyList(), Collections.emptyList(), songs); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java new file mode 100644 index 0000000..561e8eb --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java @@ -0,0 +1,19 @@ +package github.nvllsvm.audinaut.service.parser; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SubsonicRESTException extends Exception { + + private final int code; + + public SubsonicRESTException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.java new file mode 100644 index 0000000..48650c7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.java @@ -0,0 +1,58 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2016 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class TopSongsParser extends MusicDirectoryEntryParser { + + public TopSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + int trackNumber = 1; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setTrack(trackNumber); + dir.addChild(entry); + + trackNumber++; + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.java new file mode 100644 index 0000000..dbcdaed --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.java @@ -0,0 +1,108 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import android.util.Log; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.User.MusicFolderSetting; +import github.nvllsvm.audinaut.domain.User.Setting; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class UserParser extends AbstractParser { + private static final String TAG = UserParser.class.getSimpleName(); + + public UserParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + List result = new ArrayList(); + List musicFolders = null; + User user = null; + int eventType; + + String tagName = null; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + tagName = getElementName(); + if ("user".equals(tagName)) { + user = new User(); + + user.setUsername(get("username")); + user.setEmail(get("email")); + for(String role: User.ROLES) { + parseSetting(user, role); + } + + result.add(user); + } else if ("error".equals(tagName)) { + handleError(); + } + } else if(eventType == XmlPullParser.TEXT) { + if("folder".equals(tagName)) { + String id = getText(); + if(musicFolders == null) { + musicFolders = getMusicFolders(); + } + + if(user != null) { + if(user.getMusicFolderSettings() == null) { + for (MusicFolder musicFolder : musicFolders) { + user.addMusicFolder(musicFolder); + } + } + + for(Setting musicFolder: user.getMusicFolderSettings()) { + if(musicFolder.getName().equals(id)) { + musicFolder.setValue(true); + break; + } + } + } + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return result; + } + + private List getMusicFolders() throws Exception{ + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getMusicFolders(false, context, null); + } + + private void parseSetting(User user, String name) { + String value = get(name); + if(value != null) { + user.addSetting(name, "true".equals(value)); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java new file mode 100644 index 0000000..b685ed7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java @@ -0,0 +1,553 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package github.nvllsvm.audinaut.service.ssl; + +import android.os.Build; +import android.util.Log; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.scheme.HostNameResolver; +import org.apache.http.conn.scheme.LayeredSocketFactory; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Layered socket factory for TLS/SSL connections. + *

+ * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of + * trusted certificates and to authenticate to the HTTPS server using a private key. + *

+ * SSLSocketFactory will enable server authentication when supplied with + * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client + * secure socket will reject the connection during the SSL session handshake if the target HTTPS + * server attempts to authenticate itself with a non-trusted certificate. + *

+ * Use JDK keytool utility to import a trusted certificate and generate a trust-store file: + *

+ *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ *    
+ *

+ * In special cases the standard trust verification process can be bypassed by using a custom + * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed + * certificates to be accepted as trusted without having to add them to the trust-store file. + *

+ * The following parameters can be used to customize the behavior of this + * class: + *

    + *
  • {@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}
  • + *
  • {@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}
  • + *
+ *

+ * SSLSocketFactory will enable client authentication when supplied with + * a {@link KeyStore key-store} file containing a private key/public certificate + * pair. The client secure socket will use the private key to authenticate + * itself to the target HTTPS server during the SSL session handshake if + * requested to do so by the server. + * The target HTTPS server will in its turn verify the certificate presented + * by the client in order to establish client's authenticity + *

+ * Use the following sequence of actions to generate a key-store file + *

+ *
    + *
  • + *

    + * Use JDK keytool utility to generate a new key + *

    keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore
    + * For simplicity use the same password for the key as that of the key-store + *

    + *
  • + *
  • + *

    + * Issue a certificate signing request (CSR) + *

    keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore
    + *

    + *
  • + *
  • + *

    + * Send the certificate request to the trusted Certificate Authority for signature. + * One may choose to act as her own CA and sign the certificate request using a PKI + * tool, such as OpenSSL. + *

    + *
  • + *
  • + *

    + * Import the trusted CA root certificate + *

    keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore
    + *

    + *
  • + *
  • + *

    + * Import the PKCS#7 file containg the complete certificate chain + *

    keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore
    + *

    + *
  • + *
  • + *

    + * Verify the content the resultant keystore file + *

    keytool -list -v -keystore my.keystore
    + *

    + *
  • + *
+ * + * @since 4.0 + */ +public class SSLSocketFactory implements LayeredSocketFactory { + private static final String TAG = SSLSocketFactory.class.getSimpleName(); + public static final String TLS = "TLS"; + + public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER + = new AllowAllHostnameVerifier(); + + public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER + = new BrowserCompatHostnameVerifier(); + + public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER + = new StrictHostnameVerifier(); + + /** + * The default factory using the default JVM settings for secure connections. + */ + private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory(); + + /** + * Gets the default factory, which uses the default JVM settings for secure + * connections. + * + * @return the default factory + */ + public static SSLSocketFactory getSocketFactory() { + return DEFAULT_FACTORY; + } + + private final javax.net.ssl.SSLSocketFactory socketfactory; + private final HostNameResolver nameResolver; + // TODO: make final + private volatile X509HostnameVerifier hostnameVerifier; + + private static SSLContext createSSLContext( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final TrustStrategy trustStrategy) + throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { + if (algorithm == null) { + algorithm = TLS; + } + KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null); + KeyManager[] keymanagers = kmfactory.getKeyManagers(); + TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmfactory.init(keystore); + TrustManager[] trustmanagers = tmfactory.getTrustManagers(); + if (trustmanagers != null && trustStrategy != null) { + for (int i = 0; i < trustmanagers.length; i++) { + TrustManager tm = trustmanagers[i]; + if (tm instanceof X509TrustManager) { + trustmanagers[i] = new TrustManagerDecorator( + (X509TrustManager) tm, trustStrategy); + } + } + } + + SSLContext sslcontext = SSLContext.getInstance(algorithm); + sslcontext.init(keymanagers, trustmanagers, random); + return sslcontext; + } + + /** + * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)} + */ + @Deprecated + public SSLSocketFactory( + final String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final HostNameResolver nameResolver) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, null), + nameResolver); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, null), + hostnameVerifier); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final TrustStrategy trustStrategy, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, trustStrategy), + hostnameVerifier); + } + + public SSLSocketFactory( + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory( + final KeyStore keystore, + final String keystorePassword) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{ + this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory( + final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final TrustStrategy trustStrategy, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, null, null, trustStrategy, hostnameVerifier); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final TrustStrategy trustStrategy) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory(final SSLContext sslContext) { + this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + /** + * @deprecated Use {@link #SSLSocketFactory(SSLContext)} + */ + @Deprecated + public SSLSocketFactory( + final SSLContext sslContext, final HostNameResolver nameResolver) { + super(); + this.socketfactory = sslContext.getSocketFactory(); + this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; + this.nameResolver = nameResolver; + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) { + super(); + this.socketfactory = sslContext.getSocketFactory(); + this.hostnameVerifier = hostnameVerifier; + this.nameResolver = null; + } + + private SSLSocketFactory() { + super(); + this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + this.hostnameVerifier = null; + this.nameResolver = null; + } + + /** + * @param params Optional parameters. Parameters passed to this method will have no effect. + * This method will create a unconnected instance of {@link Socket} class + * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method. + * @since 4.1 + */ + @SuppressWarnings("cast") + public Socket createSocket(final HttpParams params) throws IOException { + // the cast makes sure that the factory is working as expected + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); + return sslSocket; + } + + @SuppressWarnings("cast") + public Socket createSocket() throws IOException { + // the cast makes sure that the factory is working as expected + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); + return sslSocket; + } + + /** + * @since 4.1 + */ + public Socket connectSocket( + final Socket sock, + final InetSocketAddress remoteAddress, + final InetSocketAddress localAddress, + final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { + if (remoteAddress == null) { + throw new IllegalArgumentException("Remote address may not be null"); + } + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket()); + if (localAddress != null) { +// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params)); + sslsock.bind(localAddress); + } + + setHostName(sslsock, remoteAddress.getHostName()); + int connTimeout = HttpConnectionParams.getConnectionTimeout(params); + int soTimeout = HttpConnectionParams.getSoTimeout(params); + + try { + sslsock.connect(remoteAddress, connTimeout); + } catch (SocketTimeoutException ex) { + throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/" + + remoteAddress.getAddress() + " timed out"); + } + sslsock.setSoTimeout(soTimeout); + if (this.hostnameVerifier != null) { + try { + this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock); + // verifyHostName() didn't blowup - good! + } catch (IOException iox) { + // close the socket before re-throwing the exception + try { sslsock.close(); } catch (Exception x) { /*ignore*/ } + throw iox; + } + } + return sslsock; + } + + + /** + * Checks whether a socket connection is secure. + * This factory creates TLS/SSL socket connections + * which, by default, are considered secure. + *
+ * Derived classes may override this method to perform + * runtime checks, for example based on the cypher suite. + * + * @param sock the connected socket + * + * @return true + * + * @throws IllegalArgumentException if the argument is invalid + */ + public boolean isSecure(final Socket sock) throws IllegalArgumentException { + if (sock == null) { + throw new IllegalArgumentException("Socket may not be null"); + } + // This instanceof check is in line with createSocket() above. + if (!(sock instanceof SSLSocket)) { + throw new IllegalArgumentException("Socket not created by this factory"); + } + // This check is performed last since it calls the argument object. + if (sock.isClosed()) { + throw new IllegalArgumentException("Socket is closed"); + } + return true; + } + + /** + * @since 4.1 + */ + public Socket createLayeredSocket( + final Socket socket, + final String host, + final int port, + final boolean autoClose) throws IOException, UnknownHostException { + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket( + socket, + host, + port, + autoClose + ); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); + if (this.hostnameVerifier != null) { + this.hostnameVerifier.verify(host, sslSocket); + } + // verifyHostName() didn't blowup - good! + return sslSocket; + } + + @Deprecated + public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) { + if ( hostnameVerifier == null ) { + throw new IllegalArgumentException("Hostname verifier may not be null"); + } + this.hostnameVerifier = hostnameVerifier; + } + + public X509HostnameVerifier getHostnameVerifier() { + return this.hostnameVerifier; + } + + /** + * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)} + */ + @Deprecated + public Socket connectSocket( + final Socket socket, + final String host, int port, + final InetAddress localAddress, int localPort, + final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { + InetSocketAddress local = null; + if (localAddress != null || localPort > 0) { + // we need to bind explicitly + if (localPort < 0) { + localPort = 0; // indicates "any" + } + local = new InetSocketAddress(localAddress, localPort); + } + InetAddress remoteAddress; + if (this.nameResolver != null) { + remoteAddress = this.nameResolver.resolve(host); + } else { + remoteAddress = InetAddress.getByName(host); + } + InetSocketAddress remote = new InetSocketAddress(remoteAddress, port); + return connectSocket(socket, remote, local, params); + } + + /** + * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)} + */ + @Deprecated + public Socket createSocket( + final Socket socket, + final String host, int port, + boolean autoClose) throws IOException, UnknownHostException { + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(socket, host, port, autoClose); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); + setHostName(sslSocket, host); + return sslSocket; + } + + private void setHostName(SSLSocket sslsock, String hostname){ + try { + java.lang.reflect.Method setHostnameMethod = sslsock.getClass().getMethod("setHostname", String.class); + setHostnameMethod.invoke(sslsock, hostname); + } catch (Exception e) { + Log.w(TAG, "SNI not useable", e); + } + } + + private String[] getProtocols(SSLSocket sslSocket) { + String[] protocols = sslSocket.getEnabledProtocols(); + + // Remove SSLv3 if it is not the only option + if(protocols.length > 1) { + List protocolList = new ArrayList(Arrays.asList(protocols)); + protocolList.remove("SSLv3"); + protocols = protocolList.toArray(new String[protocolList.size()]); + } + + return protocols; + } + + private String[] getCiphers(SSLSocket sslSocket) { + String[] ciphers = sslSocket.getEnabledCipherSuites(); + + List enabledCiphers = new ArrayList(Arrays.asList(ciphers)); + // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers + // Issue seems to have been fixed in M, and now won't work without them. Because Google + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); + enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"); + } + + ciphers = enabledCiphers.toArray(new String[enabledCiphers.size()]); + return ciphers; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java new file mode 100644 index 0000000..f56955c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java @@ -0,0 +1,65 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package github.nvllsvm.audinaut.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + + +/** + * @since 4.1 + */ +class TrustManagerDecorator implements X509TrustManager { + + private final X509TrustManager trustManager; + private final TrustStrategy trustStrategy; + + TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) { + super(); + this.trustManager = trustManager; + this.trustStrategy = trustStrategy; + } + + public void checkClientTrusted( + final X509Certificate[] chain, final String authType) throws CertificateException { + this.trustManager.checkClientTrusted(chain, authType); + } + + public void checkServerTrusted( + final X509Certificate[] chain, final String authType) throws CertificateException { + if (!this.trustStrategy.isTrusted(chain, authType)) { + this.trustManager.checkServerTrusted(chain, authType); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return this.trustManager.getAcceptedIssuers(); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java new file mode 100644 index 0000000..e9c2f08 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package github.nvllsvm.audinaut.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * A trust strategy that accepts self-signed certificates as trusted. Verification of all other + * certificates is done by the trust manager configured in the SSL context. + * + * @since 4.1 + */ +public class TrustSelfSignedStrategy implements TrustStrategy { + + public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + return true; + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustStrategy.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustStrategy.java new file mode 100644 index 0000000..504407b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustStrategy.java @@ -0,0 +1,57 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package github.nvllsvm.audinaut.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * A strategy to establish trustworthiness of certificates without consulting the trust manager + * configured in the actual SSL context. This interface can be used to override the standard + * JSSE certificate verification process. + * + * @since 4.1 + */ +public interface TrustStrategy { + + /** + * Determines whether the certificate chain can be trusted without consulting the trust manager + * configured in the actual SSL context. This method can be used to override the standard JSSE + * certificate verification process. + *

+ * Please note that, if this method returns false, the trust manager configured + * in the actual SSL context can still clear the certificate as trusted. + * + * @param chain the peer certificate chain + * @param authType the authentication type based on the client certificate + * @return true if the certificate can be trusted without verification by + * the trust manager, false otherwise. + * @throws CertificateException thrown if the certificate is not trusted or invalid. + */ + boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException; + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/sync/AuthenticatorService.java b/app/src/main/java/github/nvllsvm/audinaut/service/sync/AuthenticatorService.java new file mode 100644 index 0000000..956e5b1 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/sync/AuthenticatorService.java @@ -0,0 +1,90 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.service.sync; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +/** + * Created by Scott on 8/28/13. + */ + +public class AuthenticatorService extends Service { + private SubsonicAuthenticator authenticator; + + @Override + public void onCreate() { + authenticator = new SubsonicAuthenticator(this); + } + + @Override + public IBinder onBind(Intent intent) { + return authenticator.getIBinder(); + + } + + private class SubsonicAuthenticator extends AbstractAccountAuthenticator { + public SubsonicAuthenticator(Context context) { + super(context); + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { + return null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java new file mode 100644 index 0000000..fb19e6f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java @@ -0,0 +1,200 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.service.sync; + +import android.accounts.Account; +import android.annotation.TargetApi; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.BatteryManager; +import android.os.Bundle; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.CachedMusicService; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.RESTMusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +/** + * Created by Scott on 9/6/13. + */ + +public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { + private static final String TAG = SubsonicSyncAdapter.class.getSimpleName(); + protected CachedMusicService musicService = new CachedMusicService(new RESTMusicService()); + protected boolean tagBrowsing; + private Context context; + + public SubsonicSyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + this.context = context; + } + @TargetApi(14) + public SubsonicSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { + super(context, autoInitialize, allowParallelSyncs); + this.context = context; + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + Log.w(TAG, "Not running sync: " + invalidMessage); + return; + } + + // Make sure battery > x% or is charging + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = context.registerReceiver(null, intentFilter); + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + if (status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { + int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + + if ((level / (float) scale) < 0.15) { + Log.w(TAG, "Not running sync, battery too low"); + return; + } + } + + executeSync(context); + } + + private String isNetworkValid() { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + + // Don't try to sync if no network! + if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { + return "Not connected to any network"; + } + + // Check if user wants to only sync on wifi + SharedPreferences prefs = Util.getPreferences(context); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) { + if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return null; + } else { + return "Not connected to WIFI"; + } + } else { + return null; + } + } + protected void throwIfNetworkInvalid() throws NetworkNotValidException { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + throw new NetworkNotValidException(invalidMessage); + } + } + + private void executeSync(Context context) { + String className = this.getClass().getSimpleName(); + Log.i(TAG, "Running sync for " + className); + long start = System.currentTimeMillis(); + int servers = Util.getServerCount(context); + try { + for (int i = 1; i <= servers; i++) { + try { + throwIfNetworkInvalid(); + + if (isValidServer(context, i) && Util.isSyncEnabled(context, i)) { + tagBrowsing = Util.isTagBrowsing(context, i); + musicService.setInstance(i); + onExecuteSync(context, i); + } else { + Log.i(TAG, "Skipped sync for " + i); + } + } catch (Exception e) { + Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); + } + } + } catch (NetworkNotValidException e) { + Log.e(TAG, "Stopped sync due to network loss", e); + } + + Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms"); + } + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { + + } + + protected boolean downloadRecursively(List paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException { + boolean downloaded = false; + for (MusicDirectory.Entry song: parent.getChildren(false, true)) { + DownloadFile file = new DownloadFile(context, song, save); + while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) { + throwIfNetworkInvalid(); + file.downloadNow(musicService); + if(!file.isFailed()) { + downloaded = true; + } + } + + if(paths != null && file.isCompleteFileAvailable()) { + paths.add(file.getCompleteFile().getPath()); + } + } + + for (MusicDirectory.Entry dir: parent.getChildren(true, false)) { + if(downloadRecursively(paths, getMusicDirectory(dir), context, save)) { + downloaded = true; + } + } + + return downloaded; + } + protected MusicDirectory getMusicDirectory(MusicDirectory.Entry dir) throws Exception{ + String id = dir.getId(); + String name = dir.getTitle(); + + if(tagBrowsing) { + if(dir.getArtist() == null) { + return musicService.getArtist(id, name, true, context, null); + } else { + return musicService.getAlbum(id, name, true, context, null); + } + } else { + return musicService.getMusicDirectory(id, name, true, context, null); + } + } + + private boolean isValidServer(Context context, int instance) { + String url = Util.getRestUrl(context, "null", instance, false); + return !(url.contains("demo.subsonic.org") || url.contains("yourhost")); + } + + public class NetworkNotValidException extends Throwable { + public NetworkNotValidException(String reason) { + super("Not running sync: " + reason); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java b/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java new file mode 100644 index 0000000..6d48f3d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java @@ -0,0 +1,102 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.updates; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Util; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Scott + */ +public class Updater { + protected String TAG = Updater.class.getSimpleName(); + protected int version; + protected Context context; + + public Updater(int version) { + // 5.2 should show as 520 instead of 52 + if(version < 100) { + version *= 10; + } + this.version = version; + } + + public void checkUpdates(Context context) { + this.context = context; + List updaters = new ArrayList(); + updaters.add(new UpdaterSongPress()); + + SharedPreferences prefs = Util.getPreferences(context); + int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0); + if(lastVersion == 0) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.LAST_VERSION, version); + editor.commit(); + } + else if(version > lastVersion) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.LAST_VERSION, version); + editor.commit(); + + Log.i(TAG, "Updating from version " + lastVersion + " to " + version); + for(Updater updater: updaters) { + if(updater.shouldUpdate(lastVersion)) { + new BackgroundUpdate(context, updater).execute(); + } + } + } + } + + public String getName() { + return this.TAG; + } + + private class BackgroundUpdate extends SilentBackgroundTask { + private final Updater updater; + + public BackgroundUpdate(Context context, Updater updater) { + super(context); + this.updater = updater; + } + + @Override + protected Void doInBackground() { + try { + updater.update(context); + } catch(Exception e) { + Log.w(TAG, "Failed to run update for " + updater.getName()); + } + return null; + } + } + + public boolean shouldUpdate(int version) { + return this.version > version; + } + public void update(Context context) { + + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.java b/app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.java new file mode 100644 index 0000000..76d3107 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.java @@ -0,0 +1,42 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.updates; + +import android.content.Context; +import android.content.SharedPreferences; + +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +public class UpdaterSongPress extends Updater { + public UpdaterSongPress() { + super(521); + TAG = this.getClass().getSimpleName(); + } + + @Override + public void update(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + boolean playNowAfter = prefs.getBoolean("playNowAfter", true); + + // Migrate the old preference so behavior stays the same + if(playNowAfter == false) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "single"); + editor.commit(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java new file mode 100644 index 0000000..092a087 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java @@ -0,0 +1,325 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.view.ErrorDialog; + +/** + * @author Sindre Mehus + */ +public abstract class BackgroundTask implements ProgressListener { + private static final String TAG = BackgroundTask.class.getSimpleName(); + + private final Context context; + protected AtomicBoolean cancelled = new AtomicBoolean(false); + protected OnCancelListener cancelListener; + protected Runnable onCompletionListener = null; + protected Task task; + + private static final int DEFAULT_CONCURRENCY = 8; + private static final Collection threads = Collections.synchronizedCollection(new ArrayList()); + protected static final BlockingQueue queue = new LinkedBlockingQueue(10); + private static Handler handler = null; + static { + try { + handler = new Handler(Looper.getMainLooper()); + } catch(Exception e) { + // Not called from main thread + } + } + + public BackgroundTask(Context context) { + this.context = context; + + if(threads.size() < DEFAULT_CONCURRENCY) { + for(int i = threads.size(); i < DEFAULT_CONCURRENCY; i++) { + Thread thread = new Thread(new TaskRunnable(), String.format("BackgroundTask_%d", i)); + threads.add(thread); + thread.start(); + } + } + if(handler == null) { + try { + handler = new Handler(Looper.getMainLooper()); + } catch(Exception e) { + // Not called from main thread + } + } + } + + public static void stopThreads() { + for(Thread thread: threads) { + thread.interrupt(); + } + threads.clear(); + queue.clear(); + } + + protected Activity getActivity() { + return (context instanceof Activity) ? ((Activity) context) : null; + } + + protected Context getContext() { + return context; + } + protected Handler getHandler() { + return handler; + } + + public abstract void execute(); + + protected abstract T doInBackground() throws Throwable; + + protected abstract void done(T result); + + protected void error(Throwable error) { + Log.w(TAG, "Got exception: " + error, error); + Activity activity = getActivity(); + if(activity != null) { + new ErrorDialog(activity, getErrorMessage(error), true); + } + } + + protected String getErrorMessage(Throwable error) { + + if (error instanceof IOException && !Util.isNetworkConnected(context)) { + return context.getResources().getString(R.string.background_task_no_network); + } + + if (error instanceof FileNotFoundException) { + return context.getResources().getString(R.string.background_task_not_found); + } + + if (error instanceof IOException) { + return context.getResources().getString(R.string.background_task_network_error); + } + + if (error instanceof XmlPullParserException) { + return context.getResources().getString(R.string.background_task_parse_error); + } + + String message = error.getMessage(); + if (message != null) { + return message; + } + return error.getClass().getSimpleName(); + } + + public void cancel() { + if(cancelled.compareAndSet(false, true)) { + if(isRunning()) { + if(cancelListener != null) { + cancelListener.onCancel(); + } else { + task.cancel(); + } + } + + task = null; + } + } + public boolean isCancelled() { + return cancelled.get(); + } + public void setOnCancelListener(OnCancelListener listener) { + cancelListener = listener; + } + + public boolean isRunning() { + if(task == null) { + return false; + } else { + return task.isRunning(); + } + } + + @Override + public abstract void updateProgress(final String message); + + @Override + public void updateProgress(int messageId) { + updateProgress(context.getResources().getString(messageId)); + } + + @Override + public void updateCache(int changeCode) { + + } + + public void setOnCompletionListener(Runnable onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + protected class Task { + private Thread thread; + private AtomicBoolean taskStart = new AtomicBoolean(false); + + private void execute() throws Exception { + // Don't run if cancelled already + if(isCancelled()) { + return; + } + + try { + thread = Thread.currentThread(); + taskStart.set(true); + + final T result = doInBackground(); + if(isCancelled()) { + taskStart.set(false); + return; + } + + if(handler != null) { + handler.post(new Runnable() { + @Override + public void run() { + if (!isCancelled()) { + try { + onDone(result); + } catch (Throwable t) { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + } + } + + taskStart.set(false); + } + }); + } else { + taskStart.set(false); + } + } catch(InterruptedException interrupt) { + if(taskStart.get()) { + // Don't exit root thread if task cancelled + throw interrupt; + } + } catch(final Throwable t) { + if(isCancelled()) { + taskStart.set(false); + return; + } + + if(handler != null) { + handler.post(new Runnable() { + @Override + public void run() { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + + taskStart.set(false); + } + }); + } else { + taskStart.set(false); + } + } finally { + thread = null; + } + } + + public void cancel() { + if(taskStart.compareAndSet(true, false)) { + if (thread != null) { + thread.interrupt(); + } + } + } + public boolean isCancelled() { + if(Thread.interrupted()) { + return true; + } else if(BackgroundTask.this.isCancelled()) { + return true; + } else { + return false; + } + } + public void onDone(T result) { + done(result); + + if(onCompletionListener != null) { + onCompletionListener.run(); + } + } + public void onError(Throwable t) { + error(t); + } + + public boolean isRunning() { + return taskStart.get(); + } + } + + private class TaskRunnable implements Runnable { + private boolean running = true; + + public TaskRunnable() { + + } + + @Override + public void run() { + Looper.prepare(); + while(running) { + try { + Task task = queue.take(); + task.execute(); + } catch(InterruptedException stop) { + Log.e(TAG, "Thread died"); + running = false; + threads.remove(Thread.currentThread()); + } catch(Throwable t) { + Log.e(TAG, "Unexpected crash in BackgroundTask thread", t); + } + } + } + } + + public static interface OnCancelListener { + void onCancel(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java b/app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java new file mode 100644 index 0000000..ccd2150 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java @@ -0,0 +1,292 @@ +package github.nvllsvm.audinaut.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; +import android.util.Log; +import android.os.StatFs; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MediaStoreService; + +import java.util.*; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class CacheCleaner { + + private static final String TAG = CacheCleaner.class.getSimpleName(); + private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; + private static final long MAX_COVER_ART_SPACE = 100 * 1024L * 1024L; + + private final Context context; + private final DownloadService downloadService; + private final MediaStoreService mediaStore; + + public CacheCleaner(Context context, DownloadService downloadService) { + this.context = context; + this.downloadService = downloadService; + this.mediaStore = new MediaStoreService(context); + } + + public void clean() { + new BackgroundCleanup(context).execute(); + } + public void cleanSpace() { + new BackgroundSpaceCleanup(context).execute(); + } + public void cleanPlaylists(List playlists) { + new BackgroundPlaylistsCleanup(context, playlists).execute(); + } + + private void deleteEmptyDirs(List dirs, Set undeletable) { + for (File dir : dirs) { + if (undeletable.contains(dir)) { + continue; + } + + FileUtil.deleteEmptyDir(dir); + } + } + + private long getMinimumDelete(List files, List pinned) { + if(files.size() == 0) { + return 0L; + } + + long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L; + + long bytesUsedBySubsonic = 0L; + for (File file : files) { + bytesUsedBySubsonic += file.length(); + } + for (File file : pinned) { + bytesUsedBySubsonic += file.length(); + } + + // Ensure that file system is not more than 95% full. + StatFs stat = new StatFs(files.get(0).getPath()); + long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + long bytesUsedFs = bytesTotalFs - bytesAvailableFs; + long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE; + + long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L); + long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L); + long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit); + + Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available"); + Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes)); + Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic)); + Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete)); + + return bytesToDelete; + } + + private void deleteFiles(List files, Set undeletable, long bytesToDelete, boolean deletePartials) { + if (files.isEmpty()) { + return; + } + + long bytesDeleted = 0L; + for (File file : files) { + if(!deletePartials && bytesDeleted > bytesToDelete) break; + + if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) { + if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) { + long size = file.length(); + if (Util.delete(file)) { + bytesDeleted += size; + mediaStore.deleteFromMediaStore(file); + } + } + } + } + + Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted)); + } + + private void findCandidatesForDeletion(File file, List files, List pinned, List dirs) { + if (file.isFile()) { + String name = file.getName(); + boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete."); + if (isCacheFile) { + files.add(file); + } else { + pinned.add(file); + } + } else { + // Depth-first + for (File child : FileUtil.listFiles(file)) { + findCandidatesForDeletion(child, files, pinned, dirs); + } + dirs.add(file); + } + } + + private void sortByAscendingModificationTime(List files) { + Collections.sort(files, new Comparator() { + @Override + public int compare(File a, File b) { + if (a.lastModified() < b.lastModified()) { + return -1; + } + if (a.lastModified() > b.lastModified()) { + return 1; + } + return 0; + } + }); + } + + private Set findUndeletableFiles() { + Set undeletable = new HashSet(5); + + for (DownloadFile downloadFile : downloadService.getDownloads()) { + undeletable.add(downloadFile.getPartialFile()); + undeletable.add(downloadFile.getCompleteFile()); + } + + undeletable.add(FileUtil.getMusicDirectory(context)); + return undeletable; + } + + private void cleanupCoverArt(Context context) { + File dir = FileUtil.getAlbumArtDirectory(context); + + List files = new ArrayList(); + long bytesUsed = 0L; + for(File file: dir.listFiles()) { + if(file.isFile()) { + files.add(file); + bytesUsed += file.length(); + } + } + + // Don't waste time sorting if under limit already + if(bytesUsed < MAX_COVER_ART_SPACE) { + return; + } + + sortByAscendingModificationTime(files); + long bytesDeleted = 0L; + for(File file: files) { + // End as soon as the space used is below the threshold + if(bytesUsed < MAX_COVER_ART_SPACE) { + break; + } + + long bytes = file.length(); + if(file.delete()) { + bytesUsed -= bytes; + bytesDeleted += bytes; + } + } + + Log.i(TAG, "Deleted " + Util.formatBytes(bytesDeleted) + " worth of cover art"); + } + + private class BackgroundCleanup extends SilentBackgroundTask { + public BackgroundCleanup(Context context) { + super(context); + } + + @Override + protected Void doInBackground() { + if (downloadService == null) { + Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); + return null; + } + + try { + List files = new ArrayList(); + List pinned = new ArrayList(); + List dirs = new ArrayList(); + + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs); + sortByAscendingModificationTime(files); + + Set undeletable = findUndeletableFiles(); + + deleteFiles(files, undeletable, getMinimumDelete(files, pinned), true); + deleteEmptyDirs(dirs, undeletable); + + // Make sure cover art directory does not grow too large + cleanupCoverArt(context); + } catch (RuntimeException x) { + Log.e(TAG, "Error in cache cleaning.", x); + } + + return null; + } + } + + private class BackgroundSpaceCleanup extends SilentBackgroundTask { + public BackgroundSpaceCleanup(Context context) { + super(context); + } + + @Override + protected Void doInBackground() { + if (downloadService == null) { + Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); + return null; + } + + try { + List files = new ArrayList(); + List pinned = new ArrayList(); + List dirs = new ArrayList(); + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs); + + long bytesToDelete = getMinimumDelete(files, pinned); + if(bytesToDelete > 0L) { + sortByAscendingModificationTime(files); + Set undeletable = findUndeletableFiles(); + deleteFiles(files, undeletable, bytesToDelete, false); + } + } catch (RuntimeException x) { + Log.e(TAG, "Error in cache cleaning.", x); + } + + return null; + } + } + + private class BackgroundPlaylistsCleanup extends SilentBackgroundTask { + private final List playlists; + + public BackgroundPlaylistsCleanup(Context context, List playlists) { + super(context); + this.playlists = playlists; + } + + @Override + protected Void doInBackground() { + try { + String server = Util.getServerName(context); + SortedSet playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server)); + for (Playlist playlist : playlists) { + playlistFiles.remove(FileUtil.getPlaylistFile(context, server, playlist.getName())); + } + + for(File playlist : playlistFiles) { + playlist.delete(); + } + } catch (RuntimeException x) { + Log.e(TAG, "Error in playlist cache cleaning.", x); + } + + return null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Constants.java b/app/src/main/java/github/nvllsvm/audinaut/util/Constants.java new file mode 100644 index 0000000..c446755 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Constants.java @@ -0,0 +1,180 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public final class Constants { + + // Character encoding used throughout. + public static final String UTF_8 = "UTF-8"; + + // REST protocol version and client ID. + // Note: Keep it as low as possible to maintain compatibility with older servers. + public static final String REST_PROTOCOL_VERSION_SUBSONIC = "1.2.0"; + public static final String REST_CLIENT_ID = "Audinaut"; + public static final String LAST_VERSION = "subsonic.version"; + + // Names for intent extras. + public static final String INTENT_EXTRA_NAME_ID = "subsonic.id"; + public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name"; + public static final String INTENT_EXTRA_NAME_DIRECTORY = "subsonic.directory"; + public static final String INTENT_EXTRA_NAME_CHILD_ID = "subsonic.child.id"; + public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist"; + public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title"; + public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall"; + public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query"; + public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id"; + public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name"; + public static final String INTENT_EXTRA_NAME_PLAYLIST_OWNER = "subsonic.playlist.isOwner"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA = "subsonic.albumlistextra"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset"; + public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle"; + public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch"; + public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ; + public static final String INTENT_EXTRA_NAME_DOWNLOAD = "subsonic.download"; + public static final String INTENT_EXTRA_NAME_DOWNLOAD_VIEW = "subsonic.download_view"; + public static final String INTENT_EXTRA_VIEW_ALBUM = "subsonic.view_album"; + public static final String INTENT_EXTRA_NAME_SHARE = "subsonic.share"; + public static final String INTENT_EXTRA_FRAGMENT_TYPE = "fragmentType"; + public static final String INTENT_EXTRA_REFRESH_LISTINGS = "refreshListings"; + public static final String INTENT_EXTRA_SEARCH_SONG = "searchSong"; + public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks"; + public static final String INTENT_EXTRA_PLAY_LAST = "playLast"; + public static final String INTENT_EXTRA_ENTRY = "passedEntry"; + + // Preferences keys. + public static final String PREFERENCES_KEY_SERVER_KEY = "server"; + public static final String PREFERENCES_KEY_SERVER_COUNT = "serverCount"; + public static final String PREFERENCES_KEY_SERVER_ADD = "serverAdd"; + public static final String PREFERENCES_KEY_SERVER_REMOVE = "serverRemove"; + public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId"; + public static final String PREFERENCES_KEY_SERVER_NAME = "serverName"; + public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl"; + public static final String PREFERENCES_KEY_SERVER_INTERNAL_URL = "serverInternalUrl"; + public static final String PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID = "serverLocalNetworkSSID"; + public static final String PREFERENCES_KEY_TEST_CONNECTION = "serverTestConnection"; + public static final String PREFERENCES_KEY_OPEN_BROWSER = "openBrowser"; + public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"; + public static final String PREFERENCES_KEY_USERNAME = "username"; + public static final String PREFERENCES_KEY_PASSWORD = "password"; + public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime"; + public static final String PREFERENCES_KEY_THEME = "theme"; + public static final String PREFERENCES_KEY_FULL_SCREEN = "fullScreen"; + public static final String PREFERENCES_KEY_DISPLAY_TRACK = "displayTrack"; + public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi"; + public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile"; + public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout"; + public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize"; + public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation"; + public static final String PREFERENCES_KEY_PRELOAD_COUNT_WIFI = "preloadCountWifi"; + public static final String PREFERENCES_KEY_PRELOAD_COUNT_MOBILE = "preloadCountMobile"; + public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia"; + public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons"; + public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload"; + public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode"; + public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload"; + public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize"; + public static final String PREFERENCES_KEY_OFFLINE = "offline"; + public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss"; + public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear"; + public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear"; + public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre"; + public static final String PREFERENCES_KEY_KEEP_SCREEN_ON = "keepScreenOn"; + public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn"; + public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings"; + public static final String PREFERENCES_KEY_PERSISTENT_NOTIFICATION = "persistentNotification"; + public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback"; + public static final String PREFERENCES_KEY_REMOVE_PLAYED = "removePlayed"; + public static final String PREFERENCES_KEY_KEEP_PLAYED_CNT = "keepPlayedCount"; + public static final String PREFERENCES_KEY_SHUFFLE_MODE = "shuffleMode2"; + public static final String PREFERENCES_KEY_SHUFFLE_MODE_EXTRA = "shuffleModeExtra"; + public static final String PREFERENCES_KEY_SYNC_ENABLED = "syncEnabled"; + public static final String PREFERENCES_KEY_SYNC_INTERVAL = "syncInterval"; + public static final String PREFERENCES_KEY_SYNC_WIFI = "syncWifi"; + public static final String PREFERENCES_KEY_SYNC_NOTIFICATION = "syncNotification"; + public static final String PREFERENCES_KEY_SYNC_MOST_RECENT = "syncMostRecent"; + public static final String PREFERENCES_KEY_PAUSE_DISCONNECT = "pauseOnDisconnect"; + public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget"; + public static final String PREFERENCES_KEY_CUSTOM_SORT_ENABLED = "customSortEnabled"; + public static final String PREFERENCES_KEY_SHARED_ENABLED = "sharedEnabled"; + public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab"; + // public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + public static final String PREFERENCES_KEY_SONG_PRESS_ACTION = "songPressAction"; + public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt"; + public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName"; + public static final String PREFERENCES_KEY_PLAYLIST_ID = "suggestedPlaylistId"; + public static final String PREFERENCES_KEY_SERVER_SYNC = "serverSync"; + public static final String PREFERENCES_KEY_RECENT_COUNT = "mostRecentCount"; + public static final String PREFERENCES_KEY_REPLAY_GAIN = "replayGain"; + public static final String PREFERENCES_KEY_REPLAY_GAIN_BUMP = "replayGainBump2"; + public static final String PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED = "replayGainUntagged2"; + public static final String PREFERENCES_KEY_REPLAY_GAIN_TYPE= "replayGainType"; + public static final String PREFERENCES_KEY_ALBUMS_PER_FOLDER = "albumsPerFolder"; + public static final String PREFERENCES_KEY_FIRST_LEVEL_ARTIST = "firstLevelArtist"; + public static final String PREFERENCES_KEY_START_ON_HEADPHONES = "startOnHeadphones"; + public static final String PREFERENCES_KEY_COLOR_ACTION_BAR = "colorActionBar"; + public static final String PREFERENCES_KEY_SHUFFLE_BY_ALBUM = "shuffleByAlbum"; + public static final String PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER = "neverResumePlayQueue"; + public static final String PREFERENCES_KEY_BATCH_MODE = "batchMode"; + public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification"; + + public static final String OFFLINE_STAR_COUNT = "starCount"; + public static final String OFFLINE_STAR_ID = "starID"; + public static final String OFFLINE_STAR_SEARCH = "starTitle"; + public static final String OFFLINE_STAR_SETTING = "starSetting"; + + public static final String CACHE_KEY_IGNORE = "ignoreArticles"; + public static final String CACHE_AUDIO_SESSION_ID = "audioSessionId"; + public static final String CACHE_BLOCK_TOKEN_USE = "blockTokenUse"; + + public static final String MAIN_BACK_STACK = "backStackIds"; + public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize"; + public static final String MAIN_NOW_PLAYING = "nowPlayingId"; + public static final String MAIN_NOW_PLAYING_SECONDARY = "nowPlayingSecondaryId"; + public static final String MAIN_SLIDE_PANEL_STATE = "slidePanelState"; + public static final String FRAGMENT_LIST = "fragmentList"; + public static final String FRAGMENT_LIST2 = "fragmentList2"; + public static final String FRAGMENT_EXTRA = "fragmentExtra"; + public static final String FRAGMENT_DOWNLOAD_FLIPPER = "fragmentDownloadFlipper"; + public static final String FRAGMENT_NAME = "fragmentName"; + public static final String FRAGMENT_POSITION = "fragmentPosition"; + + // Name of the preferences file. + public static final String PREFERENCES_FILE_NAME = "github.nvllsvm.audinaut_preferences"; + public static final String OFFLINE_SYNC_NAME = "github.nvllsvm.audinaut.offline"; + public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults"; + + // Account prefs + public static final String SYNC_ACCOUNT_NAME = "Subsonic Account"; + public static final String SYNC_ACCOUNT_TYPE = "subsonic.org"; + public static final String SYNC_ACCOUNT_PLAYLIST_AUTHORITY = "github.nvllsvm.audinaut.playlists.provider"; + public static final String SYNC_ACCOUNT_MOST_RECENT_AUTHORITY = "github.nvllsvm.audinaut.mostrecent.provider"; + + public static final String TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"; + + public static final String ALBUM_ART_FILE = "albumart.jpg"; + + private Constants() { + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java b/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java new file mode 100644 index 0000000..bde9266 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java @@ -0,0 +1,115 @@ +package github.nvllsvm.audinaut.util; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.Log; + +import org.eclipse.jetty.util.ArrayQueue; + +import java.util.Queue; + +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFileItemHelperCallback extends ItemTouchHelper.SimpleCallback { + private static final String TAG = DownloadFileItemHelperCallback.class.getSimpleName(); + + private SubsonicFragment fragment; + private boolean mainList; + + private BackgroundTask pendingTask = null; + private Queue pendingOperations = new ArrayQueue(); + + public DownloadFileItemHelperCallback(SubsonicFragment fragment, boolean mainList) { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); + this.fragment = fragment; + this.mainList = mainList; + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder fromHolder, RecyclerView.ViewHolder toHolder) { + int from = fromHolder.getAdapterPosition(); + int to = toHolder.getAdapterPosition(); + getSectionAdapter().moveItem(from, to); + + synchronized (pendingOperations) { + pendingOperations.add(new Pair<>(from, to)); + updateDownloadService(); + } + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + SongView songView = (SongView) ((UpdateView.UpdateViewHolder) viewHolder).getUpdateView(); + DownloadFile downloadFile = songView.getDownloadFile(); + + getSectionAdapter().removeItem(downloadFile); + synchronized (pendingOperations) { + pendingOperations.add(downloadFile); + updateDownloadService(); + } + } + + public DownloadService getDownloadService() { + return fragment.getDownloadService(); + } + public SectionAdapter getSectionAdapter() { + return fragment.getCurrentAdapter(); + } + + private void updateDownloadService() { + if(pendingTask == null) { + final DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + pendingTask = new SilentBackgroundTask(downloadService) { + @Override + protected Void doInBackground() throws Throwable { + boolean running = true; + while(running) { + Object nextOperation = null; + synchronized (pendingOperations) { + if(!pendingOperations.isEmpty()) { + nextOperation = pendingOperations.remove(); + } + } + + if(nextOperation != null) { + if(nextOperation instanceof Pair) { + Pair swap = (Pair) nextOperation; + downloadService.swap(mainList, swap.getFirst(), swap.getSecond()); + } else if(nextOperation instanceof DownloadFile) { + DownloadFile downloadFile = (DownloadFile) nextOperation; + if(mainList) { + downloadService.remove(downloadFile); + } else { + downloadService.removeBackground(downloadFile); + } + } + } else { + running = false; + } + } + + synchronized (pendingOperations) { + pendingTask = null; + + // Start a task if this is non-empty. Means someone added while we were running operations + if(!pendingOperations.isEmpty()) { + updateDownloadService(); + } + } + return null; + } + }; + pendingTask.execute(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java b/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java new file mode 100644 index 0000000..d7585b8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java @@ -0,0 +1,102 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.TypedValue; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; + +import github.nvllsvm.audinaut.R; + +public class DrawableTint { + private static final Map attrMap = new HashMap<>(); + private static final WeakHashMap tintedDrawables = new WeakHashMap<>(); + + public static Drawable getTintedDrawable(Context context, @DrawableRes int drawableRes) { + return getTintedDrawable(context, drawableRes, R.attr.colorAccent); + } + public static Drawable getTintedDrawable(Context context, @DrawableRes int drawableRes, @AttrRes int colorAttr) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = getColorRes(context, colorAttr); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } + public static Drawable getTintedDrawableFromColor(Context context, @DrawableRes int drawableRes, @ColorRes int colorRes) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = context.getResources().getColor(colorRes); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } + public static int getColorRes(Context context, @AttrRes int colorAttr) { + int color; + if(attrMap.containsKey(colorAttr)) { + color = attrMap.get(colorAttr); + } else { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(colorAttr, typedValue, true); + color = typedValue.data; + attrMap.put(colorAttr, color); + } + + return color; + } + public static int getDrawableRes(Context context, @AttrRes int drawableAttr) { + if(attrMap.containsKey(drawableAttr)) { + return attrMap.get(drawableAttr); + } else { + int[] attrs = new int[]{drawableAttr}; + TypedArray typedArray = context.obtainStyledAttributes(attrs); + @DrawableRes int drawableRes = typedArray.getResourceId(0, 0); + typedArray.recycle(); + attrMap.put(drawableAttr, drawableRes); + return drawableRes; + } + } + public static Drawable getTintedAttrDrawable(Context context, @AttrRes int drawableAttr, @AttrRes int colorAttr) { + if(tintedDrawables.containsKey(drawableAttr)) { + return getTintedDrawable(context, attrMap.get(drawableAttr), colorAttr); + } + + @DrawableRes int drawableRes = getDrawableRes(context, drawableAttr); + return getTintedDrawable(context, drawableRes, colorAttr); + } + + public static void wipeTintCache() { + attrMap.clear(); + tintedDrawables.clear(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java b/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java new file mode 100644 index 0000000..30d6c23 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java @@ -0,0 +1,20 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +public final class EnvironmentVariables { + public static final String PASTEBIN_DEV_KEY = ""; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java new file mode 100644 index 0000000..ec4dc7c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java @@ -0,0 +1,800 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.Iterator; +import java.util.List; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Environment; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.service.MediaStoreService; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * @author Sindre Mehus + */ +public class FileUtil { + + private static final String TAG = FileUtil.class.getSimpleName(); + private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"}; + private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"}; + private static final List MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); + private static final List PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u"); + private static final int MAX_FILENAME_LENGTH = 254 - ".complete.mp3".length(); + private static File DEFAULT_MUSIC_DIR; + private static final Kryo kryo = new Kryo(); + private static HashMap entryLookup; + + static { + kryo.register(MusicDirectory.Entry.class); + kryo.register(Indexes.class); + kryo.register(Artist.class); + kryo.register(MusicFolder.class); + kryo.register(Playlist.class); + kryo.register(Genre.class); + } + + public static File getAnySong(Context context) { + File dir = getMusicDirectory(context); + return getAnySong(context, dir); + } + private static File getAnySong(Context context, File dir) { + for(File file: dir.listFiles()) { + if(file.isDirectory()) { + return getAnySong(context, file); + } + + String extension = getExtension(file.getName()); + if(MUSIC_FILE_EXTENSIONS.contains(extension)) { + return file; + } + } + + return null; + } + + public static File getEntryFile(Context context, MusicDirectory.Entry entry) { + if(entry.isDirectory()) { + return getAlbumDirectory(context, entry); + } else { + return getSongFile(context, entry); + } + } + + public static File getSongFile(Context context, MusicDirectory.Entry song) { + File dir = getAlbumDirectory(context, song); + + StringBuilder fileName = new StringBuilder(); + Integer track = song.getTrack(); + if (track != null) { + if (track < 10) { + fileName.append("0"); + } + fileName.append(track).append("-"); + } + + fileName.append(fileSystemSafe(song.getTitle())); + if(fileName.length() >= MAX_FILENAME_LENGTH) { + fileName.setLength(MAX_FILENAME_LENGTH); + } + + fileName.append("."); + if (song.getTranscodedSuffix() != null) { + fileName.append(song.getTranscodedSuffix()); + } else { + fileName.append(song.getSuffix()); + } + + return new File(dir, fileName.toString()); + } + + public static File getPlaylistFile(Context context, String server, String name) { + File playlistDir = getPlaylistDirectory(context, server); + return new File(playlistDir, fileSystemSafe(name) + ".m3u"); + } + public static void writePlaylistFile(Context context, File file, MusicDirectory playlist) throws IOException { + FileWriter fw = new FileWriter(file); + BufferedWriter bw = new BufferedWriter(fw); + try { + fw.write("#EXTM3U\n"); + for (MusicDirectory.Entry e : playlist.getChildren()) { + String filePath = FileUtil.getSongFile(context, e).getAbsolutePath(); + if(! new File(filePath).exists()){ + String ext = FileUtil.getExtension(filePath); + String base = FileUtil.getBaseName(filePath); + filePath = base + ".complete." + ext; + } + fw.write(filePath + "\n"); + } + } catch(Exception e) { + Log.w(TAG, "Failed to save playlist: " + playlist.getName()); + } finally { + bw.close(); + fw.close(); + } + } + public static File getPlaylistDirectory(Context context) { + File playlistDir = new File(getSubsonicDirectory(context), "playlists"); + ensureDirectoryExistsAndIsReadWritable(playlistDir); + return playlistDir; + } + public static File getPlaylistDirectory(Context context, String server) { + File playlistDir = new File(getPlaylistDirectory(context), server); + ensureDirectoryExistsAndIsReadWritable(playlistDir); + return playlistDir; + } + + public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { + if(entry.getId().indexOf(ImageLoader.PLAYLIST_PREFIX) != -1) { + File dir = getAlbumArtDirectory(context); + return new File(dir, Util.md5Hex(ImageLoader.PLAYLIST_PREFIX + entry.getTitle()) + ".jpeg"); + } else { + File albumDir = getAlbumDirectory(context, entry); + File artFile; + File albumFile = getAlbumArtFile(albumDir); + File hexFile = getHexAlbumArtFile(context, albumDir); + if (albumDir.exists()) { + if (hexFile.exists()) { + hexFile.renameTo(albumFile); + } + artFile = albumFile; + } else { + artFile = hexFile; + } + return artFile; + } + } + + public static File getAlbumArtFile(File albumDir) { + return new File(albumDir, Constants.ALBUM_ART_FILE); + } + public static File getHexAlbumArtFile(Context context, File albumDir) { + return new File(getAlbumArtDirectory(context), Util.md5Hex(albumDir.getPath()) + ".jpeg"); + } + + public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) { + File albumArtFile = getAlbumArtFile(context, entry); + if (albumArtFile.exists()) { + final BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + BitmapFactory.decodeFile(albumArtFile.getPath(), opt); + opt.inPurgeable = true; + opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); + opt.inJustDecodeBounds = false; + + Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt); + return bitmap == null ? null : getScaledBitmap(bitmap, size); + } + return null; + } + + public static File getMiscDirectory(Context context) { + File dir = new File(getSubsonicDirectory(context), "misc"); + ensureDirectoryExistsAndIsReadWritable(dir); + ensureDirectoryExistsAndIsReadWritable(new File(dir, ".nomedia")); + return dir; + } + + public static File getMiscFile(Context context, String url) { + return new File(getMiscDirectory(context), Util.md5Hex(url) + ".jpeg"); + } + + public static Bitmap getMiscBitmap(Context context, String url, int size) { + return null; + } + + public static Bitmap getSampledBitmap(byte[] bytes, int size) { + return getSampledBitmap(bytes, size, true); + } + public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean allowUnscaled) { + final BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); + opt.inPurgeable = true; + opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); + opt.inJustDecodeBounds = false; + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); + if(bitmap == null) { + return null; + } else { + return getScaledBitmap(bitmap, size, allowUnscaled); + } + } + public static Bitmap getScaledBitmap(Bitmap bitmap, int size) { + return getScaledBitmap(bitmap, size, true); + } + public static Bitmap getScaledBitmap(Bitmap bitmap, int size, boolean allowUnscaled) { + // Don't waste time scaling if the difference is minor + // Large album arts still need to be scaled since displayed as is on now playing! + if(allowUnscaled && size < 400 && bitmap.getWidth() < (size * 1.1)) { + return bitmap; + } else { + return Bitmap.createScaledBitmap(bitmap, size, Util.getScaledHeight(bitmap, size), true); + } + } + + public static File getAlbumArtDirectory(Context context) { + File albumArtDir = new File(getSubsonicDirectory(context), "artwork"); + ensureDirectoryExistsAndIsReadWritable(albumArtDir); + ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia")); + return albumArtDir; + } + + public static File getArtistDirectory(Context context, Artist artist) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName())); + return dir; + } + public static File getArtistDirectory(Context context, MusicDirectory.Entry artist) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getTitle())); + return dir; + } + + public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) { + File dir = null; + if (entry.getPath() != null) { + File f = new File(fileSystemSafeDir(entry.getPath())); + String folder = getMusicDirectory(context).getPath(); + if(entry.isDirectory()) { + folder += "/" + f.getPath(); + } else if(f.getParent() != null) { + folder += "/" + f.getParent(); + } + dir = new File(folder); + } else { + MusicDirectory.Entry firstSong; + if(!Util.isOffline(context)) { + firstSong = lookupChild(context, entry, false); + if(firstSong != null) { + File songFile = FileUtil.getSongFile(context, firstSong); + dir = songFile.getParentFile(); + } + } + + if(dir == null) { + String artist = fileSystemSafe(entry.getArtist()); + String album = fileSystemSafe(entry.getAlbum()); + if("unnamed".equals(album)) { + album = fileSystemSafe(entry.getTitle()); + } + dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album); + } + } + return dir; + } + + public static MusicDirectory.Entry lookupChild(Context context, MusicDirectory.Entry entry, boolean allowDir) { + // Initialize lookupMap if first time called + String lookupName = Util.getCacheName(context, "entryLookup"); + if(entryLookup == null) { + entryLookup = deserialize(context, lookupName, HashMap.class); + + // Create it if + if(entryLookup == null) { + entryLookup = new HashMap(); + } + } + + // Check if this lookup has already been done before + MusicDirectory.Entry child = entryLookup.get(entry.getId()); + if(child != null) { + return child; + } + + // Do a special lookup since 4.7+ doesn't match artist/album to entry.getPath + String s = Util.getRestUrl(context, null, false) + entry.getId(); + String cacheName = (Util.isTagBrowsing(context) ? "album-" : "directory-") + s.hashCode() + ".ser"; + MusicDirectory entryDir = FileUtil.deserialize(context, cacheName, MusicDirectory.class); + + if(entryDir != null) { + List songs = entryDir.getChildren(allowDir, true); + if(songs.size() > 0) { + child = songs.get(0); + entryLookup.put(entry.getId(), child); + serialize(context, entryLookup, lookupName); + return child; + } + } + + return null; + } + + public static void createDirectoryForParent(File file) { + File dir = file.getParentFile(); + if (!dir.exists()) { + if (!dir.mkdirs()) { + Log.e(TAG, "Failed to create directory " + dir); + } + } + } + + private static File createDirectory(Context context, String name) { + File dir = new File(getSubsonicDirectory(context), name); + if (!dir.exists() && !dir.mkdirs()) { + Log.e(TAG, "Failed to create " + name); + } + return dir; + } + + public static File getSubsonicDirectory(Context context) { + return context.getExternalFilesDir(null); + } + + public static File getDefaultMusicDirectory(Context context) { + if(DEFAULT_MUSIC_DIR == null) { + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music"); + + if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) { + Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR); + + // Some devices seem to have screwed up the new media directory API. Go figure. Try again with standard locations + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = ContextCompat.getExternalFilesDirs(context, null); + + DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music"); + if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) { + Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR); + } else { + Log.w(TAG, "Stupid OEM's messed up media dir API added in 5.0"); + } + } + } + } + + return DEFAULT_MUSIC_DIR; + } + private static File getBestDir(File[] dirs) { + // Past 5.0 we can query directly for SD Card + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null && Environment.isExternalStorageRemovable(dirs[i])) { + return dirs[i]; + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + for(int i = dirs.length - 1; i >= 0; i--) { + if(dirs[i] != null) { + return dirs[i]; + } + } + + // Should be impossible to be reached + return dirs[0]; + } + + public static File getMusicDirectory(Context context) { + String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, getDefaultMusicDirectory(context).getPath()); + File dir = new File(path); + return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(context); + } + public static boolean deleteMusicDirectory(Context context) { + File musicDirectory = FileUtil.getMusicDirectory(context); + MediaStoreService mediaStore = new MediaStoreService(context); + return recursiveDelete(musicDirectory, mediaStore); + } + public static void deleteSerializedCache(Context context) { + for(File file: context.getCacheDir().listFiles()) { + if(file.getName().indexOf(".ser") != -1) { + file.delete(); + } + } + } + public static boolean deleteArtworkCache(Context context) { + File artDirectory = FileUtil.getAlbumArtDirectory(context); + return recursiveDelete(artDirectory); + } + public static boolean recursiveDelete(File dir) { + return recursiveDelete(dir, null); + } + public static boolean recursiveDelete(File dir, MediaStoreService mediaStore) { + if (dir != null && dir.exists()) { + File[] list = dir.listFiles(); + if(list != null) { + for(File file: list) { + if(file.isDirectory()) { + if(!recursiveDelete(file, mediaStore)) { + return false; + } + } else if(file.exists()) { + if(!file.delete()) { + return false; + } else if(mediaStore != null) { + mediaStore.deleteFromMediaStore(file); + } + } + } + } + return dir.delete(); + } + return false; + } + + public static void deleteEmptyDir(File dir) { + try { + File[] children = dir.listFiles(); + if(children == null) { + return; + } + + // No songs left in the folder + if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) { + Util.delete(children[0]); + children = dir.listFiles(); + } + + // Delete empty directory + if (children.length == 0) { + Util.delete(dir); + } + } catch(Exception e) { + Log.w(TAG, "Error while trying to delete empty dir", e); + } + } + + public static void unpinSong(Context context, File saveFile) { + // Don't try to unpin a song which isn't actually pinned + if(saveFile.getName().contains(".complete")) { + return; + } + + // Unpin file, rename to .complete + File completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + + ".complete." + FileUtil.getExtension(saveFile.getName())); + + if(!saveFile.renameTo(completeFile)) { + Log.w(TAG, "Failed to upin " + saveFile + " to " + completeFile); + } else { + try { + new MediaStoreService(context).renameInMediaStore(completeFile, saveFile); + } catch(Exception e) { + Log.w(TAG, "Failed to write to media store"); + } + } + } + + public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) { + if (dir == null) { + return false; + } + + if (dir.exists()) { + if (!dir.isDirectory()) { + Log.w(TAG, dir + " exists but is not a directory."); + return false; + } + } else { + if (dir.mkdirs()) { + Log.i(TAG, "Created directory " + dir); + } else { + Log.w(TAG, "Failed to create directory " + dir); + return false; + } + } + + if (!dir.canRead()) { + Log.w(TAG, "No read permission for directory " + dir); + return false; + } + + if (!dir.canWrite()) { + Log.w(TAG, "No write permission for directory " + dir); + return false; + } + return true; + } + public static boolean verifyCanWrite(File dir) { + if(ensureDirectoryExistsAndIsReadWritable(dir)) { + try { + File tmp = new File(dir, "checkWrite"); + tmp.createNewFile(); + if(tmp.exists()) { + if(tmp.delete()) { + return true; + } else { + Log.w(TAG, "Failed to delete temp file, retrying"); + + // This should never be reached since this is a file Audinaut created! + Thread.sleep(100L); + tmp = new File(dir, "checkWrite"); + if(tmp.delete()) { + return true; + } else { + Log.w(TAG, "Failed retry to delete temp file"); + return false; + } + } + } else { + Log.w(TAG, "Temp file does not actually exist"); + return false; + } + } catch(Exception e) { + Log.w(TAG, "Failed to create tmp file", e); + return false; + } + } else { + return false; + } + } + + /** + * Makes a given filename safe by replacing special characters like slashes ("/" and "\") + * with dashes ("-"). + * + * @param filename The filename in question. + * @return The filename with special characters replaced by hyphens. + */ + private static String fileSystemSafe(String filename) { + if (filename == null || filename.trim().length() == 0) { + return "unnamed"; + } + + for (String s : FILE_SYSTEM_UNSAFE) { + filename = filename.replace(s, "-"); + } + return filename; + } + + /** + * Makes a given filename safe by replacing special characters like colons (":") + * with dashes ("-"). + * + * @param path The path of the directory in question. + * @return The the directory name with special characters replaced by hyphens. + */ + private static String fileSystemSafeDir(String path) { + if (path == null || path.trim().length() == 0) { + return ""; + } + + for (String s : FILE_SYSTEM_UNSAFE_DIR) { + path = path.replace(s, "-"); + } + return path; + } + + /** + * Similar to {@link File#listFiles()}, but returns a sorted set. + * Never returns {@code null}, instead a warning is logged, and an empty set is returned. + */ + public static SortedSet listFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + Log.w(TAG, "Failed to list children for " + dir.getPath()); + return new TreeSet(); + } + + return new TreeSet(Arrays.asList(files)); + } + + public static SortedSet listMediaFiles(File dir) { + SortedSet files = listFiles(dir); + Iterator iterator = files.iterator(); + while (iterator.hasNext()) { + File file = iterator.next(); + if (!file.isDirectory() && !isMediaFile(file)) { + iterator.remove(); + } + } + return files; + } + + private static boolean isMediaFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_FILE_EXTENSIONS.contains(extension); + } + + public static boolean isMusicFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_FILE_EXTENSIONS.contains(extension); + } + + public static boolean isPlaylistFile(File file) { + String extension = getExtension(file.getName()); + return PLAYLIST_FILE_EXTENSIONS.contains(extension); + } + + /** + * Returns the extension (the substring after the last dot) of the given file. The dot + * is not included in the returned extension. + * + * @param name The filename in question. + * @return The extension, or an empty string if no extension is found. + */ + public static String getExtension(String name) { + int index = name.lastIndexOf('.'); + return index == -1 ? "" : name.substring(index + 1).toLowerCase(); + } + + /** + * Returns the base name (the substring before the last dot) of the given file. The dot + * is not included in the returned basename. + * + * @param name The filename in question. + * @return The base name, or an empty string if no basename is found. + */ + public static String getBaseName(String name) { + int index = name.lastIndexOf('.'); + return index == -1 ? name : name.substring(0, index); + } + + public static Long[] getUsedSize(Context context, File file) { + long number = 0L; + long permanent = 0L; + long size = 0L; + + if(file.isFile()) { + if(isMediaFile(file)) { + if(file.getAbsolutePath().indexOf(".complete") == -1) { + permanent++; + } + return new Long[] {1L, permanent, file.length()}; + } else { + return new Long[] {0L, 0L, 0L}; + } + } else { + for (File child : FileUtil.listFiles(file)) { + Long[] pair = getUsedSize(context, child); + number += pair[0]; + permanent += pair[1]; + size += pair[2]; + } + return new Long[] {number, permanent, size}; + } + } + + public static boolean serialize(Context context, T obj, String fileName) { + Output out = null; + try { + RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw"); + out = new Output(new FileOutputStream(file.getFD())); + synchronized (kryo) { + kryo.writeObject(out, obj); + } + return true; + } catch (Throwable x) { + Log.w(TAG, "Failed to serialize object to " + fileName); + return false; + } finally { + Util.close(out); + } + } + + public static T deserialize(Context context, String fileName, Class tClass) { + return deserialize(context, fileName, tClass, 0); + } + + public static T deserialize(Context context, String fileName, Class tClass, int hoursOld) { + Input in = null; + try { + File file = new File(context.getCacheDir(), fileName); + if(!file.exists()) { + return null; + } + + if(hoursOld != 0) { + Date fileDate = new Date(file.lastModified()); + // Convert into hours + long age = (new Date().getTime() - fileDate.getTime()) / 1000 / 3600; + if(age > hoursOld) { + return null; + } + } + + RandomAccessFile randomFile = new RandomAccessFile(file, "r"); + + in = new Input(new FileInputStream(randomFile.getFD())); + synchronized (kryo) { + T result = kryo.readObject(in, tClass); + return result; + } + } catch(FileNotFoundException e) { + // Different error message + Log.w(TAG, "No serialization for object from " + fileName); + return null; + } catch (Throwable x) { + Log.w(TAG, "Failed to deserialize object from " + fileName); + return null; + } finally { + Util.close(in); + } + } + + public static boolean serializeCompressed(Context context, T obj, String fileName) { + Output out = null; + try { + RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw"); + out = new Output(new DeflaterOutputStream(new FileOutputStream(file.getFD()))); + synchronized (kryo) { + kryo.writeObject(out, obj); + } + return true; + } catch (Throwable x) { + Log.w(TAG, "Failed to serialize compressed object to " + fileName); + return false; + } finally { + Util.close(out); + } + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public static T deserializeCompressed(Context context, String fileName, Class tClass) { + Input in = null; + try { + RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "r"); + + in = new Input(new InflaterInputStream(new FileInputStream(file.getFD()))); + synchronized (kryo) { + T result = kryo.readObject(in, tClass); + return result; + } + } catch(FileNotFoundException e) { + // Different error message + Log.w(TAG, "No serialization compressed for object from " + fileName); + return null; + } catch (Throwable x) { + Log.w(TAG, "Failed to deserialize compressed object from " + fileName); + return null; + } finally { + Util.close(in); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java b/app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java new file mode 100644 index 0000000..1f9128c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java @@ -0,0 +1,523 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.Log; +import android.support.v4.util.LruCache; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; + +/** + * Asynchronous loading of images, with caching. + *

+ * There should normally be only one instance of this class. + * + * @author Sindre Mehus + */ +public class ImageLoader { + private static final String TAG = ImageLoader.class.getSimpleName(); + public static final String PLAYLIST_PREFIX = "pl-"; + + private Context context; + private LruCache cache; + private Handler handler; + private Bitmap nowPlaying; + private Bitmap nowPlayingSmall; + private final int imageSizeDefault; + private final int imageSizeLarge; + private boolean clearingCache = false; + private final int cacheSize; + + private final static int[] COLORS = {0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444}; + + public ImageLoader(Context context) { + this.context = context; + handler = new Handler(Looper.getMainLooper()); + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + cacheSize = maxMemory / 4; + + // Determine the density-dependent image sizes. + imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + imageSizeLarge = Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); + + cache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight() / 1024; + } + + @Override + protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) { + if(evicted) { + if((oldBitmap != nowPlaying && oldBitmap != nowPlayingSmall) || clearingCache) { + oldBitmap.recycle(); + } else if(oldBitmap != newBitmap) { + cache.put(key, oldBitmap); + } + } + } + }; + } + + public void clearCache() { + nowPlaying = null; + nowPlayingSmall = null; + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + clearingCache = true; + cache.evictAll(); + clearingCache = false; + return null; + } + }.execute(); + } + public void onLowMemory(float percent) { + Log.i(TAG, "Cache size: " + cache.size() + " => " + Math.round(cacheSize * (1 - percent)) + " out of " + cache.maxSize()); + cache.resize(Math.round(cacheSize * (1 - percent))); + } + public void onUIVisible() { + if(cache.maxSize() != cacheSize) { + Log.i(TAG, "Returned to full cache size"); + cache.resize(cacheSize); + } + } + + public void setNowPlayingSmall(Bitmap bitmap) { + nowPlayingSmall = bitmap; + } + + private Bitmap getUnknownImage(MusicDirectory.Entry entry, int size) { + String key; + int color; + if(entry == null) { + key = getKey("unknown", size); + color = COLORS[0]; + + return getUnknownImage(key, size, color, null, null); + } else { + key = getKey(entry.getId() + "unknown", size); + String hash; + if(entry.getAlbum() != null) { + hash = entry.getAlbum(); + } else if(entry.getArtist() != null) { + hash = entry.getArtist(); + } else { + hash = entry.getId(); + } + color = COLORS[Math.abs(hash.hashCode()) % COLORS.length]; + + return getUnknownImage(key, size, color, entry.getAlbum(), entry.getArtist()); + } + } + private Bitmap getUnknownImage(String key, int size, int color, String topText, String bottomText) { + Bitmap bitmap = cache.get(key); + if(bitmap == null) { + bitmap = createUnknownImage(size, color, topText, bottomText); + cache.put(key, bitmap); + } + + return bitmap; + } + private Bitmap createUnknownImage(int size, int primaryColor, String topText, String bottomText) { + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint color = new Paint(); + color.setColor(primaryColor); + canvas.drawRect(0, 0, size, size * 2.0f / 3.0f, color); + + color.setShader(new LinearGradient(0, 0, 0, size / 3.0f, Color.rgb(82, 82, 82), Color.BLACK, Shader.TileMode.MIRROR)); + canvas.drawRect(0, size * 2.0f / 3.0f, size, size, color); + + if(topText != null || bottomText != null) { + Paint font = new Paint(); + font.setFlags(Paint.ANTI_ALIAS_FLAG); + font.setColor(Color.WHITE); + font.setTextSize(3.0f + size * 0.07f); + + if(topText != null) { + canvas.drawText(topText, size * 0.05f, size * 0.6f, font); + } + + if(bottomText != null) { + canvas.drawText(bottomText, size * 0.05f, size * 0.8f, font); + } + } + + return bitmap; + } + + public Bitmap getCachedImage(Context context, MusicDirectory.Entry entry, boolean large) { + int size = large ? imageSizeLarge : imageSizeDefault; + if(entry == null || entry.getCoverArt() == null) { + return getUnknownImage(entry, size); + } + + Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size)); + if(bitmap == null || bitmap.isRecycled()) { + bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); + String key = getKey(entry.getCoverArt(), size); + cache.put(key, bitmap); + cache.get(key); + } + + if(bitmap != null && bitmap.isRecycled()) { + bitmap = null; + } + return bitmap; + } + + public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) { + int size = large ? imageSizeLarge : imageSizeDefault; + return loadImage(view, entry, large, size, crossfade); + } + public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossfade) { + // If we know this a artist, try to load artist info instead + if(entry != null && !entry.isAlbum() && !Util.isOffline(context)) { + SilentBackgroundTask task = new ArtistImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade); + task.execute(); + return task; + } else if(entry != null && entry.getCoverArt() == null && entry.isDirectory() && !Util.isOffline(context)) { + // Try to lookup child cover art + MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, entry, true); + if(firstChild != null) { + entry.setCoverArt(firstChild.getCoverArt()); + } + } + + Bitmap bitmap; + if (entry == null || entry.getCoverArt() == null) { + bitmap = getUnknownImage(entry, size); + setImage(view, Util.createDrawableFromBitmap(context, bitmap), crossfade); + return null; + } + + bitmap = cache.get(getKey(entry.getCoverArt(), size)); + if (bitmap != null && !bitmap.isRecycled()) { + final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); + setImage(view, drawable, crossfade); + if(large) { + nowPlaying = bitmap; + } + return null; + } + + if (!large) { + setImage(view, null, false); + } + ImageTask task = new ViewImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade); + task.execute(); + return task; + } + + public SilentBackgroundTask loadImage(View view, String url, boolean large) { + Bitmap bitmap; + int size = large ? imageSizeLarge : imageSizeDefault; + if (url == null) { + String key = getKey(url + "unknown", size); + int color = COLORS[Math.abs(key.hashCode()) % COLORS.length]; + bitmap = getUnknownImage(key, size, color, null, null); + setImage(view, Util.createDrawableFromBitmap(context, bitmap), true); + return null; + } + + bitmap = cache.get(getKey(url, size)); + if (bitmap != null && !bitmap.isRecycled()) { + final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); + setImage(view, drawable, true); + return null; + } + setImage(view, null, false); + + SilentBackgroundTask task = new ViewUrlTask(view.getContext(), view, url, size); + task.execute(); + return task; + } + + public SilentBackgroundTask loadImage(View view, Playlist playlist, boolean large, boolean crossfade) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + String id; + if(Util.isOffline(context)) { + id = PLAYLIST_PREFIX + playlist.getName(); + entry.setTitle(playlist.getComment()); + } else { + id = PLAYLIST_PREFIX + playlist.getId(); + entry.setTitle(playlist.getName()); + } + entry.setId(id); + entry.setCoverArt(id); + // So this isn't treated as a artist + entry.setParent(""); + + return loadImage(view, entry, large, crossfade); + } + + private String getKey(String coverArtId, int size) { + return coverArtId + size; + } + + private void setImage(View view, final Drawable drawable, boolean crossfade) { + if (view instanceof TextView) { + // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though. + TextView textView = (TextView) view; + textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + } else if (view instanceof ImageView) { + final ImageView imageView = (ImageView) view; + if (crossfade && drawable != null) { + Drawable existingDrawable = imageView.getDrawable(); + if (existingDrawable == null) { + Bitmap emptyImage; + if(drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) { + emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } else { + emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault, Bitmap.Config.ARGB_8888); + } + existingDrawable = new BitmapDrawable(context.getResources(), emptyImage); + } else if(existingDrawable instanceof TransitionDrawable) { + // This should only ever be used if user is skipping through many songs quickly + TransitionDrawable tmp = (TransitionDrawable) existingDrawable; + existingDrawable = tmp.getDrawable(tmp.getNumberOfLayers() - 1); + } + if(existingDrawable != null && drawable != null) { + Drawable[] layers = new Drawable[]{existingDrawable, drawable}; + final TransitionDrawable transitionDrawable = new TransitionDrawable(layers); + imageView.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(250); + + // Get rid of transition drawable after transition occurs + handler.postDelayed(new Runnable() { + @Override + public void run() { + // Only execute if still on same transition drawable + if (imageView.getDrawable() == transitionDrawable) { + imageView.setImageDrawable(drawable); + } + } + }, 500L); + } else { + imageView.setImageDrawable(drawable); + } + } else { + imageView.setImageDrawable(drawable); + } + } + } + + public abstract class ImageTask extends SilentBackgroundTask { + private final Context mContext; + protected final MusicDirectory.Entry mEntry; + private final int mSize; + private final int mSaveSize; + private final boolean mIsNowPlaying; + protected Drawable mDrawable; + + public ImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying) { + super(context); + mContext = context; + mEntry = entry; + mSize = size; + mSaveSize = saveSize; + mIsNowPlaying = isNowPlaying; + } + + @Override + protected Void doInBackground() throws Throwable { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, null, this); + if(bitmap != null) { + String key = getKey(mEntry.getCoverArt(), mSize); + cache.put(key, bitmap); + // Make sure key is the most recently "used" + cache.get(key); + if (mIsNowPlaying) { + nowPlaying = bitmap; + } + } else { + bitmap = getUnknownImage(mEntry, mSize); + } + + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + } catch (Throwable x) { + Log.e(TAG, "Failed to download album art.", x); + cancelled.set(true); + } + + return null; + } + } + + private class ViewImageTask extends ImageTask { + protected boolean mCrossfade; + private View mView; + + public ViewImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) { + super(context, entry, size, saveSize, isNowPlaying); + + mView = view; + mCrossfade = crossfade; + } + + @Override + protected void done(Void result) { + setImage(mView, mDrawable, mCrossfade); + } + } + + private class ArtistImageTask extends SilentBackgroundTask { + private final Context mContext; + private final MusicDirectory.Entry mEntry; + private final int mSize; + private final int mSaveSize; + private final boolean mIsNowPlaying; + private Drawable mDrawable; + private boolean mCrossfade; + private View mView; + + private SilentBackgroundTask subTask; + + public ArtistImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) { + super(context); + mContext = context; + mEntry = entry; + mSize = size; + mSaveSize = saveSize; + mIsNowPlaying = isNowPlaying; + mView = view; + mCrossfade = crossfade; + } + + @Override + protected Void doInBackground() throws Throwable { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + + // Figure out whether we are going to get a artist image or the standard image + if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { + // Try to lookup child cover art + MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); + if (firstChild != null) { + mEntry.setCoverArt(firstChild.getCoverArt()); + } + } + + if (mEntry != null && mEntry.getCoverArt() != null) { + subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); + } else { + // If entry is null as well, we need to just set as a blank image + Bitmap bitmap = getUnknownImage(mEntry, mSize); + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + return null; + } + + // Execute whichever way we decided to go + subTask.doInBackground(); + } catch (Throwable x) { + Log.e(TAG, "Failed to get artist info", x); + cancelled.set(true); + } + return null; + } + + @Override + public void done(Void result) { + if(subTask != null) { + subTask.done(result); + } else if(mDrawable != null) { + setImage(mView, mDrawable, mCrossfade); + } + } + } + + private class ViewUrlTask extends SilentBackgroundTask { + private final Context mContext; + private final String mUrl; + private final ImageView mView; + private Drawable mDrawable; + private int mSize; + + public ViewUrlTask(Context context, View view, String url, int size) { + super(context); + mContext = context; + mView = (ImageView) view; + mUrl = url; + mSize = size; + } + + @Override + protected Void doInBackground() throws Throwable { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + Bitmap bitmap = musicService.getBitmap(mUrl, mSize, mContext, null, this); + if(bitmap != null) { + String key = getKey(mUrl, mSize); + cache.put(key, bitmap); + // Make sure key is the most recently "used" + cache.get(key); + + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + } + } catch (Throwable x) { + Log.e(TAG, "Failed to download from url " + mUrl, x); + cancelled.set(true); + } + + return null; + } + + @Override + protected void done(Void result) { + if(mDrawable != null) { + mView.setImageDrawable(mDrawable); + } else { + failedToDownload(); + } + } + + protected void failedToDownload() { + + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java new file mode 100644 index 0000000..5436e09 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java @@ -0,0 +1,73 @@ +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; + +import github.nvllsvm.audinaut.activity.SubsonicActivity; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public abstract class LoadingTask extends BackgroundTask { + + private final Activity tabActivity; + private ProgressDialog loading; + private final boolean cancellable; + + public LoadingTask(Activity activity) { + super(activity); + tabActivity = activity; + this.cancellable = true; + } + public LoadingTask(Activity activity, final boolean cancellable) { + super(activity); + tabActivity = activity; + this.cancellable = cancellable; + } + + @Override + public void execute() { + loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + cancel(); + } + }); + + queue.offer(task = new Task() { + @Override + public void onDone(T result) { + if(loading.isShowing()) { + loading.dismiss(); + } + done(result); + } + + @Override + public void onError(Throwable t) { + if(loading.isShowing()) { + loading.dismiss(); + } + error(t); + } + }); + } + + @Override + public boolean isCancelled() { + return (tabActivity instanceof SubsonicActivity && ((SubsonicActivity) tabActivity).isDestroyedCompat()) || cancelled.get(); + } + + @Override + public void updateProgress(final String message) { + if(!cancelled.get()) { + getHandler().post(new Runnable() { + @Override + public void run() { + loading.setMessage(message); + } + }); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java new file mode 100644 index 0000000..c690a95 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.Menu; + +import java.io.File; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistEntryView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class MenuUtil { + private final static String TAG = MenuUtil.class.getSimpleName(); + + public static void hideMenuItems(Context context, Menu menu, UpdateView updateView) { + if(!Util.isOffline(context)) { + // If we are looking at a standard song view, get downloadFile to cache what options to show + if(updateView instanceof SongView) { + SongView songView = (SongView) updateView; + DownloadFile downloadFile = songView.getDownloadFile(); + + try { + if(downloadFile != null) { + if(downloadFile.isWorkDone()) { + // Remove permanent cache menu if already perma cached + if(downloadFile.isSaved()) { + menu.setGroupVisible(R.id.hide_pin, false); + } + + // Remove cache option no matter what if already downloaded + menu.setGroupVisible(R.id.hide_download, false); + } else { + // Remove delete option if nothing to delete + menu.setGroupVisible(R.id.hide_delete, false); + } + } + } catch(Exception e) { + Log.w(TAG, "Failed to lookup downloadFile info", e); + } + } + // Apply similar logic to album views + else if(updateView instanceof AlbumView || updateView instanceof ArtistView || updateView instanceof ArtistEntryView) { + File folder = null; + if(updateView instanceof AlbumView) { + folder = ((AlbumView) updateView).getFile(); + } else if(updateView instanceof ArtistView) { + folder = ((ArtistView) updateView).getFile(); + } else if(updateView instanceof ArtistEntryView) { + folder = ((ArtistEntryView) updateView).getFile(); + } + + try { + if(folder != null && !folder.exists()) { + menu.setGroupVisible(R.id.hide_delete, false); + } + } catch(Exception e) { + Log.w(TAG, "Failed to lookup album directory info", e); + } + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java b/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java new file mode 100644 index 0000000..7a28ecf --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java @@ -0,0 +1,330 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Handler; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RemoteViews; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.provider.AudinautWidgetProvider; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class Notifications { + private static final String TAG = Notifications.class.getSimpleName(); + + // Notification IDs. + public static final int NOTIFICATION_ID_PLAYING = 100; + public static final int NOTIFICATION_ID_DOWNLOADING = 102; + + private static boolean playShowing = false; + private static boolean downloadShowing = false; + private static boolean downloadForeground = false; + private static boolean persistentPlayingShowing = false; + + private final static Pair NOTIFICATION_TEXT_COLORS = new Pair(); + + public static void showPlayingNotification(final Context context, final DownloadService downloadService, final Handler handler, MusicDirectory.Entry song) { + // Set the icon, scrolling text and timestamp + final Notification notification = new Notification(R.drawable.stat_notify_playing, song.getTitle(), System.currentTimeMillis()); + + final boolean playing = downloadService.getPlayerState() == PlayerState.STARTED; + if(playing) { + notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; + } + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){ + RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded); + setupViews(expandedContentView ,context, song, true, playing); + notification.bigContentView = expandedContentView; + notification.priority = Notification.PRIORITY_HIGH; + } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notification.visibility = Notification.VISIBILITY_PUBLIC; + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HEADS_UP_NOTIFICATION, false) && !UpdateView.hasActiveActivity()) { + notification.vibrate = new long[0]; + } + } + + RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); + setupViews(smallContentView, context, song, false, playing); + notification.contentView = smallContentView; + + Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); + + playShowing = true; + if(downloadForeground && downloadShowing) { + downloadForeground = false; + handler.post(new Runnable() { + @Override + public void run() { + downloadService.stopForeground(true); + showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size()); + downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + } + }); + } else { + handler.post(new Runnable() { + @Override + public void run() { + if (playing) { + downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + } else { + playShowing = false; + persistentPlayingShowing = true; + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + downloadService.stopForeground(false); + notificationManager.notify(NOTIFICATION_ID_PLAYING, notification); + } + } + }); + } + + // Update widget + AudinautWidgetProvider.notifyInstances(context, downloadService, playing); + } + + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing) { + // Use the same text for the ticker and the expanded notification + String title = song.getTitle(); + String arist = song.getArtist(); + String album = song.getAlbum(); + + // Set the album art. + try { + ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context); + Bitmap bitmap = null; + if(imageLoader != null) { + bitmap = imageLoader.getCachedImage(context, song, false); + } + if (bitmap == null) { + // set default album art + rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } else { + imageLoader.setNowPlayingSmall(bitmap); + rv.setImageViewBitmap(R.id.notification_image, bitmap); + } + } catch (Exception x) { + Log.w(TAG, "Failed to get notification cover art", x); + rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } + + // set the text for the notifications + rv.setTextViewText(R.id.notification_title, title); + rv.setTextViewText(R.id.notification_artist, arist); + rv.setTextViewText(R.id.notification_album, album); + + boolean persistent = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false); + if(persistent) { + if(expanded) { + rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_start); + + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } else { + rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_start); + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_close); + } + } else { + // Necessary for switching back since it appears to re-use the same layout + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } + + // Create actions for media buttons + PendingIntent pendingIntent; + int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 0; + if(persistent && !expanded) { + pause = R.id.control_previous; + next = R.id.control_pause; + close = R.id.control_next; + } else { + previous = R.id.control_previous; + pause = R.id.control_pause; + next = R.id.control_next; + } + + if(persistent && close == 0 && expanded) { + close = R.id.notification_close; + rv.setViewVisibility(close, View.VISIBLE); + } + + if(previous > 0) { + Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS"); + prevIntent.setComponent(new ComponentName(context, DownloadService.class)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(previous, pendingIntent); + } + if(rewind > 0) { + Intent rewindIntent = new Intent("KEYCODE_MEDIA_REWIND"); + rewindIntent.setComponent(new ComponentName(context, DownloadService.class)); + rewindIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)); + pendingIntent = PendingIntent.getService(context, 0, rewindIntent, 0); + rv.setOnClickPendingIntent(rewind, pendingIntent); + } + if(pause > 0) { + if(playing) { + Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE"); + pauseIntent.setComponent(new ComponentName(context, DownloadService.class)); + pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0); + rv.setOnClickPendingIntent(pause, pendingIntent); + } else { + Intent prevIntent = new Intent("KEYCODE_MEDIA_START"); + prevIntent.setComponent(new ComponentName(context, DownloadService.class)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(pause, pendingIntent); + } + } + if(next > 0) { + Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT"); + nextIntent.setComponent(new ComponentName(context, DownloadService.class)); + nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); + pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); + rv.setOnClickPendingIntent(next, pendingIntent); + } + if(fastForward > 0) { + Intent fastForwardIntent = new Intent("KEYCODE_MEDIA_FAST_FORWARD"); + fastForwardIntent.setComponent(new ComponentName(context, DownloadService.class)); + fastForwardIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + pendingIntent = PendingIntent.getService(context, 0, fastForwardIntent, 0); + rv.setOnClickPendingIntent(fastForward, pendingIntent); + } + if(close > 0) { + Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP"); + prevIntent.setComponent(new ComponentName(context, DownloadService.class)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(close, pendingIntent); + } + } + + public static void hidePlayingNotification(final Context context, final DownloadService downloadService, Handler handler) { + playShowing = false; + + // Remove notification and remove the service from the foreground + handler.post(new Runnable() { + @Override + public void run() { + downloadService.stopForeground(true); + + if(persistentPlayingShowing) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID_PLAYING); + persistentPlayingShowing = false; + } + } + }); + + // Get downloadNotification in foreground if playing + if(downloadShowing) { + showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size()); + } + + // Update widget + AudinautWidgetProvider.notifyInstances(context, downloadService, false); + } + + public static void showDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler, DownloadFile file, int size) { + Intent cancelIntent = new Intent(context, DownloadService.class); + cancelIntent.setAction(DownloadService.CANCEL_DOWNLOADS); + PendingIntent cancelPI = PendingIntent.getService(context, 0, cancelIntent, 0); + + String currentDownloading, currentSize; + if(file != null) { + currentDownloading = file.getSong().getTitle(); + currentSize = Util.formatLocalizedBytes(file.getEstimatedSize(), context); + } else { + currentDownloading = "none"; + currentSize = "0"; + } + + NotificationCompat.Builder builder; + builder = new NotificationCompat.Builder(context) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(context.getResources().getString(R.string.download_downloading_title, size)) + .setContentText(context.getResources().getString(R.string.download_downloading_summary, currentDownloading)) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(context.getResources().getString(R.string.download_downloading_summary_expanded, currentDownloading, currentSize))) + .setProgress(10, 5, true) + .setOngoing(true) + .addAction(R.drawable.notification_close, + context.getResources().getString(R.string.common_cancel), + cancelPI); + + Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + builder.setContentIntent(PendingIntent.getActivity(context, 2, notificationIntent, 0)); + + final Notification notification = builder.build(); + downloadShowing = true; + if(playShowing) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID_DOWNLOADING, notification); + } else { + downloadForeground = true; + handler.post(new Runnable() { + @Override + public void run() { + downloadService.startForeground(NOTIFICATION_ID_DOWNLOADING, notification); + } + }); + } + + } + public static void hideDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler) { + downloadShowing = false; + if(playShowing) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID_DOWNLOADING); + } else { + downloadForeground = false; + handler.post(new Runnable() { + @Override + public void run() { + downloadService.stopForeground(true); + } + }); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Pair.java b/app/src/main/java/github/nvllsvm/audinaut/util/Pair.java new file mode 100644 index 0000000..5d45533 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Pair.java @@ -0,0 +1,54 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Pair implements Serializable { + + private S first; + private T second; + + public Pair() { + } + + public Pair(S first, T second) { + this.first = first; + this.second = second; + } + + public S getFirst() { + return first; + } + + public void setFirst(S first) { + this.first = first; + } + + public T getSecond() { + return second; + } + + public void setSecond(T second) { + this.second = second; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.java b/app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.java new file mode 100644 index 0000000..aed50d2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.java @@ -0,0 +1,28 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +/** + * @author Sindre Mehus + */ +public interface ProgressListener { + void updateProgress(String message); + void updateProgress(int messageId); + void updateCache(int changeCode); +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java b/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java new file mode 100644 index 0000000..bb31699 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java @@ -0,0 +1,44 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus +*/ +package github.nvllsvm.audinaut.util; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInput; +import android.app.backup.SharedPreferencesBackupHelper; +import android.os.ParcelFileDescriptor; + +import java.io.IOError; +import java.io.IOException; + +import github.nvllsvm.audinaut.util.Constants; + +public class SettingsBackupAgent extends BackupAgentHelper { + @Override + public void onCreate() { + super.onCreate(); + SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME); + addHelper("mypreferences", helper); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException{ + super.onRestore(data, appVersionCode, newState); + Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_CACHE_LOCATION).apply(); + } + } diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java b/app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java new file mode 100644 index 0000000..284218f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java @@ -0,0 +1,212 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.FileUtil; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class ShufflePlayBuffer { + + private static final String TAG = ShufflePlayBuffer.class.getSimpleName(); + private static final String CACHE_FILENAME = "shuffleBuffer.ser"; + + private ScheduledExecutorService executorService; + private Runnable runnable; + private boolean firstRun = true; + private final ArrayList buffer = new ArrayList(); + private int lastCount = -1; + private DownloadService context; + private boolean awaitingResults = false; + private int capacity; + private int refillThreshold; + + private SharedPreferences.OnSharedPreferenceChangeListener listener; + private int currentServer; + private String currentFolder = ""; + private String genre = ""; + private String startYear = ""; + private String endYear = ""; + + public ShufflePlayBuffer(DownloadService context) { + this.context = context; + + executorService = Executors.newSingleThreadScheduledExecutor(); + runnable = new Runnable() { + @Override + public void run() { + refill(); + } + }; + executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); + + // Calculate out the capacity and refill threshold based on the user's random size preference + int shuffleListSize = Math.max(1, Integer.parseInt(Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"))); + // ex: default 20 -> 50 + capacity = shuffleListSize * 5 / 2; + capacity = Math.min(500, capacity); + + // ex: default 20 -> 40 + refillThreshold = capacity * 4 / 5; + } + + public List get(int size) { + clearBufferIfnecessary(); + // Make sure fetcher is running if needed + restart(); + + List result = new ArrayList(size); + synchronized (buffer) { + boolean removed = false; + while (!buffer.isEmpty() && result.size() < size) { + result.add(buffer.remove(buffer.size() - 1)); + removed = true; + } + + // Re-cache if anything is taken out + if(removed) { + FileUtil.serialize(context, buffer, CACHE_FILENAME); + } + } + Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining."); + if(result.isEmpty()) { + awaitingResults = true; + } + return result; + } + + public void shutdown() { + executorService.shutdown(); + Util.getPreferences(context).unregisterOnSharedPreferenceChangeListener(listener); + } + + private void restart() { + synchronized(buffer) { + if(buffer.size() <= refillThreshold && lastCount != 0 && executorService.isShutdown()) { + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS); + } + } + } + + private void refill() { + // Check if active server has changed. + clearBufferIfnecessary(); + + if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { + executorService.shutdown(); + return; + } + + try { + MusicService service = MusicServiceFactory.getMusicService(context); + + // Get capacity based + int n = capacity - buffer.size(); + String folder = null; + if(!Util.isTagBrowsing(context)) { + folder = Util.getSelectedMusicFolderId(context); + } + MusicDirectory songs = service.getRandomSongs(n, folder, genre, startYear, endYear, context, null); + + synchronized (buffer) { + lastCount = 0; + for(MusicDirectory.Entry entry: songs.getChildren()) { + if(!buffer.contains(entry)) { + buffer.add(entry); + lastCount++; + } + } + Log.i(TAG, "Refilled shuffle play buffer with " + lastCount + " songs."); + + // Cache buffer + FileUtil.serialize(context, buffer, CACHE_FILENAME); + } + } catch (Exception x) { + // Give it one more try before quitting + if(lastCount != -2) { + lastCount = -2; + } else if(lastCount == -2) { + lastCount = 0; + } + Log.w(TAG, "Failed to refill shuffle play buffer.", x); + } + + if(awaitingResults) { + awaitingResults = false; + context.checkDownloads(); + } + } + + private void clearBufferIfnecessary() { + synchronized (buffer) { + final SharedPreferences prefs = Util.getPreferences(context); + if (currentServer != Util.getActiveServer(context) + || !Util.equals(currentFolder, Util.getSelectedMusicFolderId(context)) + || (genre != null && !genre.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""))) + || (startYear != null && !startYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""))) + || (endYear != null && !endYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "")))) { + lastCount = -1; + currentServer = Util.getActiveServer(context); + currentFolder = Util.getSelectedMusicFolderId(context); + genre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""); + startYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""); + endYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, ""); + buffer.clear(); + + if(firstRun) { + ArrayList cacheList = FileUtil.deserialize(context, CACHE_FILENAME, ArrayList.class); + if(cacheList != null) { + buffer.addAll(cacheList); + } + + listener = new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + clearBufferIfnecessary(); + restart(); + } + }; + prefs.registerOnSharedPreferenceChangeListener(listener); + firstRun = false; + } else { + // Clear cache + File file = new File(context.getCacheDir(), CACHE_FILENAME); + file.delete(); + } + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SilentBackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/SilentBackgroundTask.java new file mode 100644 index 0000000..53e9a89 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SilentBackgroundTask.java @@ -0,0 +1,48 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.content.Context; + +/** + * @author Sindre Mehus + */ +public abstract class SilentBackgroundTask extends BackgroundTask { + public SilentBackgroundTask(Context context) { + super(context); + } + + @Override + public void execute() { + queue.offer(task = new Task()); + } + + @Override + protected void done(T result) { + // Don't do anything unless overriden + } + + @Override + public void updateProgress(int messageId) { + } + + @Override + public void updateProgress(String message) { + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java new file mode 100644 index 0000000..05c79f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java @@ -0,0 +1,41 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; + +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; + +public abstract class SilentServiceTask extends SilentBackgroundTask { + protected MusicService musicService; + + public SilentServiceTask(Context context) { + super(context); + } + + @Override + protected T doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(getContext()); + return doInBackground(musicService); + } + + protected abstract T doInBackground(MusicService musicService) throws Throwable; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java b/app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java new file mode 100644 index 0000000..eb5fd6d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java @@ -0,0 +1,37 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.os.Binder; + +/** + * @author Sindre Mehus + */ +public class SimpleServiceBinder extends Binder { + + private final S service; + + public SimpleServiceBinder(S service) { + this.service = service; + } + + public S getService() { + return service; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java b/app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java new file mode 100644 index 0000000..203effd --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java @@ -0,0 +1,260 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadFile; + +public class SongDBHandler extends SQLiteOpenHelper { + private static final String TAG = SongDBHandler.class.getSimpleName(); + private static SongDBHandler dbHandler; + + private static final int DATABASE_VERSION = 2; + public static final String DATABASE_NAME = "SongsDB"; + + public static final String TABLE_SONGS = "RegisteredSongs"; + public static final String SONGS_ID = "id"; + public static final String SONGS_SERVER_KEY = "serverKey"; + public static final String SONGS_SERVER_ID = "serverId"; + public static final String SONGS_COMPLETE_PATH = "completePath"; + public static final String SONGS_LAST_PLAYED = "lastPlayed"; + public static final String SONGS_LAST_COMPLETED = "lastCompleted"; + + private Context context; + + private SongDBHandler(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_SONGS + " ( " + + SONGS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + SONGS_SERVER_KEY + " INTEGER NOT NULL, " + + SONGS_SERVER_ID + " TEXT NOT NULL, " + + SONGS_COMPLETE_PATH + " TEXT NOT NULL, " + + SONGS_LAST_PLAYED + " INTEGER, " + + SONGS_LAST_COMPLETED + " INTEGER, " + + "UNIQUE(" + SONGS_SERVER_KEY + ", " + SONGS_SERVER_ID + "))"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SONGS); + this.onCreate(db); + } + + public synchronized void addSong(DownloadFile downloadFile) { + addSong(Util.getMostRecentActiveServer(context), downloadFile); + } + public synchronized void addSong(int instance, DownloadFile downloadFile) { + SQLiteDatabase db = this.getWritableDatabase(); + addSong(db, instance, downloadFile); + db.close(); + } + protected synchronized void addSong(SQLiteDatabase db, DownloadFile downloadFile) { + addSong(db, Util.getMostRecentActiveServer(context), downloadFile); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, DownloadFile downloadFile) { + addSong(db, instance, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath()); + } + + protected synchronized void addSong(SQLiteDatabase db, String id, String absolutePath) { + addSong(db, Util.getMostRecentActiveServer(context), id, absolutePath); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, String id, String absolutePath) { + addSongImpl(db, Util.getRestUrlHash(context, instance), id, absolutePath); + } + protected synchronized void addSongImpl(SQLiteDatabase db, int serverKey, String id, String absolutePath) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, id); + values.put(SONGS_COMPLETE_PATH, absolutePath); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + public synchronized void addSongs(int instance, List entries) { + SQLiteDatabase db = this.getWritableDatabase(); + + List> pairs = new ArrayList<>(); + for(MusicDirectory.Entry entry: entries) { + pairs.add(new Pair<>(entry.getId(), FileUtil.getSongFile(context, entry).getAbsolutePath())); + } + addSongs(db, instance, pairs); + + db.close(); + } + public synchronized void addSongs(SQLiteDatabase db, int instance, List> entries) { + addSongsImpl(db, Util.getRestUrlHash(context, instance), entries); + } + protected synchronized void addSongsImpl(SQLiteDatabase db, int serverKey, List> entries) { + db.beginTransaction(); + try { + for (Pair entry : entries) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, entry.getFirst()); + values.put(SONGS_COMPLETE_PATH, entry.getSecond()); + // Util.sleepQuietly(10000); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + db.setTransactionSuccessful(); + } catch(Exception e) {} + + db.endTransaction(); + } + + public synchronized void setSongPlayed(DownloadFile downloadFile, boolean submission) { + // TODO: In case of offline want to update all matches + Pair pair = getOnlineSongId(downloadFile); + if(pair == null) { + return; + } + int serverKey = pair.getFirst(); + String id = pair.getSecond(); + + // Open and make sure song is in db + SQLiteDatabase db = this.getWritableDatabase(); + addSongImpl(db, serverKey, id, downloadFile.getSaveFile().getAbsolutePath()); + + // Update song's last played + ContentValues values = new ContentValues(); + values.put(submission ? SONGS_LAST_COMPLETED : SONGS_LAST_PLAYED, System.currentTimeMillis()); + db.update(TABLE_SONGS, values, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}); + db.close(); + } + + public boolean hasBeenPlayed(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[0] != null && lastPlayed[0] > 0; + } + public boolean hasBeenCompleted(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[1] != null && lastPlayed[1] > 0; + } + public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { + return getLastPlayed(getOnlineSongId(entry)); + } + protected synchronized Long[] getLastPlayed(Pair pair) { + if(pair == null) { + return null; + } else { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } + } + public synchronized Long[] getLastPlayed(int serverKey, String id) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_LAST_PLAYED, SONGS_LAST_COMPLETED}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}, null, null, null, null); + + try { + cursor.moveToFirst(); + + Long[] dates = new Long[2]; + dates[0] = cursor.getLong(0); + dates[1] = cursor.getLong(1); + return dates; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + + public synchronized Pair getOnlineSongId(MusicDirectory.Entry entry) { + return getOnlineSongId(Util.getRestUrlHash(context), entry.getId(), FileUtil.getSongFile(context, entry).getAbsolutePath(), Util.isOffline(context) ? false : true); + } + public synchronized Pair getOnlineSongId(DownloadFile downloadFile) { + return getOnlineSongId(Util.getRestUrlHash(context), downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), Util.isOffline(context) ? false : true); + } + + public synchronized Pair getOnlineSongId(int serverKey, MusicDirectory.Entry entry) { + return getOnlineSongId(serverKey, new DownloadFile(context, entry, true)); + } + public synchronized Pair getOnlineSongId(int serverKey, DownloadFile downloadFile) { + return getOnlineSongId(serverKey, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), true); + } + public synchronized Pair getOnlineSongId(int serverKey, String id, String savePath, boolean requireServerKey) { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { + if(requireServerKey) { + return getIdFromPath(serverKey, savePath); + } else { + return getIdFromPath(savePath); + } + } else { + return new Pair<>(serverKey, id); + } + } + + public synchronized Pair getIdFromPath(String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_COMPLETE_PATH + " = ?", new String[] { path }, null, null, SONGS_LAST_PLAYED + " DESC", null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + public synchronized Pair getIdFromPath(int serverKey, String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_COMPLETE_PATH + " = ?", new String[] {Integer.toString(serverKey), path }, null, null, null, null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + + public static SongDBHandler getHandler(Context context) { + if(dbHandler == null) { + dbHandler = new SongDBHandler(context); + } + + return dbHandler; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java new file mode 100644 index 0000000..e3d9873 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java @@ -0,0 +1,156 @@ +package github.nvllsvm.audinaut.util; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; + +/** + * Created by Scott on 11/24/13. + */ +public final class SyncUtil { + private static String TAG = SyncUtil.class.getSimpleName(); + private static ArrayList syncedPlaylists; + private static String url; + + private static void checkRestURL(Context context) { + int instance = Util.getActiveServer(context); + String newURL = Util.getRestUrl(context, null, instance, false); + if(url == null || !url.equals(newURL)) { + syncedPlaylists = null; + url = newURL; + } + } + + // Playlist sync + public static boolean isSyncedPlaylist(Context context, String playlistId) { + checkRestURL(context); + if(syncedPlaylists == null) { + syncedPlaylists = getSyncedPlaylists(context); + } + return syncedPlaylists.contains(new SyncSet(playlistId)); + } + public static ArrayList getSyncedPlaylists(Context context) { + return getSyncedPlaylists(context, Util.getActiveServer(context)); + } + public static ArrayList getSyncedPlaylists(Context context, int instance) { + String syncFile = getPlaylistSyncFile(context, instance); + ArrayList playlists = FileUtil.deserializeCompressed(context, syncFile, ArrayList.class); + if(playlists == null) { + playlists = new ArrayList(); + + // Try to convert old style into new style + ArrayList oldPlaylists = FileUtil.deserialize(context, syncFile, ArrayList.class); + // If exists, time to convert! + if(oldPlaylists != null) { + for(String id: oldPlaylists) { + playlists.add(new SyncSet(id)); + } + + FileUtil.serializeCompressed(context, playlists, syncFile); + } + } + return playlists; + } + public static void setSyncedPlaylists(Context context, int instance, ArrayList playlists) { + FileUtil.serializeCompressed(context, playlists, getPlaylistSyncFile(context, instance)); + } + public static void addSyncedPlaylist(Context context, String playlistId) { + String playlistFile = getPlaylistSyncFile(context); + ArrayList playlists = getSyncedPlaylists(context); + SyncSet set = new SyncSet(playlistId); + if(!playlists.contains(set)) { + playlists.add(set); + } + FileUtil.serializeCompressed(context, playlists, playlistFile); + syncedPlaylists = playlists; + } + public static void removeSyncedPlaylist(Context context, String playlistId) { + int instance = Util.getActiveServer(context); + removeSyncedPlaylist(context, playlistId, instance); + } + public static void removeSyncedPlaylist(Context context, String playlistId, int instance) { + String playlistFile = getPlaylistSyncFile(context, instance); + ArrayList playlists = getSyncedPlaylists(context, instance); + SyncSet set = new SyncSet(playlistId); + if(playlists.contains(set)) { + playlists.remove(set); + FileUtil.serializeCompressed(context, playlists, playlistFile); + syncedPlaylists = playlists; + } + } + public static String getPlaylistSyncFile(Context context) { + int instance = Util.getActiveServer(context); + return getPlaylistSyncFile(context, instance); + } + public static String getPlaylistSyncFile(Context context, int instance) { + return "sync-playlist-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser"; + } + + // Most Recently Added + public static ArrayList getSyncedMostRecent(Context context, int instance) { + ArrayList list = FileUtil.deserialize(context, getMostRecentSyncFile(context, instance), ArrayList.class); + if(list == null) { + list = new ArrayList(); + } + return list; + } + public static void removeMostRecentSyncFiles(Context context) { + int total = Util.getServerCount(context); + for(int i = 0; i < total; i++) { + File file = new File(context.getCacheDir(), getMostRecentSyncFile(context, i)); + file.delete(); + } + } + public static String getMostRecentSyncFile(Context context, int instance) { + return "sync-most_recent-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser"; + } + + public static String joinNames(List names) { + StringBuilder builder = new StringBuilder(); + for (String val : names) { + builder.append(val).append(", "); + } + builder.setLength(builder.length() - 2); + return builder.toString(); + } + + public static class SyncSet implements Serializable { + public String id; + public List synced; + + protected SyncSet() { + + } + public SyncSet(String id) { + this.id = id; + } + public SyncSet(String id, List synced) { + this.id = id; + this.synced = synced; + } + + @Override + public boolean equals(Object obj) { + if(obj instanceof SyncSet) { + return this.id.equals(((SyncSet)obj).id); + } else { + return false; + } + } + + @Override + public int hashCode() { + return id.hashCode(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java new file mode 100644 index 0000000..bcba00e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java @@ -0,0 +1,51 @@ +package github.nvllsvm.audinaut.util; + +import github.nvllsvm.audinaut.fragments.SubsonicFragment; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public abstract class TabBackgroundTask extends BackgroundTask { + + private final SubsonicFragment tabFragment; + + public TabBackgroundTask(SubsonicFragment fragment) { + super(fragment.getActivity()); + tabFragment = fragment; + } + + @Override + public void execute() { + tabFragment.setProgressVisible(true); + + queue.offer(task = new Task() { + @Override + public void onDone(T result) { + tabFragment.setProgressVisible(false); + done(result); + } + + @Override + public void onError(Throwable t) { + tabFragment.setProgressVisible(false); + error(t); + } + }); + } + + @Override + public boolean isCancelled() { + return !tabFragment.isAdded() || cancelled.get(); + } + + @Override + public void updateProgress(final String message) { + getHandler().post(new Runnable() { + @Override + public void run() { + tabFragment.updateProgress(message); + } + }); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.java new file mode 100644 index 0000000..173dfca --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.java @@ -0,0 +1,98 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; + +import java.util.Locale; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SettingsActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; + +public final class ThemeUtil { + public static final String THEME_DARK = "dark"; + public static final String THEME_BLACK = "black"; + public static final String THEME_LIGHT = "light"; + public static final String THEME_DAY_NIGHT = "day/night"; + public static final String THEME_DAY_BLACK_NIGHT = "day/black"; + + public static String getTheme(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + String theme = prefs.getString(Constants.PREFERENCES_KEY_THEME, null); + + if(THEME_DAY_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_DARK; + } else { + theme = THEME_LIGHT; + } + } else if(THEME_DAY_BLACK_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_BLACK; + } else { + theme = THEME_LIGHT; + } + } + + return theme; + } + public static int getThemeRes(Context context) { + return getThemeRes(context, getTheme(context)); + } + public static int getThemeRes(Context context, String theme) { + if(context instanceof SubsonicFragmentActivity || context instanceof SettingsActivity) { + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark_No_Actionbar; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black_No_Actionbar; + } else { + return R.style.Theme_Audinaut_Light_No_Actionbar; + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark_No_Color; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black_No_Color; + } else { + return R.style.Theme_Audinaut_Light_No_Color; + } + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black; + } else { + return R.style.Theme_Audinaut_Light; + } + } + } + public static void setTheme(Context context, String theme) { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putString(Constants.PREFERENCES_KEY_THEME, theme); + editor.commit(); + } + + public static void applyTheme(Context context, String theme) { + context.setTheme(getThemeRes(context, theme)); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java b/app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java new file mode 100644 index 0000000..072b15b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java @@ -0,0 +1,55 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.lang.ref.SoftReference; +import java.util.concurrent.TimeUnit; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class TimeLimitedCache { + + private SoftReference value; + private final long ttlMillis; + private long expires; + + public TimeLimitedCache(long ttl, TimeUnit timeUnit) { + this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit); + } + + public T get() { + return System.currentTimeMillis() < expires ? value.get() : null; + } + + public void set(T value) { + set(value, ttlMillis, TimeUnit.MILLISECONDS); + } + + public void set(T value, long ttl, TimeUnit timeUnit) { + this.value = new SoftReference(value); + expires = System.currentTimeMillis() + timeUnit.toMillis(ttl); + } + + public void clear() { + expires = 0L; + value = null; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java b/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java new file mode 100644 index 0000000..212d442 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java @@ -0,0 +1,92 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class UpdateHelper { + private static final String TAG = UpdateHelper.class.getSimpleName(); + + public static abstract class EntryInstanceUpdater { + private Entry entry; + protected int metadataUpdate = DownloadService.METADATA_UPDATED_ALL; + + public EntryInstanceUpdater(Entry entry) { + this.entry = entry; + } + public EntryInstanceUpdater(Entry entry, int metadataUpdate) { + this.entry = entry; + this.metadataUpdate = metadataUpdate; + } + + public abstract void update(Entry found); + + public void execute() { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !entry.isDirectory()) { + boolean serializeChanges = false; + List downloadFiles = downloadService.getDownloads(); + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + + for(DownloadFile file: downloadFiles) { + Entry check = file.getSong(); + if(entry.getId().equals(check.getId())) { + update(check); + serializeChanges = true; + + if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) { + downloadService.onMetadataUpdate(metadataUpdate); + } + } + } + + if(serializeChanges) { + downloadService.serializeQueue(); + } + } + + Entry find = UpdateView.findEntry(entry); + if(find != null) { + update(find); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java new file mode 100644 index 0000000..ece11de --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java @@ -0,0 +1,122 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.adapter.SettingsAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class UserUtil { + private static final String TAG = UserUtil.class.getSimpleName(); + private static final long MIN_VERIFY_DURATION = 1000L * 60L * 60L; + + private static int instance = -1; + private static int instanceHash = -1; + private static User currentUser; + private static long lastVerifiedTime = 0; + + public static void refreshCurrentUser(Context context, boolean forceRefresh) { + refreshCurrentUser(context, forceRefresh, false); + } + public static void refreshCurrentUser(Context context, boolean forceRefresh, boolean unAuth) { + currentUser = null; + if(unAuth) { + lastVerifiedTime = 0; + } + seedCurrentUser(context, forceRefresh); + } + + public static void seedCurrentUser(Context context) { + seedCurrentUser(context, false); + } + public static void seedCurrentUser(final Context context, final boolean refresh) { + // Only try to seed if online + if(Util.isOffline(context)) { + currentUser = null; + return; + } + + final int instance = Util.getActiveServer(context); + final int instanceHash = (instance == 0) ? 0 : Util.getRestUrl(context, null).hashCode(); + if(UserUtil.instance == instance && UserUtil.instanceHash == instanceHash && currentUser != null) { + return; + } else { + UserUtil.instance = instance; + UserUtil.instanceHash = instanceHash; + } + + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + currentUser = MusicServiceFactory.getMusicService(context).getUser(refresh, getCurrentUsername(context, instance), context, null); + + // If running, redo cast selector + DownloadService downloadService = DownloadService.getInstance(); + + return null; + } + + @Override + protected void done(Void result) { + if(context instanceof AppCompatActivity) { + ((AppCompatActivity) context).supportInvalidateOptionsMenu(); + } + } + + @Override + protected void error(Throwable error) { + // Don't do anything, supposed to be background pull + Log.e(TAG, "Failed to seed user information"); + } + }.execute(); + } + + public static User getCurrentUser() { + return currentUser; + } + public static String getCurrentUsername(Context context, int instance) { + SharedPreferences prefs = Util.getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + } + + public static String getCurrentUsername(Context context) { + return getCurrentUsername(context, Util.getActiveServer(context)); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Util.java b/app/src/main/java/github/nvllsvm/audinaut/util/Util.java new file mode 100644 index 0000000..4f684c4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Util.java @@ -0,0 +1,1389 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.graphics.Color; +import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Environment; +import android.text.Html; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SettingsActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.DetailsAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.receiver.MediaButtonIntentReceiver; +import github.nvllsvm.audinaut.service.DownloadService; + +import org.apache.http.HttpEntity; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public final class Util { + private static final String TAG = Util.class.getSimpleName(); + + private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB"); + private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB"); + private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB"); + + private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; + private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a"); + private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a"); + private static SimpleDateFormat DATE_FORMAT_NO_TIME = new SimpleDateFormat("MMM d, yyyy"); + private static int CURRENT_YEAR = new Date().getYear(); + + public static final String EVENT_META_CHANGED = "github.nvllsvm.audinaut.EVENT_META_CHANGED"; + public static final String EVENT_PLAYSTATE_CHANGED = "github.nvllsvm.audinaut.EVENT_PLAYSTATE_CHANGED"; + + public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged"; + public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged"; + + private static OnAudioFocusChangeListener focusListener; + private static boolean pauseFocus = false; + private static boolean lowerFocus = false; + + // Used by hexEncode() + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static Toast toast; + // private static Map> tokens = new HashMap<>(); + private static SparseArray> tokens = new SparseArray<>(); + private static Random random; + + private Util() { + } + + public static boolean isOffline(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false); + } + + public static void setOffline(Context context, boolean offline) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, offline); + editor.commit(); + } + + public static boolean isScreenLitOnDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false); + } + + public static RepeatMode getRepeatMode(Context context) { + SharedPreferences prefs = getPreferences(context); + return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name())); + } + + public static void setRepeatMode(Context context, RepeatMode repeatMode) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name()); + editor.commit(); + } + + public static void setActiveServer(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); + editor.commit(); + } + + public static int getActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + // Don't allow the SERVER_INSTANCE to ever be 0 + return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false) ? 0 : Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); + } + public static int getMostRecentActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + return Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); + } + + public static int getServerCount(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + } + + public static void removeInstanceName(Context context, int instance, int activeInstance) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + + int newInstance = instance + 1; + + // Get what the +1 server details are + String server = prefs.getString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null); + String serverName = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null); + String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); + String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); + String musicFolderId = prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null); + + // Store the +1 server details in the to be deleted instance + editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + instance, server); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); + + // Delete the +1 server instance + // Calling method will loop up to fill this in if +2 server exists + editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null); + editor.commit(); + + if (instance == activeInstance) { + if(instance != 1) { + Util.setActiveServer(context, 1); + } else { + Util.setOffline(context, true); + } + } else if (newInstance == activeInstance) { + Util.setActiveServer(context, instance); + } + } + + public static String getServerName(Context context) { + SharedPreferences prefs = getPreferences(context); + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + } + public static String getServerName(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + } + + public static void setSelectedMusicFolderId(Context context, String musicFolderId) { + int instance = getActiveServer(context); + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); + editor.commit(); + } + + public static String getSelectedMusicFolderId(Context context) { + return getSelectedMusicFolderId(context, getActiveServer(context)); + } + public static String getSelectedMusicFolderId(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + } + + public static boolean getAlbumListsPerFolder(Context context) { + return getAlbumListsPerFolder(context, getActiveServer(context)); + } + public static boolean getAlbumListsPerFolder(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, false); + } + public static void setAlbumListsPerFolder(Context context, boolean perFolder) { + int instance = getActiveServer(context); + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, perFolder); + editor.commit(); + } + + public static boolean getDisplayTrack(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_DISPLAY_TRACK, false); + } + + public static int getMaxBitrate(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 0; + } + + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0")); + } + + public static int getPreloadCount(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 3; + } + + SharedPreferences prefs = getPreferences(context); + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + int preloadCount = Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI : Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE, "-1")); + return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount; + } + + public static int getCacheSizeMB(Context context) { + SharedPreferences prefs = getPreferences(context); + int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); + return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; + } + public static boolean isBatchMode(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false); + } + public static void setBatchMode(Context context, boolean batchMode) { + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, batchMode).commit(); + } + + public static String getRestUrl(Context context, String method) { + return getRestUrl(context, method, true); + } + public static String getRestUrl(Context context, String method, boolean allowAltAddress) { + SharedPreferences prefs = getPreferences(context); + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + return getRestUrl(context, method, prefs, instance, allowAltAddress); + } + public static String getRestUrl(Context context, String method, int instance) { + return getRestUrl(context, method, instance, true); + } + public static String getRestUrl(Context context, String method, int instance, boolean allowAltAddress) { + SharedPreferences prefs = getPreferences(context); + return getRestUrl(context, method, prefs, instance, allowAltAddress); + } + public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance) { + return getRestUrl(context, method, prefs, instance, true); + } + public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance, boolean allowAltAddress) { + StringBuilder builder = new StringBuilder(); + + String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + if(allowAltAddress && Util.isWifiConnected(context)) { + String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, ""); + if(!SSID.isEmpty()) { + String currentSSID = Util.getSSID(context); + + String[] ssidParts = SSID.split(","); + if ("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { + String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); + if (internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { + serverUrl = internalUrl; + } + } + } + } + + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + + builder.append(serverUrl); + if (builder.charAt(builder.length() - 1) != '/') { + builder.append("/"); + } + builder.append("rest/"); + builder.append(method).append(".view"); + builder.append("?u=").append(username); + int hash = (username + password).hashCode(); + Pair values = tokens.get(hash); + if(values == null) { + String salt = new BigInteger(130, getRandom()).toString(32); + String token = md5Hex(password + salt); + values = new Pair<>(salt, token); + tokens.put(hash, values); + } + + builder.append("&s=").append(values.getFirst()); + builder.append("&t=").append(values.getSecond()); + + builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION_SUBSONIC); + builder.append("&c=").append(Constants.REST_CLIENT_ID); + + return builder.toString(); + } + public static int getRestUrlHash(Context context) { + return getRestUrlHash(context, Util.getMostRecentActiveServer(context)); + } + public static int getRestUrlHash(Context context, int instance) { + StringBuilder builder = new StringBuilder(); + + SharedPreferences prefs = Util.getPreferences(context); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null)); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null)); + + return builder.toString().hashCode(); + } + + public static String getBlockTokenUsePref(Context context, int instance) { + return Constants.CACHE_BLOCK_TOKEN_USE + Util.getRestUrl(context, null, instance, false); + } + public static boolean getBlockTokenUse(Context context, int instance) { + return getPreferences(context).getBoolean(getBlockTokenUsePref(context, instance), false); + } + public static void setBlockTokenUse(Context context, int instance, boolean block) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putBoolean(getBlockTokenUsePref(context, instance), block); + editor.commit(); + } + + public static boolean isTagBrowsing(Context context) { + return isTagBrowsing(context, Util.getActiveServer(context)); + } + public static boolean isTagBrowsing(Context context, int instance) { + return true; + } + + public static boolean isSyncEnabled(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SERVER_SYNC + instance, true); + } + + public static String getParentFromEntry(Context context, MusicDirectory.Entry entry) { + if(Util.isTagBrowsing(context)) { + if(!entry.isDirectory()) { + return entry.getAlbumId(); + } else if(entry.isAlbum()) { + return entry.getArtistId(); + } else { + return null; + } + } else { + return entry.getParent(); + } + } + + public static String openToTab(Context context) { + return "Library"; + } + + public static SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0); + } + public static SharedPreferences getOfflineSync(Context context) { + return context.getSharedPreferences(Constants.OFFLINE_SYNC_NAME, 0); + } + + public static String getSyncDefault(Context context) { + SharedPreferences prefs = Util.getOfflineSync(context); + return prefs.getString(Constants.OFFLINE_SYNC_DEFAULT, null); + } + public static void setSyncDefault(Context context, String defaultValue) { + SharedPreferences.Editor editor = Util.getOfflineSync(context).edit(); + editor.putString(Constants.OFFLINE_SYNC_DEFAULT, defaultValue); + editor.commit(); + } + + public static String getCacheName(Context context, String name, String id) { + return getCacheName(context, getActiveServer(context), name, id); + } + public static String getCacheName(Context context, int instance, String name, String id) { + String s = getRestUrl(context, null, instance, false) + id; + return name + "-" + s.hashCode() + ".ser"; + } + public static String getCacheName(Context context, String name) { + return getCacheName(context, getActiveServer(context), name); + } + public static String getCacheName(Context context, int instance, String name) { + String s = getRestUrl(context, null, instance, false); + return name + "-" + s.hashCode() + ".ser"; + } + + public static int offlineStarsCount(Context context) { + SharedPreferences offline = getOfflineSync(context); + return offline.getInt(Constants.OFFLINE_STAR_COUNT, 0); + } + + public static String parseOfflineIDSearch(Context context, String id, String cacheLocation) { + // Try to get this info based off of tags first + String name = parseOfflineIDSearch(id); + if(name != null) { + return name; + } + + // Otherwise go nuts trying to parse from file structure + name = id.replace(cacheLocation, ""); + if(name.startsWith("/")) { + name = name.substring(1); + } + name = name.replace(".complete", "").replace(".partial", ""); + int index = name.lastIndexOf("."); + name = index == -1 ? name : name.substring(0, index); + String[] details = name.split("/"); + + String title = details[details.length - 1]; + if(index == -1) { + if(details.length > 1) { + String artist = "artist:\"" + details[details.length - 2] + "\""; + String simpleArtist = "artist:\"" + title + "\""; + title = "album:\"" + title + "\""; + if(details[details.length - 1].equals(details[details.length - 2])) { + name = title; + } else { + name = "(" + artist + " AND " + title + ")" + " OR " + simpleArtist; + } + } else { + name = "artist:\"" + title + "\" OR album:\"" + title + "\""; + } + } else { + String artist; + if(details.length > 2) { + artist = "artist:\"" + details[details.length - 3] + "\""; + } else { + artist = "(artist:\"" + details[0] + "\" OR album:\"" + details[0] + "\")"; + } + title = "title:\"" + title.substring(title.indexOf('-') + 1) + "\""; + name = artist + " AND " + title; + } + + return name; + } + + public static String parseOfflineIDSearch(String id) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + File file = new File(id); + + if(file.exists()) { + entry.loadMetadata(file); + + if(entry.getArtist() != null) { + String title = file.getName(); + title = title.replace(".complete", "").replace(".partial", ""); + int index = title.lastIndexOf("."); + title = index == -1 ? title : title.substring(0, index); + title = title.substring(title.indexOf('-') + 1); + + String query = "artist:\"" + entry.getArtist() + "\"" + + " AND title:\"" + title + "\""; + + return query; + } else { + return null; + } + } else { + return null; + } + } + + public static String getContentType(HttpEntity entity) { + if (entity == null || entity.getContentType() == null) { + return null; + } + return entity.getContentType().getValue(); + } + + public static boolean isFirstLevelArtist(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true); + } + public static void toggleFirstLevelArtist(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + + if(prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true)) { + editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), false); + } else { + editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true); + } + + editor.commit(); + } + + public static boolean shouldStartOnHeadphones(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false); + } + + public static String getSongPressAction(Context context) { + return getPreferences(context).getString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "all"); + } + + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + public static long copy(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[1024 * 4]; + long count = 0; + int n; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + public static void renameFile(File from, File to) throws IOException { + if(!from.renameTo(to)) { + Log.i(TAG, "Failed to rename " + from + " to " + to); + } + } + + public static void close(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (Throwable x) { + // Ignored + } + } + + public static boolean delete(File file) { + if (file != null && file.exists()) { + if (!file.delete()) { + Log.w(TAG, "Failed to delete file " + file); + return false; + } + Log.i(TAG, "Deleted file " + file); + } + return true; + } + + public static void toast(Context context, int messageId) { + toast(context, messageId, true); + } + + public static void toast(Context context, int messageId, boolean shortDuration) { + toast(context, context.getString(messageId), shortDuration); + } + + public static void toast(Context context, String message) { + toast(context, message, true); + } + + public static void toast(Context context, String message, boolean shortDuration) { + if (toast == null) { + toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + } else { + toast.setText(message); + toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + } + toast.show(); + } + + public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick) { + Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, null); + } + public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) { + Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, onCancel); + } + public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick) { + Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, null); + } + public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) { + Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, onCancel); + } + public static void confirmDialog(Context context, String action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) { + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.common_confirm) + .setMessage(context.getResources().getString(R.string.common_confirm_message, action, subject)) + .setPositiveButton(R.string.common_ok, onClick) + .setNegativeButton(R.string.common_cancel, onCancel) + .show(); + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + *

    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * This method assumes that 1 KB is 1024 bytes. + * To get a localized string, please use formatLocalizedBytes instead. + * + * @param byteCount The number of bytes. + * @return The formatted string. + */ + public static synchronized String formatBytes(long byteCount) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT; + return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + NumberFormat megaByteFormat = MEGA_BYTE_FORMAT; + return megaByteFormat.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + NumberFormat kiloByteFormat = KILO_BYTE_FORMAT; + return kiloByteFormat.format((double) byteCount / 1024); + } + + return byteCount + " B"; + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + *
    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * This method assumes that 1 KB is 1024 bytes. + * This version of the method returns a localized string. + * + * @param byteCount The number of bytes. + * @return The formatted string. + */ + public static synchronized String formatLocalizedBytes(long byteCount, Context context) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + if (GIGA_BYTE_LOCALIZED_FORMAT == null) { + GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte)); + } + + return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + if (MEGA_BYTE_LOCALIZED_FORMAT == null) { + MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte)); + } + + return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + if (KILO_BYTE_LOCALIZED_FORMAT == null) { + KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte)); + } + + return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024); + } + + if (BYTE_LOCALIZED_FORMAT == null) { + BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte)); + } + + return BYTE_LOCALIZED_FORMAT.format((double) byteCount); + } + + public static String formatDuration(Integer seconds) { + if (seconds == null) { + return null; + } + + int hours = seconds / 3600; + int minutes = (seconds / 60) % 60; + int secs = seconds % 60; + + StringBuilder builder = new StringBuilder(7); + if(hours > 0) { + builder.append(hours).append(":"); + if(minutes < 10) { + builder.append("0"); + } + } + builder.append(minutes).append(":"); + if (secs < 10) { + builder.append("0"); + } + builder.append(secs); + return builder.toString(); + } + + public static String formatDate(Context context, String dateString) { + return formatDate(context, dateString, true); + } + public static String formatDate(Context context, String dateString, boolean includeTime) { + if(dateString == null) { + return ""; + } + + try { + dateString = dateString.replace(' ', 'T'); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + return formatDate(dateFormat.parse(dateString), includeTime); + } catch(ParseException e) { + Log.e(TAG, "Failed to parse date string", e); + return dateString; + } + } + public static String formatDate(Date date) { + return formatDate(date, true); + } + public static String formatDate(Date date, boolean includeTime) { + if(date == null) { + return "Never"; + } else { + if(includeTime) { + if (date.getYear() != CURRENT_YEAR) { + return DATE_FORMAT_LONG.format(date); + } else { + return DATE_FORMAT_SHORT.format(date); + } + } else { + return DATE_FORMAT_NO_TIME.format(date); + } + } + } + public static String formatDate(long millis) { + return formatDate(new Date(millis)); + } + + public static String formatBoolean(Context context, boolean value) { + return context.getResources().getString(value ? R.string.common_true : R.string.common_false); + } + + public static boolean equals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data Bytes to convert to hexadecimal characters. + * @return A string containing hexadecimal characters. + */ + public static String hexEncode(byte[] data) { + int length = data.length; + char[] out = new char[length << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < length; i++) { + out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4]; + out[j++] = HEX_DIGITS[0x0F & data[i]]; + } + return new String(out); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + public static String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return hexEncode(md5.digest(s.getBytes(Constants.UTF_8))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + public static boolean isNullOrWhiteSpace(String string) { + return string == null || "".equals(string) || "".equals(string.trim()); + } + + public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + // Calculate ratios of height and width to requested height and + // width + final int heightRatio = Math.round((float) height / (float) reqHeight); + final int widthRatio = Math.round((float) width / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will + // guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + + return inSampleSize; + } + + public static int getScaledHeight(double height, double width, int newWidth) { + // Try to keep correct aspect ratio of the original image, do not force a square + double aspectRatio = height / width; + + // Assume the size given refers to the width of the image, so calculate the new height using + // the previously determined aspect ratio + return (int) Math.round(newWidth * aspectRatio); + } + + public static int getScaledHeight(Bitmap bitmap, int width) { + return Util.getScaledHeight((double) bitmap.getHeight(), (double) bitmap.getWidth(), width); + } + + public static int getStringDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + if(t.toString().toLowerCase().indexOf(s.toString().toLowerCase()) != -1) { + return 1; + } + + int n = s.length(); + int m = t.length(); + + if (n == 0) { + return m; + } else if (m == 0) { + return n; + } + + if (n > m) { + final CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n + 1]; + int d[] = new int[n + 1]; + int _d[]; + + int i; + int j; + char t_j; + int cost; + + for (i = 0; i <= n; i++) { + p[i] = i; + } + + for (j = 1; j <= m; j++) { + t_j = t.charAt(j - 1); + d[0] = j; + + for (i = 1; i <= n; i++) { + cost = s.charAt(i - 1) == t_j ? 0 : 1; + d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost); + } + + _d = p; + p = d; + d = _d; + } + + return p[n]; + } + + public static boolean isNetworkConnected(Context context) { + return isNetworkConnected(context, false); + } + public static boolean isNetworkConnected(Context context, boolean streaming) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + boolean connected = networkInfo != null && networkInfo.isConnected(); + + if(streaming) { + boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + boolean wifiRequired = isWifiRequiredForDownload(context); + + return connected && (!wifiRequired || wifiConnected); + } else { + return connected; + } + } + public static boolean isWifiConnected(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + boolean connected = networkInfo != null && networkInfo.isConnected(); + return connected && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI); + } + public static String getSSID(Context context) { + if (isWifiConnected(context)) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + if (wifiManager.getConnectionInfo() != null && wifiManager.getConnectionInfo().getSSID() != null) { + return wifiManager.getConnectionInfo().getSSID().replace("\"", ""); + } + return null; + } + return null; + } + + public static boolean isExternalStoragePresent() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + public static boolean isAllowedToDownload(Context context) { + return isNetworkConnected(context, true) && !isOffline(context); + } + public static boolean isWifiRequiredForDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false); + } + + public static void info(Context context, int titleId, int messageId) { + info(context, titleId, messageId, true); + } + public static void info(Context context, int titleId, String message) { + info(context, titleId, message, true); + } + public static void info(Context context, String title, String message) { + info(context, title, message, true); + } + public static void info(Context context, int titleId, int messageId, boolean linkify) { + showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId, linkify); + } + public static void info(Context context, int titleId, String message, boolean linkify) { + showDialog(context, android.R.drawable.ic_dialog_info, titleId, message, linkify); + } + public static void info(Context context, String title, String message, boolean linkify) { + showDialog(context, android.R.drawable.ic_dialog_info, title, message, linkify); + } + + public static void showDialog(Context context, int icon, int titleId, int messageId) { + showDialog(context, icon, titleId, messageId, true); + } + public static void showDialog(Context context, int icon, int titleId, String message) { + showDialog(context, icon, titleId, message, true); + } + public static void showDialog(Context context, int icon, String title, String message) { + showDialog(context, icon, title, message, true); + } + public static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) { + showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId), linkify); + } + public static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) { + showDialog(context, icon, context.getResources().getString(titleId), message, linkify); + } + public static void showDialog(Context context, int icon, String title, String message, boolean linkify) { + SpannableString ss = new SpannableString(message); + if(linkify) { + Linkify.addLinks(ss, Linkify.ALL); + } + + AlertDialog dialog = new AlertDialog.Builder(context) + .setIcon(icon) + .setTitle(title) + .setMessage(ss) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + + ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); + } + public static void showHTMLDialog(Context context, int title, int message) { + showHTMLDialog(context, title, context.getResources().getString(message)); + } + public static void showHTMLDialog(Context context, int title, String message) { + AlertDialog dialog = new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(title) + .setMessage(Html.fromHtml(message)) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + + ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); + } + + public static void showDetailsDialog(Context context, @StringRes int title, List headers, List details) { + List headerStrings = new ArrayList<>(); + for(@StringRes Integer res: headers) { + headerStrings.add(context.getResources().getString(res)); + } + showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); + } + public static void showDetailsDialog(Context context, String title, List headers, final List details) { + ListView listView = new ListView(context); + listView.setAdapter(new DetailsAdapter(context, R.layout.details_item, headers, details)); + listView.setDivider(null); + listView.setScrollbarFadingEnabled(false); + + // Let the user long-click on a row to copy its value to the clipboard + final Context contextRef = context; + listView.setOnItemLongClickListener(new ListView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int pos, long id) { + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + if(nameView == null || detailsView == null) { + return false; + } + + CharSequence name = nameView.getText(); + CharSequence value = detailsView.getText(); + + ClipboardManager clipboard = (ClipboardManager) contextRef.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(name, value); + clipboard.setPrimaryClip(clip); + + toast(contextRef, "Copied " + name + " to clipboard"); + + return true; + } + }); + + new AlertDialog.Builder(context) + // .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(title) + .setView(listView) + .setPositiveButton(R.string.common_close, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + } + + public static void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException x) { + Log.w(TAG, "Interrupted from sleep.", x); + } + } + + public static void startActivityWithoutTransition(Activity currentActivity, Class newActivitiy) { + startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy)); + } + + public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) { + currentActivity.startActivity(intent); + disablePendingTransition(currentActivity); + } + + public static void disablePendingTransition(Activity activity) { + + // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain + // compatibility with 1.5. + try { + Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class); + method.invoke(activity, 0, 0); + } catch (Throwable x) { + // Ignored + } + } + + public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) { + // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain + // compatibility with 1.5. + try { + Constructor constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class); + return constructor.newInstance(context.getResources(), bitmap); + } catch (Throwable x) { + return new BitmapDrawable(bitmap); + } + } + + public static void registerMediaButtonEventReceiver(Context context) { + + // Only do it if enabled in the settings. + SharedPreferences prefs = getPreferences(context); + boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true); + + if (enabled) { + + // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + } + + public static void unregisterMediaButtonEventReceiver(Context context) { + // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + + @TargetApi(8) + public static void requestAudioFocus(final Context context) { + if (Build.VERSION.SDK_INT >= 8 && focusListener == null) { + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.requestAudioFocus(focusListener = new OnAudioFocusChangeListener() { + public void onAudioFocusChange(int focusChange) { + DownloadService downloadService = (DownloadService)context; + if((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) { + if(downloadService.getPlayerState() == PlayerState.STARTED) { + Log.i(TAG, "Temporary loss of focus"); + SharedPreferences prefs = getPreferences(context); + int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")); + if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) { + lowerFocus = true; + downloadService.setVolume(0.1f); + } else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) { + pauseFocus = true; + downloadService.pause(true); + } + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if(pauseFocus) { + pauseFocus = false; + downloadService.start(); + } + if(lowerFocus) { + lowerFocus = false; + downloadService.setVolume(1.0f); + } + } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS) { + Log.i(TAG, "Permanently lost focus"); + focusListener = null; + downloadService.pause(); + audioManager.abandonAudioFocus(this); + } + } + }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + } + + public static void abandonAudioFocus(Context context) { + if(focusListener != null) { + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.abandonAudioFocus(focusListener); + focusListener = null; + } + } + + /** + *

Broadcasts the given song info as the new song being played.

+ */ + public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { + try { + Intent intent = new Intent(EVENT_META_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); + + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); + + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + avrcpIntent.putExtra("playing", true); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + avrcpIntent.putExtra("playing", false); + } + addTrackInfo(context, song, avrcpIntent); + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastNewTrackInfo", e); + } + } + + /** + *

Broadcasts the given player state as the one being set.

+ */ + public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) { + try { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + intent.putExtra("state", "stop"); + avrcpIntent.putExtra("playing", false); + break; + case PAUSED: + intent.putExtra("state", "pause"); + avrcpIntent.putExtra("playing", false); + break; + case PREPARED: + // Only send quick pause event for samsung devices, causes issues for others + if (Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + avrcpIntent.putExtra("playing", false); + } else { + return; // Don't broadcast anything + } + break; + case COMPLETED: + intent.putExtra("state", "complete"); + avrcpIntent.putExtra("playing", false); + break; + default: + return; // No need to broadcast. + } + addTrackInfo(context, song, avrcpIntent); + + if (state != PlayerState.PREPARED) { + context.sendBroadcast(intent); + } + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e); + } + } + + private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) { + if (song != null) { + DownloadService downloadService = (DownloadService)context; + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + + intent.putExtra("track", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); + intent.putExtra("ListSize", (long) downloadService.getSongs().size()); + intent.putExtra("id", (long) downloadService.getCurrentPlayingIndex() + 1); + intent.putExtra("duration", (long) downloadService.getPlayerDuration()); + intent.putExtra("position", (long) downloadService.getPlayerPosition()); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + intent.putExtra("package","github.nvllsvm.audinaut"); + } else { + intent.putExtra("track", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("ListSize", (long) 0); + intent.putExtra("id", (long) 0); + intent.putExtra("duration", (long) 0); + intent.putExtra("position", (long) 0); + intent.putExtra("coverart", ""); + intent.putExtra("package","github.nvllsvm.audinaut"); + } + } + + public static WifiManager.WifiLock createWifiLock(Context context, String tag) { + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + int lockType = WifiManager.WIFI_MODE_FULL; + if (Build.VERSION.SDK_INT >= 12) { + lockType = 3; + } + return wm.createWifiLock(lockType, tag); + } + + public static Random getRandom() { + if(random == null) { + random = new SecureRandom(); + } + + return random; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java new file mode 100644 index 0000000..cb06ced --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +package github.nvllsvm.audinaut.util.tags; + +import java.io.RandomAccessFile; +import java.io.IOException; +import java.util.HashMap; + + +public class Bastp { + + public Bastp() { + } + + public HashMap getTags(String fname) { + HashMap tags = new HashMap(); + try { + RandomAccessFile ra = new RandomAccessFile(fname, "r"); + tags = getTags(ra); + ra.close(); + } + catch(Exception e) { + /* we dont' care much: SOMETHING went wrong. d'oh! */ + } + + return tags; + } + + public HashMap getTags(RandomAccessFile s) { + HashMap tags = new HashMap(); + byte[] file_ff = new byte[4]; + + try { + s.read(file_ff); + String magic = new String(file_ff); + if(magic.equals("fLaC")) { + tags = (new FlacFile()).getTags(s); + } + else if(magic.equals("OggS")) { + tags = (new OggFile()).getTags(s); + } + else if(file_ff[0] == -1 && file_ff[1] == -5) { /* aka 0xfffb in real languages */ + tags = (new LameHeader()).getTags(s); + } + else if(magic.substring(0,3).equals("ID3")) { + tags = (new ID3v2File()).getTags(s); + if(tags.containsKey("_hdrlen")) { + Long hlen = Long.parseLong( tags.get("_hdrlen").toString(), 10 ); + HashMap lameInfo = (new LameHeader()).parseLameHeader(s, hlen); + /* add gain tags if not already present */ + inheritTag("REPLAYGAIN_TRACK_GAIN", lameInfo, tags); + inheritTag("REPLAYGAIN_ALBUM_GAIN", lameInfo, tags); + } + } + tags.put("_magic", magic); + } + catch (IOException e) { + } + return tags; + } + + private void inheritTag(String key, HashMap from, HashMap to) { + if(!to.containsKey(key) && from.containsKey(key)) { + to.put(key, from.get(key)); + } + } + +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java new file mode 100644 index 0000000..a76d824 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package github.nvllsvm.audinaut.util.tags; + +import android.support.v4.util.LruCache; +import java.util.HashMap; +import java.util.Vector; + +public final class BastpUtil { + private static final RGLruCache rgCache = new RGLruCache(16); + + /** Returns the ReplayGain values of 'path' as + */ + public static float[] getReplayGainValues(String path) { + float[] cached = rgCache.get(path); + + if(cached == null) { + cached = getReplayGainValuesFromFile(path); + rgCache.put(path, cached); + } + return cached; + } + + + + /** Parse given file and return track,album replay gain values + */ + private static float[] getReplayGainValuesFromFile(String path) { + String[] keys = { "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN" }; + float[] adjust= { 0f , 0f }; + HashMap tags = (new Bastp()).getTags(path); + + for (int i=0; i { + public RGLruCache(int size) { + super(size); + } + } + +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java new file mode 100644 index 0000000..e8d3e79 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +package github.nvllsvm.audinaut.util.tags; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Vector; + +public class Common { + private static final long MAX_PKT_SIZE = 524288; + + public void xdie(String reason) throws IOException { + throw new IOException(reason); + } + + /* + ** Returns a 32bit int from given byte offset in LE + */ + public int b2le32(byte[] b, int off) { + int r = 0; + for(int i=0; i<4; i++) { + r |= ( b2u(b[off+i]) << (8*i) ); + } + return r; + } + + public int b2be32(byte[] b, int off) { + return swap32(b2le32(b, off)); + } + + public int swap32(int i) { + return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff); + } + + /* + ** convert 'byte' value into unsigned int + */ + public int b2u(byte x) { + return (x & 0xFF); + } + + /* + ** Printout debug message to STDOUT + */ + public void debug(String s) { + System.out.println("DBUG "+s); + } + + public HashMap parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException { + HashMap tags = new HashMap(); + int comments = 0; // number of found comments + int xoff = 0; // offset within 'scratch' + int can_read = (int)(payload_len > MAX_PKT_SIZE ? MAX_PKT_SIZE : payload_len); + byte[] scratch = new byte[can_read]; + + // seek to given position and slurp in the payload + s.seek(offset); + s.read(scratch); + + // skip vendor string in format: [LEN][VENDOR_STRING] + xoff += 4 + b2le32(scratch, xoff); // 4 = LEN = 32bit int + comments = b2le32(scratch, xoff); + xoff += 4; + + // debug("comments count = "+comments); + for(int i=0; i scratch.length) + xdie("string out of bounds"); + + String tag_raw = new String(scratch, xoff-clen, clen); + String[] tag_vec = tag_raw.split("=",2); + String tag_key = tag_vec[0].toUpperCase(); + + addTagEntry(tags, tag_key, tag_vec[1]); + } + return tags; + } + + public void addTagEntry(HashMap tags, String key, String value) { + if(tags.containsKey(key)) { + ((Vector)tags.get(key)).add(value); // just add to existing vector + } + else { + Vector vx = new Vector(); + vx.add(value); + tags.put(key, vx); + } + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java new file mode 100644 index 0000000..a3e2341 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package github.nvllsvm.audinaut.util.tags; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Enumeration; + + +public class FlacFile extends Common { + private static final int FLAC_TYPE_COMMENT = 4; // ID of 'VorbisComment's + + public FlacFile() { + } + + public HashMap getTags(RandomAccessFile s) throws IOException { + int xoff = 4; // skip file magic + int retry = 64; + int r[]; + HashMap tags = new HashMap(); + + for(; retry > 0; retry--) { + r = parse_metadata_block(s, xoff); + + if(r[2] == FLAC_TYPE_COMMENT) { + tags = parse_vorbis_comment(s, xoff+r[0], r[1]); + break; + } + + if(r[3] != 0) + break; // eof reached + + // else: calculate next offset + xoff += r[0] + r[1]; + } + return tags; + } + + /* Parses the metadata block at 'offset' and returns + ** [header_size, payload_size, type, stop_after] + */ + private int[] parse_metadata_block(RandomAccessFile s, long offset) throws IOException { + int[] result = new int[4]; + byte[] mb_head = new byte[4]; + int stop_after = 0; + int block_type = 0; + int block_size = 0; + + s.seek(offset); + + if( s.read(mb_head) != 4 ) + xdie("failed to read metadata block header"); + + block_size = b2be32(mb_head,0); // read whole header as 32 big endian + block_type = (block_size >> 24) & 127; // BIT 1-7 are the type + stop_after = (((block_size >> 24) & 128) > 0 ? 1 : 0 ); // BIT 0 indicates the last-block flag + block_size = (block_size & 0x00FFFFFF); // byte 1-7 are the size + + // debug("size="+block_size+", type="+block_type+", is_last="+stop_after); + + result[0] = 4; // hardcoded - only returned to be consistent with OGG parser + result[1] = block_size; + result[2] = block_type; + result[3] = stop_after; + + return result; + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java new file mode 100644 index 0000000..1a77d37 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package github.nvllsvm.audinaut.util.tags; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; + + +public class ID3v2File extends Common { + private static int ID3_ENC_LATIN = 0x00; + private static int ID3_ENC_UTF16LE = 0x01; + private static int ID3_ENC_UTF16BE = 0x02; + private static int ID3_ENC_UTF8 = 0x03; + + public ID3v2File() { + } + + public HashMap getTags(RandomAccessFile s) throws IOException { + HashMap tags = new HashMap(); + + final int v2hdr_len = 10; + byte[] v2hdr = new byte[v2hdr_len]; + + // read the whole 10 byte header into memory + s.seek(0); + s.read(v2hdr); + + int id3v = ((b2be32(v2hdr,0))) & 0xFF; // swapped ID3\04 -> ver. ist the first byte + int v3len = ((b2be32(v2hdr,6))); // total size EXCLUDING the this 10 byte header + v3len = ((v3len & 0x7f000000) >> 3) | // for some funky reason, this is encoded as 7*4 bits + ((v3len & 0x007f0000) >> 2) | + ((v3len & 0x00007f00) >> 1) | + ((v3len & 0x0000007f) >> 0) ; + + // debug(">> tag version ID3v2."+id3v); + // debug(">> LEN= "+v3len+" // "+v3len); + + // we should already be at the first frame + // so we can start the parsing right now + tags = parse_v3_frames(s, v3len); + tags.put("_hdrlen", v3len+v2hdr_len); + return tags; + } + + /* Parses all ID3v2 frames at the current position up until payload_len + ** bytes were read + */ + public HashMap parse_v3_frames(RandomAccessFile s, long payload_len) throws IOException { + HashMap tags = new HashMap(); + byte[] frame = new byte[10]; // a frame header is always 10 bytes + long bread = 0; // total amount of read bytes + + while(bread < payload_len) { + bread += s.read(frame); + String framename = new String(frame, 0, 4); + int slen = b2be32(frame, 4); + + /* Abort on silly sizes */ + if(slen < 1 || slen > 524288) + break; + + byte[] xpl = new byte[slen]; + bread += s.read(xpl); + + if(framename.substring(0,1).equals("T")) { + String[] nmzInfo = normalizeTaginfo(framename, xpl); + + for(int i = 0; i < nmzInfo.length; i += 2) { + String oggKey = nmzInfo[i]; + String decPld = nmzInfo[i + 1]; + + if (oggKey.length() > 0 && !tags.containsKey(oggKey)) { + addTagEntry(tags, oggKey, decPld); + } + } + } + else if(framename.equals("RVA2")) { + // + } + + } + return tags; + } + + /* Converts ID3v2 sillyframes to OggNames */ + private String[] normalizeTaginfo(String k, byte[] v) { + String[] rv = new String[] {"",""}; + HashMap lu = new HashMap(); + lu.put("TIT2", "TITLE"); + lu.put("TALB", "ALBUM"); + lu.put("TPE1", "ARTIST"); + + if(lu.containsKey(k)) { + /* A normal, known key: translate into Ogg-Frame name */ + rv[0] = (String)lu.get(k); + rv[1] = getDecodedString(v); + } + else if(k.equals("TXXX")) { + /* A freestyle field, ieks! */ + String txData[] = getDecodedString(v).split(Character.toString('\0'), 2); + /* Check if we got replaygain info in key\0value style */ + if(txData.length == 2) { + if(txData[0].matches("^(?i)REPLAYGAIN_(ALBUM|TRACK)_GAIN$")) { + rv[0] = txData[0].toUpperCase(); /* some tagwriters use lowercase for this */ + rv[1] = txData[1]; + } else { + // Check for replaygain tags just thrown randomly in field + int nextStartIndex = 1; + int startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_"); + ArrayList parts = new ArrayList(); + while(startName != -1) { + int endName = txData[1].indexOf((char) 0, startName); + if(endName != -1) { + parts.add(txData[1].substring(startName, endName).toUpperCase()); + int endValue = txData[1].indexOf((char) 0, endName + 1); + if(endValue != -1) { + parts.add(txData[1].substring(endName + 1, endValue)); + nextStartIndex = endValue + 1; + } else { + break; + } + } else { + break; + } + + startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex); + } + + if(parts.size() > 0) { + rv = new String[parts.size()]; + rv = parts.toArray(rv); + } + } + } + } + + return rv; + } + + /* Converts a raw byte-stream text into a java String */ + private String getDecodedString(byte[] raw) { + int encid = raw[0] & 0xFF; + int len = raw.length; + String v = ""; + try { + if(encid == ID3_ENC_LATIN) { + v = new String(raw, 1, len-1, "ISO-8859-1"); + } + else if (encid == ID3_ENC_UTF8) { + v = new String(raw, 1, len-1, "UTF-8"); + } + else if (encid == ID3_ENC_UTF16LE) { + v = new String(raw, 3, len-3, "UTF-16LE"); + } + else if (encid == ID3_ENC_UTF16BE) { + v = new String(raw, 3, len-3, "UTF-16BE"); + } + } catch(Exception e) {} + return v; + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java new file mode 100644 index 0000000..340e7e4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package github.nvllsvm.audinaut.util.tags; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Enumeration; + + +public class LameHeader extends Common { + + public LameHeader() { + } + + public HashMap getTags(RandomAccessFile s) throws IOException { + return parseLameHeader(s, 0); + } + + public HashMap parseLameHeader(RandomAccessFile s, long offset) throws IOException { + HashMap tags = new HashMap(); + byte[] chunk = new byte[4]; + + s.seek(offset + 0x24); + s.read(chunk); + + String lameMark = new String(chunk, 0, chunk.length, "ISO-8859-1"); + + if(lameMark.equals("Info") || lameMark.equals("Xing")) { + s.seek(offset+0xAB); + s.read(chunk); + + int raw = b2be32(chunk, 0); + int gtrk_raw = raw >> 16; /* first 16 bits are the raw track gain value */ + int galb_raw = raw & 0xFFFF; /* the rest is for the album gain value */ + + float gtrk_val = (float)(gtrk_raw & 0x01FF)/10; + float galb_val = (float)(galb_raw & 0x01FF)/10; + + gtrk_val = ((gtrk_raw&0x0200)!=0 ? -1*gtrk_val : gtrk_val); + galb_val = ((galb_raw&0x0200)!=0 ? -1*galb_val : galb_val); + + if( (gtrk_raw&0xE000) == 0x2000 ) { + addTagEntry(tags, "REPLAYGAIN_TRACK_GAIN", gtrk_val+" dB"); + } + if( (gtrk_raw&0xE000) == 0x4000 ) { + addTagEntry(tags, "REPLAYGAIN_ALBUM_GAIN", galb_val+" dB"); + } + + } + + return tags; + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java new file mode 100644 index 0000000..176af00 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package github.nvllsvm.audinaut.util.tags; + + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; + + +public class OggFile extends Common { + + private static final int OGG_PAGE_SIZE = 27; // Static size of an OGG Page + private static final int OGG_TYPE_COMMENT = 3; // ID of 'VorbisComment's + + public OggFile() { + } + + public HashMap getTags(RandomAccessFile s) throws IOException { + long offset = 0; + int retry = 64; + HashMap tags = new HashMap(); + + for( ; retry > 0 ; retry-- ) { + long res[] = parse_ogg_page(s, offset); + if(res[2] == OGG_TYPE_COMMENT) { + tags = parse_ogg_vorbis_comment(s, offset+res[0], res[1]); + break; + } + offset += res[0] + res[1]; + } + return tags; + } + + + /* Parses the ogg page at offset 'offset' and returns + ** [header_size, payload_size, type] + */ + private long[] parse_ogg_page(RandomAccessFile s, long offset) throws IOException { + long[] result = new long[3]; // [header_size, payload_size] + byte[] p_header = new byte[OGG_PAGE_SIZE]; // buffer for the page header + byte[] scratch; + int bread = 0; // number of bytes read + int psize = 0; // payload-size + int nsegs = 0; // Number of segments + + s.seek(offset); + bread = s.read(p_header); + if(bread != OGG_PAGE_SIZE) + xdie("Unable to read() OGG_PAGE_HEADER"); + if((new String(p_header, 0, 5)).equals("OggS\0") != true) + xdie("Invalid magic - not an ogg file?"); + + nsegs = b2u(p_header[26]); + // debug("> file seg: "+nsegs); + if(nsegs > 0) { + scratch = new byte[nsegs]; + bread = s.read(scratch); + if(bread != nsegs) + xdie("Failed to read segtable"); + + for(int i=0; i pre-read */ + if(psize >= 1 && s.read(p_header, 0, 1) == 1) { + result[2] = b2u(p_header[0]); + } + + return result; + } + + /* In 'vorbiscomment' field is prefixed with \3vorbis in OGG files + ** we check that this marker is present and call the generic comment + ** parset with the correct offset (+7) */ + private HashMap parse_ogg_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException { + final int pfx_len = 7; + byte[] pfx = new byte[pfx_len]; + + if(pl_len < pfx_len) + xdie("ogg vorbis comment field is too short!"); + + s.seek(offset); + s.read(pfx); + + if( (new String(pfx, 0, pfx_len)).equals("\3vorbis") == false ) + xdie("Damaged packet found!"); + + return parse_vorbis_comment(s, offset+pfx_len, pl_len-pfx_len); + } + +}; diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java new file mode 100644 index 0000000..02172d0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java @@ -0,0 +1,130 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import java.util.ArrayList; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +public class AlbumListCountView extends UpdateView2 { + private final String TAG = AlbumListCountView.class.getSimpleName(); + + private TextView titleView; + private TextView countView; + private int startCount; + private int count = 0; + + public AlbumListCountView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_count_item, this, true); + + titleView = (TextView) findViewById(R.id.basic_count_name); + countView = (TextView) findViewById(R.id.basic_count_count); + } + + protected void setObjectImpl(Integer albumListString, Void dummy) { + titleView.setText(albumListString); + + SharedPreferences prefs = Util.getPreferences(context); + startCount = prefs.getInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0); + count = startCount; + update(); + } + + @Override + protected void updateBackground() { + try { + String recentAddedFile = Util.getCacheName(context, "recent_count"); + ArrayList recents = FileUtil.deserialize(context, recentAddedFile, ArrayList.class); + if (recents == null) { + recents = new ArrayList(); + } + + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory recentlyAdded = musicService.getAlbumList("newest", 20, 0, false, context, null); + + // If first run, just put everything in it and return 0 + boolean firstRun = recents.isEmpty(); + + // Count how many new albums are in the list + count = 0; + for (MusicDirectory.Entry album : recentlyAdded.getChildren()) { + if (!recents.contains(album.getId())) { + recents.add(album.getId()); + count++; + } + } + + // Keep recents list from growing infinitely + while (recents.size() > 40) { + recents.remove(0); + } + FileUtil.serialize(context, recents, recentAddedFile); + + if (!firstRun) { + // Add the old count which will get cleared out after viewing recents + count += startCount; + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), count); + editor.commit(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to refresh most recent count", e); + } + } + + @Override + protected void update() { + // Update count display with appropriate information + if(count <= 0) { + countView.setVisibility(View.GONE); + } else { + String displayName; + if(count < 10) { + displayName = "0" + count; + } else { + displayName = "" + count; + } + + countView.setText(displayName); + countView.setVisibility(View.VISIBLE); + } + } + + @Override + public void onClick() { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0); + editor.commit(); + + count = 0; + update(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java new file mode 100644 index 0000000..9c120a2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java @@ -0,0 +1,116 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; + +import java.io.File; + +public class AlbumView extends UpdateView2 { + private static final String TAG = AlbumView.class.getSimpleName(); + + private File file; + private TextView titleView; + private TextView artistView; + private boolean showArtist = true; + private String coverArtId; + + public AlbumView(Context context, boolean cell) { + super(context); + + if(cell) { + LayoutInflater.from(context).inflate(R.layout.album_cell_item, this, true); + } else { + LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); + } + + coverArtView = findViewById(R.id.album_coverart); + titleView = (TextView) findViewById(R.id.album_title); + artistView = (TextView) findViewById(R.id.album_artist); + + moreButton = (ImageView) findViewById(R.id.item_more); + + checkable = true; + } + + public void setShowArtist(boolean showArtist) { + this.showArtist = showArtist; + } + + protected void setObjectImpl(MusicDirectory.Entry album, ImageLoader imageLoader) { + titleView.setText(album.getAlbumDisplay()); + String artist = ""; + if(showArtist) { + artist = album.getArtist(); + if (artist == null) { + artist = ""; + } + if (album.getYear() != null) { + artist += " - " + album.getYear(); + } + } else if(album.getYear() != null) { + artist += album.getYear(); + } + artistView.setText(album.getArtist() == null ? "" : artist); + onUpdateImageView(); + file = null; + } + + public void onUpdateImageView() { + imageTask = item2.loadImage(coverArtView, item, false, true); + coverArtId = item.getCoverArt(); + } + + @Override + protected void updateBackground() { + if(file == null) { + file = FileUtil.getAlbumDirectory(context, item); + } + + exists = file.exists(); + } + + @Override + public void update() { + super.update(); + + if(!Util.equals(item.getCoverArt(), coverArtId)) { + onUpdateImageView(); + } + } + + public MusicDirectory.Entry getEntry() { + return item; + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java new file mode 100644 index 0000000..7b34f05 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java @@ -0,0 +1,69 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; + +import java.io.File; +/** + * Used to display albums in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class ArtistEntryView extends UpdateView { + private static final String TAG = ArtistEntryView.class.getSimpleName(); + + private File file; + private TextView titleView; + + public ArtistEntryView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(MusicDirectory.Entry artist) { + titleView.setText(artist.getTitle()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java new file mode 100644 index 0000000..afb242c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java @@ -0,0 +1,70 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.util.FileUtil; + +import java.io.File; + +/** + * Used to display albums in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class ArtistView extends UpdateView { + private static final String TAG = ArtistView.class.getSimpleName(); + + private File file; + private TextView titleView; + + public ArtistView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Artist artist) { + titleView.setText(artist.getName()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java b/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java new file mode 100644 index 0000000..2442393 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java @@ -0,0 +1,86 @@ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; + +public class AutoRepeatButton extends ImageButton { + + private static final long initialRepeatDelay = 1000; + private static final long repeatIntervalInMilliseconds = 300; + private boolean doClick = true; + private Runnable repeatEvent = null; + + private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() { + @Override + public void run() { + doClick = false; + //Perform the present repetition of the click action provided by the user + // in setOnClickListener(). + if(repeatEvent != null) + repeatEvent.run(); + + //Schedule the next repetitions of the click action, using a faster repeat + // interval than the initial repeat delay interval. + postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds); + } + }; + + private void commonConstructorCode() { + this.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + int action = event.getAction(); + if(action == MotionEvent.ACTION_DOWN) + { + doClick = true; + //Just to be sure that we removed all callbacks, + // which should have occurred in the ACTION_UP + removeCallbacks(repeatClickWhileButtonHeldRunnable); + + //Schedule the start of repetitions after a one half second delay. + postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay); + + setPressed(true); + } + else if(action == MotionEvent.ACTION_UP) { + //Cancel any repetition in progress. + removeCallbacks(repeatClickWhileButtonHeldRunnable); + + if(doClick || repeatEvent == null) { + performClick(); + } + + setPressed(false); + } + + //Returning true here prevents performClick() from getting called + // in the usual manner, which would be redundant, given that we are + // already calling it above. + return true; + } + }); + } + + public void setOnRepeatListener(Runnable runnable) { + repeatEvent = runnable; + } + + public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + commonConstructorCode(); + } + + + public AutoRepeatButton(Context context, AttributeSet attrs) { + super(context, attrs); + commonConstructorCode(); + } + + public AutoRepeatButton(Context context) { + super(context); + commonConstructorCode(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java b/app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java new file mode 100644 index 0000000..a104a53 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java @@ -0,0 +1,40 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; + +public class BasicHeaderView extends UpdateView { + TextView nameView; + + public BasicHeaderView(Context context) { + this(context, R.layout.basic_header); + } + public BasicHeaderView(Context context, int layout) { + super(context, false); + + LayoutInflater.from(context).inflate(layout, this, true); + nameView = (TextView) findViewById(R.id.item_name); + } + + protected void setObjectImpl(String string) { + nameView.setText(string); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java b/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java new file mode 100644 index 0000000..84221e6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java @@ -0,0 +1,42 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; + +public class BasicListView extends UpdateView { + private TextView titleView; + + public BasicListView(Context context) { + super(context, false); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setVisibility(View.GONE); + } + + protected void setObjectImpl(String string) { + titleView.setText(string); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java b/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java new file mode 100644 index 0000000..52e47e4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java @@ -0,0 +1,146 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.preference.DialogPreference; +import android.preference.EditTextPreference; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.File; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.FileUtil; + +public class CacheLocationPreference extends EditTextPreference { + private static final String TAG = CacheLocationPreference.class.getSimpleName(); + private Context context; + + public CacheLocationPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + public CacheLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + public CacheLocationPreference(Context context) { + super(context); + this.context = context; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setLayoutParams(new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + ViewGroup vg = (ViewGroup) editText.getParent(); + + LinearLayout cacheButtonsWrapper = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.cache_location_buttons, vg, true); + Button internalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_internal); + Button externalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_external); + + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + // Past 5.0 we can query directly for SD Card + File internalDir = null, externalDir = null; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null) { + if(Environment.isExternalStorageRemovable(dirs[i])) { + if(externalDir != null) { + externalDir = dirs[i]; + } + } else { + internalDir = dirs[i]; + } + + if(internalDir != null && externalDir != null) { + break; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + if(externalDir == null) { + for (int i = dirs.length - 1; i >= 0; i--) { + if (dirs[i] != null) { + externalDir = dirs[i]; + break; + } + } + } + if(internalDir == null) { + for (int i = 0; i < dirs.length; i++) { + if (dirs[i] != null) { + internalDir = dirs[i]; + break; + } + } + } + final File finalInternalDir = new File(internalDir, "music"); + final File finalExternalDir = new File(externalDir, "music"); + + final EditText editTextBox = (EditText)view.findViewById(android.R.id.edit); + if(finalInternalDir != null && (finalInternalDir.exists() || finalInternalDir.mkdirs())) { + internalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalInternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + internalLocation.setEnabled(false); + } + + if(finalExternalDir != null && !finalInternalDir.equals(finalExternalDir) && (finalExternalDir.exists() || finalExternalDir.mkdirs())) { + externalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalExternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + externalLocation.setEnabled(false); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java b/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java new file mode 100644 index 0000000..20cd126 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java @@ -0,0 +1,67 @@ +package github.nvllsvm.audinaut.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; + +public class CardView extends FrameLayout{ + private static final String TAG = CardView.class.getSimpleName(); + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + @Override + public void onDraw(Canvas canvas) { + try { + Path clipPath = new Path(); + float roundedDp = getResources().getDimension(R.dimen.Card_Radius); + clipPath.addRoundRect(new RectF(canvas.getClipBounds()), roundedDp, roundedDp, Path.Direction.CW); + canvas.clipPath(clipPath); + } catch(Exception e) { + Log.e(TAG, "Failed to clip path on canvas", e); + } + super.onDraw(canvas); + } + + private void init(Context context) { + setClipChildren(true); + setBackgroundResource(DrawableTint.getDrawableRes(context, R.attr.cardBackgroundDrawable)); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(getResources().getInteger(R.integer.Card_Elevation)); + } + + // clipPath is not supported with Hardware Acceleration before API 18 + // http://stackoverflow.com/questions/8895677/work-around-canvas-clippath-that-is-not-supported-in-android-any-more/8895894#8895894 + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && isHardwareAccelerated()) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java b/app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java new file mode 100644 index 0000000..96931d7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java @@ -0,0 +1,75 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class ErrorDialog { + + public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) { + this(activity, activity.getResources().getString(messageId), finishActivityOnCancel); + } + + public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) { + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setTitle(R.string.error_label); + builder.setMessage(message); + builder.setCancelable(true); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + if (finishActivityOnClose) { + restart(activity); + } + } + }); + builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (finishActivityOnClose) { + restart(activity); + } + } + }); + + try { + builder.create().show(); + } catch(Exception e) { + // Don't care, just means no activity to attach to + } + } + + private void restart(Activity activity) { + Intent intent = new Intent(activity, SubsonicFragmentActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(activity, intent); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/FadeOutAnimation.java b/app/src/main/java/github/nvllsvm/audinaut/view/FadeOutAnimation.java new file mode 100644 index 0000000..6f9f507 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/FadeOutAnimation.java @@ -0,0 +1,77 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; + +/** + * Fades a view out by changing its alpha value. + * + * @author Sindre Mehus + * @version $Id: Util.java 3203 2012-10-04 09:12:08Z sindre_mehus $ + */ +public class FadeOutAnimation extends AlphaAnimation { + + private boolean cancelled; + + /** + * Creates and starts the fade out animation. + * + * @param view The view to fade out (or display). + * @param fadeOut If true, the view is faded out. Otherwise it is immediately made visible. + * @param durationMillis Fade duration. + */ + public static void createAndStart(View view, boolean fadeOut, long durationMillis) { + if (fadeOut) { + view.clearAnimation(); + view.startAnimation(new FadeOutAnimation(view, durationMillis)); + } else { + Animation animation = view.getAnimation(); + if (animation instanceof FadeOutAnimation) { + ((FadeOutAnimation) animation).cancelFadeOut(); + } + view.clearAnimation(); + view.setVisibility(View.VISIBLE); + } + } + + FadeOutAnimation(final View view, long durationMillis) { + super(1.0F, 0.0F); + setDuration(durationMillis); + setAnimationListener(new AnimationListener() { + public void onAnimationStart(Animation animation) { + } + + public void onAnimationRepeat(Animation animation) { + } + + public void onAnimationEnd(Animation animation) { + if (!cancelled) { + view.setVisibility(View.INVISIBLE); + } + } + }); + } + + private void cancelFadeOut() { + cancelled = true; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java b/app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java new file mode 100644 index 0000000..115e435 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java @@ -0,0 +1,335 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.AdapterDataObserver; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; + +import static android.support.v7.widget.RecyclerView.OnScrollListener; + +public class FastScroller extends LinearLayout { + private static final String TAG = FastScroller.class.getSimpleName(); + private static final int BUBBLE_ANIMATION_DURATION = 100; + private static final int TRACK_SNAP_RANGE = 5; + + private TextView bubble; + private View handle; + private RecyclerView recyclerView; + private final ScrollListener scrollListener = new ScrollListener(); + private int height; + private int visibleRange = -1; + private RecyclerView.Adapter adapter; + private AdapterDataObserver adapterObserver; + private boolean visibleBubble = true; + private boolean hasScrolled = false; + + private ObjectAnimator currentAnimator = null; + + public FastScroller(final Context context,final AttributeSet attrs,final int defStyleAttr) { + super(context,attrs,defStyleAttr); + initialise(context); + } + + public FastScroller(final Context context) { + super(context); + initialise(context); + } + + public FastScroller(final Context context,final AttributeSet attrs) { + super(context, attrs); + initialise(context); + } + + private void initialise(Context context) { + setOrientation(HORIZONTAL); + setClipChildren(false); + LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.fast_scroller,this,true); + bubble = (TextView)findViewById(R.id.fastscroller_bubble); + handle = findViewById(R.id.fastscroller_handle); + bubble.setVisibility(INVISIBLE); + setVisibility(GONE); + } + + @Override + protected void onSizeChanged(int w,int h,int oldw,int oldh) { + super.onSizeChanged(w,h,oldw,oldh); + height = h; + visibleRange = -1; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + final int action = event.getAction(); + switch(action) + { + case MotionEvent.ACTION_DOWN: + if(event.getX() < (handle.getX() - 30)) { + return false; + } + + if(currentAnimator != null) + currentAnimator.cancel(); + if(bubble.getVisibility() == INVISIBLE) { + if(visibleBubble) { + showBubble(); + } + } else if(!visibleBubble) { + hideBubble(); + } + handle.setSelected(true); + case MotionEvent.ACTION_MOVE: + setRecyclerViewPosition(event.getY()); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + handle.setSelected(false); + hideBubble(); + return true; + } + return super.onTouchEvent(event); + } + + public void attachRecyclerView(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + recyclerView.addOnScrollListener(scrollListener); + registerAdapter(); + visibleRange = -1; + } + public void detachRecyclerView() { + recyclerView.removeOnScrollListener(scrollListener); + recyclerView.setVerticalScrollBarEnabled(true); + unregisterAdapter(); + recyclerView = null; + setVisibility(View.GONE); + } + public boolean isAttached() { + return recyclerView != null; + } + + private void setRecyclerViewPosition(float y) { + if(recyclerView != null) { + if(recyclerView.getChildCount() == 0) { + return; + } + + int itemCount = recyclerView.getAdapter().getItemCount(); + float proportion = getValueInRange(0, 1f, y / (float) height); + + float targetPosFloat = getValueInRange(0, itemCount - 1, proportion * (float)itemCount); + int targetPos = (int) targetPosFloat; + + // Immediately make sure that the target is visible + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + // layoutManager.scrollToPositionWithOffset(targetPos, 0); + View firstVisibleView = recyclerView.getChildAt(0); + + // Calculate how far through this position we are + int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth()); + int firstVisiblePosition = recyclerView.getChildPosition(firstVisibleView); + int remainder = (targetPos - firstVisiblePosition) % columns; + float offsetPercentage = (targetPosFloat - targetPos + remainder) / columns; + if(offsetPercentage < 0) { + offsetPercentage = 1 + offsetPercentage; + } + int firstVisibleHeight = firstVisibleView.getHeight(); + if(columns > 1) { + firstVisibleHeight += (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, GridSpacingDecoration.SPACING, firstVisibleView.getResources().getDisplayMetrics()); + } + int offset = (int) (offsetPercentage * firstVisibleHeight); + + layoutManager.scrollToPositionWithOffset(targetPos, -offset); + onUpdateScroll(1, 1); + + try { + String bubbleText = null; + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if(adapter instanceof BubbleTextGetter) { + bubbleText = ((BubbleTextGetter) adapter).getTextToShowInBubble(targetPos); + } + + if(bubbleText == null) { + visibleBubble = false; + bubble.setVisibility(View.INVISIBLE); + } else { + bubble.setText(bubbleText); + bubble.setVisibility(View.VISIBLE); + visibleBubble = true; + } + } catch(Exception e) { + Log.e(TAG, "Error getting text for bubble", e); + } + } + } + + private float getValueInRange(float min, float max, float value) { + float minimum = Math.max(min, value); + return Math.min(minimum,max); + } + + private void setBubbleAndHandlePosition(float y) { + int bubbleHeight = bubble.getHeight(); + int handleHeight = handle.getHeight(); + handle.setY(getValueInRange(0,height-handleHeight,(int)(y-handleHeight/2))); + bubble.setY(getValueInRange(0, height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight))); + } + + private void showBubble() { + bubble.setVisibility(VISIBLE); + if(currentAnimator != null) + currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble,"alpha",0f,1f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.start(); + } + + private void hideBubble() { + if(currentAnimator != null) + currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble,"alpha",1f,0f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + }); + currentAnimator.start(); + } + + private void registerAdapter() { + RecyclerView.Adapter newAdapter = recyclerView.getAdapter(); + if(newAdapter != adapter) { + unregisterAdapter(); + } + + if(newAdapter != null) { + adapterObserver = new AdapterDataObserver() { + @Override + public void onChanged() { + visibleRange = -1; + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + visibleRange = -1; + } + }; + newAdapter.registerAdapterDataObserver(adapterObserver); + adapter = newAdapter; + } + } + private void unregisterAdapter() { + if(adapter != null) { + adapter.unregisterAdapterDataObserver(adapterObserver); + adapter = null; + adapterObserver = null; + } + } + + private class ScrollListener extends OnScrollListener { + @Override + public void onScrolled(RecyclerView rv,int dx,int dy) { + onUpdateScroll(dx, dy); + } + } + + private void onUpdateScroll(int dx, int dy) { + if(recyclerView.getWidth() == 0) { + return; + } + registerAdapter(); + + View firstVisibleView = recyclerView.getChildAt(0); + if(firstVisibleView == null) { + return; + } + int firstVisiblePosition = recyclerView.getChildPosition(firstVisibleView); + + int itemCount = recyclerView.getAdapter().getItemCount(); + int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth()); + if(visibleRange == -1) { + visibleRange = recyclerView.getChildCount(); + } + + // Add the percentage of the item the user has scrolled past already + float pastFirst = -firstVisibleView.getY() / firstVisibleView.getHeight() * columns; + float position = firstVisiblePosition + pastFirst; + + // Scale this so as we move down the visible range gets added to position from 0 -> visible range + float scaledVisibleRange = position / (float) (itemCount - visibleRange) * visibleRange; + position += scaledVisibleRange; + + float proportion = position / itemCount; + setBubbleAndHandlePosition(height * proportion); + + if((visibleRange * 2) < itemCount) { + if (!hasScrolled && (dx > 0 || dy > 0)) { + setVisibility(View.VISIBLE); + hasScrolled = true; + recyclerView.setVerticalScrollBarEnabled(false); + } + } else if(hasScrolled) { + setVisibility(View.GONE); + hasScrolled = false; + recyclerView.setVerticalScrollBarEnabled(true); + } + } + + public interface BubbleTextGetter { + String getTextToShowInBubble(int position); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java b/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java new file mode 100644 index 0000000..c76b260 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java @@ -0,0 +1,57 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Genre; + +public class GenreView extends UpdateView { + private static final String TAG = GenreView.class.getSimpleName(); + + private TextView titleView; + private TextView songsView; + private TextView albumsView; + + public GenreView(Context context) { + super(context, false); + LayoutInflater.from(context).inflate(R.layout.genre_list_item, this, true); + + titleView = (TextView) findViewById(R.id.genre_name); + songsView = (TextView) findViewById(R.id.genre_songs); + albumsView = (TextView) findViewById(R.id.genre_albums); + } + + public void setObjectImpl(Genre genre) { + titleView.setText(genre.getName()); + + if(genre.getAlbumCount() != null) { + songsView.setVisibility(View.VISIBLE); + albumsView.setVisibility(View.VISIBLE); + songsView.setText(context.getResources().getString(R.string.select_genre_songs, genre.getSongCount())); + albumsView.setText(context.getResources().getString(R.string.select_genre_albums, genre.getAlbumCount())); + } else { + songsView.setVisibility(View.GONE); + albumsView.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java b/app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java new file mode 100644 index 0000000..0713760 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java @@ -0,0 +1,133 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.graphics.Rect; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import static android.widget.LinearLayout.*; + +public class GridSpacingDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = GridSpacingDecoration.class.getSimpleName(); + public static final int SPACING = 10; + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + + int spacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SPACING, view.getResources().getDisplayMetrics()); + int halfSpacing = spacing / 2; + + int childCount = parent.getChildCount(); + int childIndex = parent.getChildPosition(view); + // Not an actual child (ie: during delete event) + if(childIndex == -1) { + return; + } + int spanCount = getTotalSpan(view, parent); + int spanIndex = childIndex % spanCount; + + // If we can, use the SpanSizeLookup since headers screw up the index calculation + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); + if(spanSizeLookup != null) { + spanIndex = spanSizeLookup.getSpanIndex(childIndex, spanCount); + } + } + int spanSize = getSpanSize(parent, childIndex); + + /* INVALID SPAN */ + if (spanCount < 1 || spanSize > 1) return; + + int margins = 0; + if(view instanceof UpdateView) { + View firstChild = ((ViewGroup) view).getChildAt(0); + ViewGroup.LayoutParams layoutParams = firstChild.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + margins = ((LinearLayout.LayoutParams) layoutParams).bottomMargin; + } else if (layoutParams instanceof FrameLayout.LayoutParams) { + margins = ((FrameLayout.LayoutParams) layoutParams).bottomMargin; + } + } + int doubleMargins = margins * 2; + + outRect.top = halfSpacing - margins; + outRect.bottom = halfSpacing - margins; + outRect.left = halfSpacing - margins; + outRect.right = halfSpacing - margins; + + if (isTopEdge(childIndex, spanIndex, spanCount)) { + outRect.top = spacing - doubleMargins; + } + + if (isLeftEdge(spanIndex, spanCount)) { + outRect.left = spacing - doubleMargins; + } + + if (isRightEdge(spanIndex, spanCount)) { + outRect.right = spacing - doubleMargins; + } + + if (isBottomEdge(childIndex, childCount, spanCount)) { + outRect.bottom = spacing - doubleMargins; + } + } + + protected int getTotalSpan(View view, RecyclerView parent) { + RecyclerView.LayoutManager mgr = parent.getLayoutManager(); + if (mgr instanceof GridLayoutManager) { + return ((GridLayoutManager) mgr).getSpanCount(); + } + + return -1; + } + protected int getSpanSize(RecyclerView parent, int childIndex) { + RecyclerView.LayoutManager mgr = parent.getLayoutManager(); + if (mgr instanceof GridLayoutManager) { + GridLayoutManager.SpanSizeLookup lookup = ((GridLayoutManager) mgr).getSpanSizeLookup(); + if(lookup != null) { + return lookup.getSpanSize(childIndex); + } + } + + return 1; + } + + protected boolean isLeftEdge(int spanIndex, int spanCount) { + return spanIndex == 0; + } + + protected boolean isRightEdge(int spanIndex, int spanCount) { + return spanIndex == spanCount - 1; + } + + protected boolean isTopEdge(int childIndex, int spanIndex, int spanCount) { + return childIndex < spanCount && childIndex == spanIndex; + } + + protected boolean isBottomEdge(int childIndex, int childCount, int spanCount) { + return childIndex >= childCount - spanCount; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java b/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java new file mode 100644 index 0000000..3588d97 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java @@ -0,0 +1,34 @@ +package github.nvllsvm.audinaut.view; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.style.LeadingMarginSpan; + +/** + * Created by Scott on 1/13/2015. + */ +public class MyLeadingMarginSpan2 implements LeadingMarginSpan.LeadingMarginSpan2 { + private int margin; + private int lines; + + public MyLeadingMarginSpan2(int lines, int margin) { + this.margin = margin; + this.lines = lines; + } + + @Override + public int getLeadingMargin(boolean first) { + return first ? margin : 0; + } + + @Override + public int getLeadingMarginLineCount() { + return lines; + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, CharSequence text, + int start, int end, boolean first, Layout layout) {} +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java new file mode 100644 index 0000000..5de25ea --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java @@ -0,0 +1,95 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +public class PlaylistSongView extends UpdateView2> { + private static final String TAG = PlaylistSongView.class.getSimpleName(); + + private TextView titleView; + private TextView countView; + private int count = 0; + + public PlaylistSongView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_count_item, this, true); + + titleView = (TextView) findViewById(R.id.basic_count_name); + countView = (TextView) findViewById(R.id.basic_count_count); + } + + protected void setObjectImpl(Playlist playlist, List songs) { + count = 0; + titleView.setText(playlist.getName()); + // Make sure to hide initially so it's not present briefly before update + countView.setVisibility(View.GONE); + } + + @Override + protected void updateBackground() { + // Make sure to reset when starting count + count = 0; + + // Don't try to lookup playlist for Create New + if(!"-1".equals(item.getId())) { + MusicDirectory cache = FileUtil.deserialize(context, Util.getCacheName(context, "playlist", item.getId()), MusicDirectory.class); + if(cache != null) { + // Try to find song instances in the given playlists + for(MusicDirectory.Entry song: item2) { + if(cache.getChildren().contains(song)) { + count++; + } + } + } + } + } + + @Override + protected void update() { + // Update count display with appropriate information + if(count <= 0) { + countView.setVisibility(View.GONE); + } else { + String displayName; + if(count < 10) { + displayName = "0" + count; + } else { + displayName = "" + count; + } + + countView.setText(displayName); + countView.setVisibility(View.VISIBLE); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.java b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.java new file mode 100644 index 0000000..3ce430c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.SyncUtil; + +/** + * Used to display albums in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class PlaylistView extends UpdateView { + private static final String TAG = PlaylistView.class.getSimpleName(); + + private TextView titleView; + private ImageLoader imageLoader; + + public PlaylistView(Context context, ImageLoader imageLoader, boolean largeCell) { + super(context); + LayoutInflater.from(context).inflate(largeCell ? R.layout.basic_cell_item : R.layout.basic_art_item, this, true); + + coverArtView = findViewById(R.id.item_art); + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + + this.imageLoader = imageLoader; + } + + protected void setObjectImpl(Playlist playlist) { + titleView.setText(playlist.getName()); + imageTask = imageLoader.loadImage(coverArtView, playlist, false, true); + } + + public void onUpdateImageView() { + imageTask = imageLoader.loadImage(coverArtView, item, false, true); + } + + @Override + protected void updateBackground() { + pinned = SyncUtil.isSyncedPlaylist(context, item.getId()); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java b/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java new file mode 100644 index 0000000..227635d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java @@ -0,0 +1,121 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class RecyclingImageView extends ImageView { + private boolean invalidated = false; + private OnInvalidated onInvalidated; + + public RecyclingImageView(Context context) { + super(context); + } + + public RecyclingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RecyclingImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public RecyclingImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onDraw(Canvas canvas) { + Drawable drawable = this.getDrawable(); + if(drawable != null) { + if(drawable instanceof BitmapDrawable) { + if (isBitmapRecycled(drawable)) { + this.setImageDrawable(null); + setInvalidated(true); + } + } else if(drawable instanceof TransitionDrawable) { + TransitionDrawable transitionDrawable = (TransitionDrawable) drawable; + + // If last bitmap in chain is recycled, just blank this out since it would be invalid anyways + Drawable lastDrawable = transitionDrawable.getDrawable(transitionDrawable.getNumberOfLayers() - 1); + if(isBitmapRecycled(lastDrawable)) { + this.setImageDrawable(null); + setInvalidated(true); + } else { + // Go through earlier bitmaps and make sure that they are not recycled + for (int i = 0; i < transitionDrawable.getNumberOfLayers(); i++) { + Drawable layerDrawable = transitionDrawable.getDrawable(i); + if (isBitmapRecycled(layerDrawable)) { + // If anything in the chain is broken, just get rid of transition and go to last drawable + this.setImageDrawable(lastDrawable); + break; + } + } + } + } + } + + super.onDraw(canvas); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + setInvalidated(false); + } + + private boolean isBitmapRecycled(Drawable drawable) { + if(!(drawable instanceof BitmapDrawable)) { + return false; + } + + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null && bitmapDrawable.getBitmap().isRecycled()) { + return true; + } else { + return false; + } + } + + public void setInvalidated(boolean invalidated) { + this.invalidated = invalidated; + + if(invalidated && onInvalidated != null) { + onInvalidated.onInvalidated(this); + } + } + public boolean isInvalidated() { + return invalidated; + } + + public void setOnInvalidated(OnInvalidated onInvalidated) { + this.onInvalidated = onInvalidated; + } + + public interface OnInvalidated { + void onInvalidated(RecyclingImageView imageView); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java b/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java new file mode 100644 index 0000000..a45c36a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 Christopher Eby + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.Constants; + +/** + * SeekBar preference to set the shake force threshold. + */ +public class SeekBarPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener { + private static final String TAG = SeekBarPreference.class.getSimpleName(); + /** + * The current value. + */ + private String mValue; + private int mMin; + private int mMax; + private float mStepSize; + private String mDisplay; + + /** + * Our context (needed for getResources()) + */ + private Context mContext; + + /** + * TextView to display current threshold. + */ + private TextView mValueText; + + public SeekBarPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + mContext = context; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SeekBarPreference); + mMin = a.getInteger(R.styleable.SeekBarPreference_min, 0); + mMax = a.getInteger(R.styleable.SeekBarPreference_max, 100); + mStepSize = a.getFloat(R.styleable.SeekBarPreference_stepSize, 1f); + mDisplay = a.getString(R.styleable.SeekBarPreference_display); + if(mDisplay == null) { + mDisplay = "%.0f"; + } + } + + @Override + public CharSequence getSummary() + { + return getSummary(mValue); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) + { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) + { + mValue = restoreValue ? getPersistedString((String) defaultValue) : (String)defaultValue; + } + + /** + * Create the summary for the given value. + * + * @param value The force threshold. + * @return A string representation of the threshold. + */ + private String getSummary(String value) { + try { + int val = Integer.parseInt(value); + return String.format(mDisplay, (val + mMin) / mStepSize); + } catch (Exception e) { + return ""; + } + } + + @Override + protected View onCreateDialogView() + { + View view = super.onCreateDialogView(); + + mValueText = (TextView)view.findViewById(R.id.value); + mValueText.setText(getSummary(mValue)); + + SeekBar seekBar = (SeekBar)view.findViewById(R.id.seek_bar); + seekBar.setMax(mMax - mMin); + try { + seekBar.setProgress(Integer.parseInt(mValue)); + } catch(Exception e) { + seekBar.setProgress(0); + } + seekBar.setOnSeekBarChangeListener(this); + + return view; + } + + @Override + protected void onDialogClosed(boolean positiveResult) + { + if(positiveResult) { + persistString(mValue); + notifyChanged(); + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) + { + if (fromUser) { + mValue = String.valueOf(progress); + mValueText.setText(getSummary(mValue)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) + { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) + { + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java new file mode 100644 index 0000000..0298123 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java @@ -0,0 +1,88 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.User.MusicFolderSetting; + +import static github.nvllsvm.audinaut.domain.User.Setting; + +public class SettingView extends UpdateView2 { + private final TextView titleView; + private final CheckBox checkBox; + + public SettingView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_choice_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + checkBox = (CheckBox) findViewById(R.id.item_checkbox); + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if(item != null) { + item.setValue(isChecked); + } + } + }); + checkBox.setClickable(false); + } + + protected void setObjectImpl(Setting setting, Boolean isEditable) { + // Can't edit non-role parts + String name = setting.getName(); + if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) { + item2 = false; + } + + int res = -1; + if(setting instanceof MusicFolderSetting) { + titleView.setText(((MusicFolderSetting) setting).getLabel()); + } else { + // Last resort to display the raw value + titleView.setText(name); + } + + if(res != -1) { + titleView.setText(res); + } + + if(setting.getValue()) { + checkBox.setChecked(setting.getValue()); + } else { + checkBox.setChecked(false); + } + + checkBox.setEnabled(item2); + } + + @Override + public boolean isCheckable() { + return item2; + } + + public void setChecked(boolean checked) { + checkBox.setChecked(checked); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java new file mode 100644 index 0000000..eaf4bc5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java @@ -0,0 +1,239 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.*; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.ThemeUtil; +import github.nvllsvm.audinaut.util.Util; + +import java.io.File; + +/** + * Used to display songs in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class SongView extends UpdateView2 { + private static final String TAG = SongView.class.getSimpleName(); + + private TextView trackTextView; + private TextView titleTextView; + private TextView playingTextView; + private TextView artistTextView; + private TextView durationTextView; + private TextView statusTextView; + private ImageView statusImageView; + private ImageView playedButton; + private View bottomRowView; + + private DownloadService downloadService; + private long revision = -1; + private DownloadFile downloadFile; + private boolean dontChangeDownloadFile = false; + + private boolean playing = false; + private boolean rightImage = false; + private int moreImage = 0; + private boolean isWorkDone = false; + private boolean isSaved = false; + private File partialFile; + private boolean partialFileExists = false; + private boolean loaded = false; + private boolean isPlayed = false; + private boolean isPlayedShown = false; + private boolean showAlbum = false; + + public SongView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + + trackTextView = (TextView) findViewById(R.id.song_track); + titleTextView = (TextView) findViewById(R.id.song_title); + artistTextView = (TextView) findViewById(R.id.song_artist); + durationTextView = (TextView) findViewById(R.id.song_duration); + statusTextView = (TextView) findViewById(R.id.song_status); + statusImageView = (ImageView) findViewById(R.id.song_status_icon); + playedButton = (ImageButton) findViewById(R.id.song_played); + moreButton = (ImageView) findViewById(R.id.item_more); + bottomRowView = findViewById(R.id.song_bottom); + } + + public void setObjectImpl(MusicDirectory.Entry song, Boolean checkable) { + this.checkable = checkable; + + StringBuilder artist = new StringBuilder(40); + + if(showAlbum) { + artist.append(song.getAlbum()); + } else { + artist.append(song.getArtist()); + } + + durationTextView.setText(Util.formatDuration(song.getDuration())); + bottomRowView.setVisibility(View.VISIBLE); + + String title = song.getTitle(); + Integer track = song.getTrack(); + TextView newPlayingTextView; + if(track != null && Util.getDisplayTrack(context)) { + trackTextView.setText(String.format("%02d", track)); + trackTextView.setVisibility(View.VISIBLE); + newPlayingTextView = trackTextView; + } else { + trackTextView.setVisibility(View.GONE); + newPlayingTextView = titleTextView; + } + + if(newPlayingTextView != playingTextView || playingTextView == null) { + if(playing) { + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playing = false; + } + + playingTextView = newPlayingTextView; + } + + titleTextView.setText(title); + artistTextView.setText(artist); + + this.setBackgroundColor(0x00000000); + + revision = -1; + loaded = false; + dontChangeDownloadFile = false; + } + + public void setDownloadFile(DownloadFile downloadFile) { + this.downloadFile = downloadFile; + dontChangeDownloadFile = true; + } + + public DownloadFile getDownloadFile() { + return downloadFile; + } + + @Override + protected void updateBackground() { + if (downloadService == null) { + downloadService = DownloadService.getInstance(); + if(downloadService == null) { + return; + } + } + + long newRevision = downloadService.getDownloadListUpdateRevision(); + if((revision != newRevision && dontChangeDownloadFile == false) || downloadFile == null) { + downloadFile = downloadService.forSong(item); + revision = newRevision; + } + + isWorkDone = downloadFile.isWorkDone(); + isSaved = downloadFile.isSaved(); + partialFile = downloadFile.getPartialFile(); + partialFileExists = partialFile.exists(); + + // Check if needs to load metadata: check against all fields that we know are null in offline mode + if(item.getBitRate() == null && item.getDuration() == null && item.getDiscNumber() == null && isWorkDone) { + item.loadMetadata(downloadFile.getCompleteFile()); + loaded = true; + } + } + + @Override + protected void update() { + if(loaded) { + setObjectImpl(item, item2); + } + if (downloadService == null || downloadFile == null) { + return; + } + + if (isWorkDone) { + int moreImage = isSaved ? R.drawable.download_pinned : R.drawable.download_cached; + if(moreImage != this.moreImage) { + moreButton.setImageResource(moreImage); + this.moreImage = moreImage; + } + } else if(this.moreImage != R.drawable.download_none_light) { + moreButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.download_none)); + this.moreImage = R.drawable.download_none_light; + } + + if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) { + double percentage = (partialFile.length() * 100.0) / downloadFile.getEstimatedSize(); + percentage = Math.min(percentage, 100); + statusTextView.setText((int)percentage + " %"); + if(!rightImage) { + statusImageView.setVisibility(View.VISIBLE); + rightImage = true; + } + } else if(rightImage) { + statusTextView.setText(null); + statusImageView.setVisibility(View.GONE); + rightImage = false; + } + + boolean playing = Util.equals(downloadService.getCurrentPlaying(), downloadFile); + if (playing) { + if(!this.playing) { + this.playing = playing; + playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); + } + } else { + if(this.playing) { + this.playing = playing; + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + + if(isPlayed) { + if(!isPlayedShown) { + if(playedButton.getDrawable() == null) { + playedButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_played)); + } + + playedButton.setVisibility(View.VISIBLE); + isPlayedShown = true; + } + } else { + if(isPlayedShown) { + playedButton.setVisibility(View.GONE); + isPlayedShown = false; + } + } + } + + public MusicDirectory.Entry getEntry() { + return item; + } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java new file mode 100644 index 0000000..8b46ebe --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java @@ -0,0 +1,32 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class SquareImageView extends RecyclingImageView { + public SquareImageView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(final int widthSpec, final int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java new file mode 100644 index 0000000..f1d4882 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java @@ -0,0 +1,310 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; + +public abstract class UpdateView extends LinearLayout { + private static final String TAG = UpdateView.class.getSimpleName(); + private static final WeakHashMap INSTANCES = new WeakHashMap(); + + protected static Handler backgroundHandler; + protected static Handler uiHandler; + private static Runnable updateRunnable; + private static int activeActivities = 0; + + protected Context context; + protected T item; + protected ImageView moreButton; + protected View coverArtView; + + protected boolean exists = false; + protected boolean pinned = false; + protected boolean shaded = false; + protected SilentBackgroundTask imageTask = null; + protected Drawable startBackgroundDrawable; + + protected final boolean autoUpdate; + protected boolean checkable; + + public UpdateView(Context context) { + this(context, true); + } + public UpdateView(Context context, boolean autoUpdate) { + super(context); + this.context = context; + this.autoUpdate = autoUpdate; + + setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + if(autoUpdate) { + INSTANCES.put(this, null); + } + startUpdater(); + } + + @Override + public void setPressed(boolean pressed) { + + } + + public void setObject(T obj) { + if(item == obj) { + return; + } + + item = obj; + if(imageTask != null) { + imageTask.cancel(); + imageTask = null; + } + if(coverArtView != null && coverArtView instanceof ImageView) { + ((ImageView) coverArtView).setImageDrawable(null); + } + setObjectImpl(obj); + updateBackground(); + update(); + } + public void setObject(T obj1, Object obj2) { + setObject(obj1, null); + } + protected abstract void setObjectImpl(T obj); + + private static synchronized void startUpdater() { + if(uiHandler != null) { + return; + } + + uiHandler = new Handler(); + // Needed so handler is never null until thread creates it + backgroundHandler = uiHandler; + updateRunnable = new Runnable() { + @Override + public void run() { + updateAll(); + } + }; + + new Thread(new Runnable() { + public void run() { + Looper.prepare(); + backgroundHandler = new Handler(Looper.myLooper()); + uiHandler.post(updateRunnable); + Looper.loop(); + } + }, "UpdateView").start(); + } + + public static synchronized void triggerUpdate() { + if(backgroundHandler != null) { + uiHandler.removeCallbacksAndMessages(null); + backgroundHandler.removeCallbacksAndMessages(null); + uiHandler.post(updateRunnable); + } + } + + private static void updateAll() { + try { + // If nothing can see this, stop updating + if(activeActivities == 0) { + activeActivities--; + return; + } + + List views = new ArrayList(); + for (UpdateView view : INSTANCES.keySet()) { + if (view.isShown()) { + views.add(view); + } + } + if(views.size() > 0) { + updateAllLive(views); + } else { + uiHandler.postDelayed(updateRunnable, 2000L); + } + } catch (Throwable x) { + Log.w(TAG, "Error when updating song views.", x); + } + } + private static void updateAllLive(final List views) { + final Runnable runnable = new Runnable() { + @Override + public void run() { + try { + for(UpdateView view: views) { + view.update(); + } + } catch (Throwable x) { + Log.w(TAG, "Error when updating song views.", x); + } + uiHandler.postDelayed(updateRunnable, 1000L); + } + }; + + backgroundHandler.post(new Runnable() { + @Override + public void run() { + try { + for(UpdateView view: views) { + view.updateBackground(); + } + uiHandler.post(runnable); + } catch (Throwable x) { + Log.w(TAG, "Error when updating song views.", x); + } + } + }); + } + + public static boolean hasActiveActivity() { + return activeActivities > 0; + } + + public static void addActiveActivity() { + activeActivities++; + + if(activeActivities == 0 && uiHandler != null && updateRunnable != null) { + activeActivities++; + uiHandler.post(updateRunnable); + } + } + public static void removeActiveActivity() { + activeActivities--; + } + + public static MusicDirectory.Entry findEntry(MusicDirectory.Entry entry) { + for(UpdateView view: INSTANCES.keySet()) { + MusicDirectory.Entry check = null; + if(view instanceof SongView) { + check = ((SongView) view).getEntry(); + } else if(view instanceof AlbumView) { + check = ((AlbumView) view).getEntry(); + } + + if(check != null && entry != check && check.getId().equals(entry.getId())) { + return check; + } + } + + return null; + } + + protected void updateBackground() { + + } + protected void update() { + if(moreButton != null) { + if(exists || pinned) { + if(!shaded) { + moreButton.setImageResource(exists ? R.drawable.download_cached : R.drawable.download_pinned); + shaded = true; + } + } else { + if(shaded) { + moreButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.download_none)); + shaded = false; + } + } + } + + if(coverArtView != null && coverArtView instanceof RecyclingImageView) { + RecyclingImageView recyclingImageView = (RecyclingImageView) coverArtView; + if(recyclingImageView.isInvalidated()) { + onUpdateImageView(); + } + } + } + + public boolean isCheckable() { + return checkable; + } + public void setChecked(boolean checked) { + View child = getChildAt(0); + if (checked && startBackgroundDrawable == null) { + startBackgroundDrawable = child.getBackground(); + child.setBackgroundColor(DrawableTint.getColorRes(context, R.attr.colorPrimary)); + } else if (!checked && startBackgroundDrawable != null) { + child.setBackgroundDrawable(startBackgroundDrawable); + startBackgroundDrawable = null; + } + } + + public void onClick() { + + } + + public void onUpdateImageView() { + + } + + public static class UpdateViewHolder extends RecyclerView.ViewHolder { + private UpdateView updateView; + private View view; + private T item; + + public UpdateViewHolder(UpdateView itemView) { + super(itemView); + + this.updateView = itemView; + this.view = itemView; + } + + // Different is so that call is not ambiguous + public UpdateViewHolder(View view, boolean different) { + super(view); + this.view = view; + } + + public UpdateView getUpdateView() { + return updateView; + } + public View getView() { + return view; + } + public void setItem(T item) { + this.item = item; + } + public T getItem() { + return item; + } + } +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java new file mode 100644 index 0000000..1ec507e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java @@ -0,0 +1,55 @@ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.widget.ImageView; + +public abstract class UpdateView2 extends UpdateView { + protected T2 item2; + + public UpdateView2(Context context) { + super(context); + } + + public UpdateView2(Context context, boolean autoUpdate) { + super(context, autoUpdate); + } + + public final void setObject(T1 obj1) { + setObject(obj1, null); + } + @Override + public void setObject(T1 obj1, Object obj2) { + if(item == obj1 && item2 == obj2) { + return; + } + + item = obj1; + item2 = (T2) obj2; + if(imageTask != null) { + imageTask.cancel(); + imageTask = null; + } + if(coverArtView != null && coverArtView instanceof ImageView) { + ((ImageView) coverArtView).setImageDrawable(null); + } + + setObjectImpl(item, item2); + backgroundHandler.post(new Runnable() { + @Override + public void run() { + updateBackground(); + uiHandler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }); + } + + protected final void setObjectImpl(T1 obj1) { + setObjectImpl(obj1, null); + } + protected abstract void setObjectImpl(T1 obj1, T2 obj2); +} diff --git a/app/src/main/res/anim/enter_from_left.xml b/app/src/main/res/anim/enter_from_left.xml new file mode 100644 index 0000000..3c11332 --- /dev/null +++ b/app/src/main/res/anim/enter_from_left.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/enter_from_right.xml b/app/src/main/res/anim/enter_from_right.xml new file mode 100644 index 0000000..568a0c0 --- /dev/null +++ b/app/src/main/res/anim/enter_from_right.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/exit_to_left.xml b/app/src/main/res/anim/exit_to_left.xml new file mode 100644 index 0000000..2cb8feb --- /dev/null +++ b/app/src/main/res/anim/exit_to_left.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/exit_to_right.xml b/app/src/main/res/anim/exit_to_right.xml new file mode 100644 index 0000000..a3fa5ba --- /dev/null +++ b/app/src/main/res/anim/exit_to_right.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..c41db06 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..d615f2a --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/push_down_in.xml b/app/src/main/res/anim/push_down_in.xml new file mode 100644 index 0000000..6ab9a04 --- /dev/null +++ b/app/src/main/res/anim/push_down_in.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/anim/push_down_out.xml b/app/src/main/res/anim/push_down_out.xml new file mode 100644 index 0000000..ce36458 --- /dev/null +++ b/app/src/main/res/anim/push_down_out.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/anim/push_up_in.xml b/app/src/main/res/anim/push_up_in.xml new file mode 100644 index 0000000..6ef582c --- /dev/null +++ b/app/src/main/res/anim/push_up_in.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/anim/push_up_out.xml b/app/src/main/res/anim/push_up_out.xml new file mode 100644 index 0000000..2b267d5 --- /dev/null +++ b/app/src/main/res/anim/push_up_out.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png new file mode 100644 index 0000000..a312638 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_light.png b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png new file mode 100644 index 0000000..df74111 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_art_default.png b/app/src/main/res/drawable-hdpi/appwidget_art_default.png new file mode 100644 index 0000000..083379b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/appwidget_art_default.png differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png b/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png new file mode 100644 index 0000000..083379b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_bg.9.png b/app/src/main/res/drawable-hdpi/appwidget_bg.9.png new file mode 100644 index 0000000..af8748f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/appwidget_bg.9.png differ diff --git a/app/src/main/res/drawable-hdpi/background.png b/app/src/main/res/drawable-hdpi/background.png new file mode 100644 index 0000000..c38a2a8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/background.png differ diff --git a/app/src/main/res/drawable-hdpi/download_cached.png b/app/src/main/res/drawable-hdpi/download_cached.png new file mode 100644 index 0000000..8b6680b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/download_cached.png differ diff --git a/app/src/main/res/drawable-hdpi/download_none_dark.png b/app/src/main/res/drawable-hdpi/download_none_dark.png new file mode 100644 index 0000000..c3953cc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/download_none_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/download_none_light.png b/app/src/main/res/drawable-hdpi/download_none_light.png new file mode 100644 index 0000000..3cd9f7a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/download_none_light.png differ diff --git a/app/src/main/res/drawable-hdpi/download_pinned.png b/app/src/main/res/drawable-hdpi/download_pinned.png new file mode 100644 index 0000000..2b93e32 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/download_pinned.png differ diff --git a/app/src/main/res/drawable-hdpi/downloading_dark.png b/app/src/main/res/drawable-hdpi/downloading_dark.png new file mode 100644 index 0000000..e8e2351 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/downloading_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/downloading_light.png b/app/src/main/res/drawable-hdpi/downloading_light.png new file mode 100644 index 0000000..13290f0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/downloading_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_add_dark.png b/app/src/main/res/drawable-hdpi/ic_action_add_dark.png new file mode 100644 index 0000000..3933f35 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_add_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_add_light.png b/app/src/main/res/drawable-hdpi/ic_action_add_light.png new file mode 100644 index 0000000..bf583d0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_add_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_album.png b/app/src/main/res/drawable-hdpi/ic_action_album.png new file mode 100644 index 0000000..eec654a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_album.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_artist.png b/app/src/main/res/drawable-hdpi/ic_action_artist.png new file mode 100644 index 0000000..3a7001a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_artist.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png new file mode 100644 index 0000000..88a159c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png new file mode 100644 index 0000000..af80dbd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png new file mode 100644 index 0000000..8bcaeeb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000..d401d69 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png new file mode 100644 index 0000000..e94e68c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000..fac2c93 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good.png new file mode 100644 index 0000000..0e2b7ee Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_good.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png new file mode 100644 index 0000000..ea8e359 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000..5fd1fe5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png new file mode 100644 index 0000000..47d66eb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_song.png b/app/src/main/res/drawable-hdpi/ic_action_song.png new file mode 100644 index 0000000..448611c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_song.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png new file mode 100644 index 0000000..eecb370 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000..cb6f71f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000..3672cd1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png new file mode 100644 index 0000000..10919b9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png new file mode 100644 index 0000000..c0c8448 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000..f0181f7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png new file mode 100644 index 0000000..064f475 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000..7247595 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png new file mode 100644 index 0000000..d2207f1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png new file mode 100644 index 0000000..9aa6213 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png new file mode 100644 index 0000000..e715f19 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png new file mode 100644 index 0000000..b374370 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_download_light.png b/app/src/main/res/drawable-hdpi/ic_menu_download_light.png new file mode 100644 index 0000000..4fd0717 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_download_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png new file mode 100644 index 0000000..006b521 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_library_light.png b/app/src/main/res/drawable-hdpi/ic_menu_library_light.png new file mode 100644 index 0000000..2fc874e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_library_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png new file mode 100644 index 0000000..986b0d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_password_light.png b/app/src/main/res/drawable-hdpi/ic_menu_password_light.png new file mode 100644 index 0000000..c7c6603 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_password_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000..192c718 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000..91860c7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000..68d07bc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000..52240ec Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000..514ee64 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png new file mode 100644 index 0000000..7217435 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png new file mode 100644 index 0000000..b76ee54 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000..d94921c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000..a4db8b2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png new file mode 100644 index 0000000..7758d21 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png new file mode 100644 index 0000000..e22906d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_save_light.png b/app/src/main/res/drawable-hdpi/ic_menu_save_light.png new file mode 100644 index 0000000..5efe601 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_save_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png new file mode 100644 index 0000000..0e82171 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_search_light.png b/app/src/main/res/drawable-hdpi/ic_menu_search_light.png new file mode 100644 index 0000000..a345ccf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_search_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png new file mode 100644 index 0000000..1d4e52d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png new file mode 100644 index 0000000..589c06e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png new file mode 100644 index 0000000..5980e28 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_share_light.png b/app/src/main/res/drawable-hdpi/ic_menu_share_light.png new file mode 100644 index 0000000..04e631b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_share_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png new file mode 100644 index 0000000..4195867 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000..39f5478 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_number_border.png b/app/src/main/res/drawable-hdpi/ic_number_border.png new file mode 100644 index 0000000..84332e6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_number_border.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_social_person.png b/app/src/main/res/drawable-hdpi/ic_social_person.png new file mode 100644 index 0000000..e7b6af3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_social_person.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_played.png b/app/src/main/res/drawable-hdpi/ic_toggle_played.png new file mode 100644 index 0000000..cec4c0b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_toggle_played.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star.png b/app/src/main/res/drawable-hdpi/ic_toggle_star.png new file mode 100644 index 0000000..c30725f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_toggle_star.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000..0733c44 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000..dbdc54c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000..25620fe Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline_light.png differ diff --git a/app/src/main/res/drawable-hdpi/launch.png b/app/src/main/res/drawable-hdpi/launch.png new file mode 100644 index 0000000..e15db35 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/launch.png differ diff --git a/app/src/main/res/drawable-hdpi/main_offline_dark.png b/app/src/main/res/drawable-hdpi/main_offline_dark.png new file mode 100644 index 0000000..8eddd08 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_offline_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/main_offline_light.png b/app/src/main/res/drawable-hdpi/main_offline_light.png new file mode 100644 index 0000000..160dcfc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_offline_light.png differ diff --git a/app/src/main/res/drawable-hdpi/main_select_server_dark.png b/app/src/main/res/drawable-hdpi/main_select_server_dark.png new file mode 100644 index 0000000..fc2345a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_select_server_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/main_select_server_light.png b/app/src/main/res/drawable-hdpi/main_select_server_light.png new file mode 100644 index 0000000..3a256ff Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_select_server_light.png differ diff --git a/app/src/main/res/drawable-hdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-hdpi/main_select_tabs_dark.png new file mode 100644 index 0000000..2775c14 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_select_tabs_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/main_select_tabs_light.png b/app/src/main/res/drawable-hdpi/main_select_tabs_light.png new file mode 100644 index 0000000..2b5198e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/main_select_tabs_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_backward_dark.png b/app/src/main/res/drawable-hdpi/media_backward_dark.png new file mode 100644 index 0000000..91b8171 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_backward_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_backward_light.png b/app/src/main/res/drawable-hdpi/media_backward_light.png new file mode 100644 index 0000000..92e8000 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_backward_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_dark.png b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png new file mode 100644 index 0000000..bf23a56 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_light.png b/app/src/main/res/drawable-hdpi/media_fastforward_light.png new file mode 100644 index 0000000..2935e61 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_fastforward_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_forward_dark.png b/app/src/main/res/drawable-hdpi/media_forward_dark.png new file mode 100644 index 0000000..d488bd9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_forward_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_forward_light.png b/app/src/main/res/drawable-hdpi/media_forward_light.png new file mode 100644 index 0000000..fb819e1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_forward_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_pause_dark.png b/app/src/main/res/drawable-hdpi/media_pause_dark.png new file mode 100644 index 0000000..139d448 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_pause_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_pause_light.png b/app/src/main/res/drawable-hdpi/media_pause_light.png new file mode 100644 index 0000000..b96ea88 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_pause_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-hdpi/media_repeat_all_dark.png new file mode 100644 index 0000000..a26a729 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_all_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_all_light.png b/app/src/main/res/drawable-hdpi/media_repeat_all_light.png new file mode 100644 index 0000000..feeb662 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_all_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-hdpi/media_repeat_off_dark.png new file mode 100644 index 0000000..172575f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_off_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_off_light.png b/app/src/main/res/drawable-hdpi/media_repeat_off_light.png new file mode 100644 index 0000000..6a7f51a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_off_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-hdpi/media_repeat_single_dark.png new file mode 100644 index 0000000..568cf18 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_single_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_repeat_single_light.png b/app/src/main/res/drawable-hdpi/media_repeat_single_light.png new file mode 100644 index 0000000..d3b75bc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_repeat_single_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_rewind_dark.png b/app/src/main/res/drawable-hdpi/media_rewind_dark.png new file mode 100644 index 0000000..127b658 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_rewind_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_rewind_light.png b/app/src/main/res/drawable-hdpi/media_rewind_light.png new file mode 100644 index 0000000..7a6d6ae Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_rewind_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_start_dark.png b/app/src/main/res/drawable-hdpi/media_start_dark.png new file mode 100644 index 0000000..4039ee5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_start_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_start_light.png b/app/src/main/res/drawable-hdpi/media_start_light.png new file mode 100644 index 0000000..068c4ba Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_start_light.png differ diff --git a/app/src/main/res/drawable-hdpi/media_stop_dark.png b/app/src/main/res/drawable-hdpi/media_stop_dark.png new file mode 100644 index 0000000..1f4f773 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_stop_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/media_stop_light.png b/app/src/main/res/drawable-hdpi/media_stop_light.png new file mode 100644 index 0000000..e0b2358 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_stop_light.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_close_dark.png b/app/src/main/res/drawable-hdpi/notification_close_dark.png new file mode 100644 index 0000000..561f300 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_close_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_close_light.png b/app/src/main/res/drawable-hdpi/notification_close_light.png new file mode 100644 index 0000000..7bd2d51 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_close_light.png differ diff --git a/app/src/main/res/drawable-hdpi/playing_dark.png b/app/src/main/res/drawable-hdpi/playing_dark.png new file mode 100644 index 0000000..bf0b086 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playing_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/playing_light.png b/app/src/main/res/drawable-hdpi/playing_light.png new file mode 100644 index 0000000..48203a1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playing_light.png differ diff --git a/app/src/main/res/drawable-hdpi/stat_notify_playing.png b/app/src/main/res/drawable-hdpi/stat_notify_playing.png new file mode 100644 index 0000000..aa58825 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/stat_notify_playing.png differ diff --git a/app/src/main/res/drawable-hdpi/stat_notify_sync.png b/app/src/main/res/drawable-hdpi/stat_notify_sync.png new file mode 100644 index 0000000..437ff7b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/stat_notify_sync.png differ diff --git a/app/src/main/res/drawable-hdpi/unknown_album.png b/app/src/main/res/drawable-hdpi/unknown_album.png new file mode 100644 index 0000000..61bfc0e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/unknown_album.png differ diff --git a/app/src/main/res/drawable-hdpi/unknown_album_large.png b/app/src/main/res/drawable-hdpi/unknown_album_large.png new file mode 100644 index 0000000..f5d008c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/unknown_album_large.png differ diff --git a/app/src/main/res/drawable-large/unknown_album.png b/app/src/main/res/drawable-large/unknown_album.png new file mode 100644 index 0000000..0f90dfc Binary files /dev/null and b/app/src/main/res/drawable-large/unknown_album.png differ diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png new file mode 100644 index 0000000..f4640d3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_light.png b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png new file mode 100644 index 0000000..a6a255e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png differ diff --git a/app/src/main/res/drawable-mdpi/download_cached.png b/app/src/main/res/drawable-mdpi/download_cached.png new file mode 100644 index 0000000..71277b0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/download_cached.png differ diff --git a/app/src/main/res/drawable-mdpi/download_none_dark.png b/app/src/main/res/drawable-mdpi/download_none_dark.png new file mode 100644 index 0000000..f55a163 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/download_none_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/download_none_light.png b/app/src/main/res/drawable-mdpi/download_none_light.png new file mode 100644 index 0000000..dd29866 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/download_none_light.png differ diff --git a/app/src/main/res/drawable-mdpi/download_pinned.png b/app/src/main/res/drawable-mdpi/download_pinned.png new file mode 100644 index 0000000..4dd6583 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/download_pinned.png differ diff --git a/app/src/main/res/drawable-mdpi/downloading_dark.png b/app/src/main/res/drawable-mdpi/downloading_dark.png new file mode 100644 index 0000000..d2616a4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/downloading_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/downloading_light.png b/app/src/main/res/drawable-mdpi/downloading_light.png new file mode 100644 index 0000000..9fcbe50 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/downloading_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_add_dark.png b/app/src/main/res/drawable-mdpi/ic_action_add_dark.png new file mode 100644 index 0000000..078d385 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_add_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_add_light.png b/app/src/main/res/drawable-mdpi/ic_action_add_light.png new file mode 100644 index 0000000..56eb651 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_add_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_album.png b/app/src/main/res/drawable-mdpi/ic_action_album.png new file mode 100644 index 0000000..db4c5ee Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_album.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_artist.png b/app/src/main/res/drawable-mdpi/ic_action_artist.png new file mode 100644 index 0000000..7f1ac6c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_artist.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png new file mode 100644 index 0000000..e98cd31 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png new file mode 100644 index 0000000..558584b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png new file mode 100644 index 0000000..e34fae2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000..85bd876 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png new file mode 100644 index 0000000..2b065cb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000..5a560b3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good.png new file mode 100644 index 0000000..5d3390c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_good.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png new file mode 100644 index 0000000..074e4b3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000..4d909ae Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png new file mode 100644 index 0000000..1df8ea8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_song.png b/app/src/main/res/drawable-mdpi/ic_action_song.png new file mode 100644 index 0000000..b707921 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_song.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png new file mode 100644 index 0000000..7a68e9f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_volume_light.png b/app/src/main/res/drawable-mdpi/ic_action_volume_light.png new file mode 100644 index 0000000..4e5dc12 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_volume_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png new file mode 100644 index 0000000..23cb6ac Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000..e1c1200 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000..079fdcc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png new file mode 100644 index 0000000..33a4f80 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png new file mode 100644 index 0000000..7117a2f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000..8edef41 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png new file mode 100644 index 0000000..6ad9264 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000..2ebb97d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png new file mode 100644 index 0000000..3ede325 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png new file mode 100644 index 0000000..3e85314 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png new file mode 100644 index 0000000..30e4b57 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png new file mode 100644 index 0000000..361f8a7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_download_light.png b/app/src/main/res/drawable-mdpi/ic_menu_download_light.png new file mode 100644 index 0000000..3818538 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_download_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png new file mode 100644 index 0000000..b4a56f3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_library_light.png b/app/src/main/res/drawable-mdpi/ic_menu_library_light.png new file mode 100644 index 0000000..89b0927 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_library_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png new file mode 100644 index 0000000..177b9e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_password_light.png b/app/src/main/res/drawable-mdpi/ic_menu_password_light.png new file mode 100644 index 0000000..2d57bb0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_password_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000..d37d26f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000..ea5c055 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000..bc30803 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000..218720c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000..33bae5d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png new file mode 100644 index 0000000..c1939a2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png new file mode 100644 index 0000000..b9bcca6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000..102e0f1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000..4ef960d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png new file mode 100644 index 0000000..cb97678 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png new file mode 100644 index 0000000..ad14439 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_save_light.png b/app/src/main/res/drawable-mdpi/ic_menu_save_light.png new file mode 100644 index 0000000..3614cd0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_save_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png new file mode 100644 index 0000000..7919a79 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_search_light.png b/app/src/main/res/drawable-mdpi/ic_menu_search_light.png new file mode 100644 index 0000000..9eda3c2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_search_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png new file mode 100644 index 0000000..1f4c88a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png new file mode 100644 index 0000000..a99f86d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png new file mode 100644 index 0000000..96eca7c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_share_light.png b/app/src/main/res/drawable-mdpi/ic_menu_share_light.png new file mode 100644 index 0000000..e9fd691 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_share_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png new file mode 100644 index 0000000..de814cd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000..e833c0c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_number_border.png b/app/src/main/res/drawable-mdpi/ic_number_border.png new file mode 100644 index 0000000..1964fd6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_number_border.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_social_person.png b/app/src/main/res/drawable-mdpi/ic_social_person.png new file mode 100644 index 0000000..f2ff9f1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_social_person.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_played.png b/app/src/main/res/drawable-mdpi/ic_toggle_played.png new file mode 100644 index 0000000..4fa676a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_toggle_played.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star.png b/app/src/main/res/drawable-mdpi/ic_toggle_star.png new file mode 100644 index 0000000..fbdd764 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_toggle_star.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000..566345b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000..6837e52 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000..62d446b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png differ diff --git a/app/src/main/res/drawable-mdpi/launch.png b/app/src/main/res/drawable-mdpi/launch.png new file mode 100644 index 0000000..ee2b9e6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/launch.png differ diff --git a/app/src/main/res/drawable-mdpi/main_offline_dark.png b/app/src/main/res/drawable-mdpi/main_offline_dark.png new file mode 100644 index 0000000..b0a909a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_offline_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/main_offline_light.png b/app/src/main/res/drawable-mdpi/main_offline_light.png new file mode 100644 index 0000000..a9890dd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_offline_light.png differ diff --git a/app/src/main/res/drawable-mdpi/main_select_server_dark.png b/app/src/main/res/drawable-mdpi/main_select_server_dark.png new file mode 100644 index 0000000..8f8de3a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_select_server_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/main_select_server_light.png b/app/src/main/res/drawable-mdpi/main_select_server_light.png new file mode 100644 index 0000000..28e00fc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_select_server_light.png differ diff --git a/app/src/main/res/drawable-mdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-mdpi/main_select_tabs_dark.png new file mode 100644 index 0000000..682f201 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_select_tabs_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/main_select_tabs_light.png b/app/src/main/res/drawable-mdpi/main_select_tabs_light.png new file mode 100644 index 0000000..05ed913 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/main_select_tabs_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_backward_dark.png b/app/src/main/res/drawable-mdpi/media_backward_dark.png new file mode 100644 index 0000000..be797b5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_backward_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_backward_light.png b/app/src/main/res/drawable-mdpi/media_backward_light.png new file mode 100644 index 0000000..ea543c1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_backward_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_dark.png b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png new file mode 100644 index 0000000..16455b1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_light.png b/app/src/main/res/drawable-mdpi/media_fastforward_light.png new file mode 100644 index 0000000..b1e0571 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_fastforward_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_forward_dark.png b/app/src/main/res/drawable-mdpi/media_forward_dark.png new file mode 100644 index 0000000..1a72ac9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_forward_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_forward_light.png b/app/src/main/res/drawable-mdpi/media_forward_light.png new file mode 100644 index 0000000..e9dd01c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_forward_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_pause_dark.png b/app/src/main/res/drawable-mdpi/media_pause_dark.png new file mode 100644 index 0000000..7244185 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_pause_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_pause_light.png b/app/src/main/res/drawable-mdpi/media_pause_light.png new file mode 100644 index 0000000..049fef3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_pause_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-mdpi/media_repeat_all_dark.png new file mode 100644 index 0000000..321eb3c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_all_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_all_light.png b/app/src/main/res/drawable-mdpi/media_repeat_all_light.png new file mode 100644 index 0000000..18627bb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_all_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-mdpi/media_repeat_off_dark.png new file mode 100644 index 0000000..33bdeb9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_off_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_off_light.png b/app/src/main/res/drawable-mdpi/media_repeat_off_light.png new file mode 100644 index 0000000..f534899 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_off_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-mdpi/media_repeat_single_dark.png new file mode 100644 index 0000000..ce18dda Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_single_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_repeat_single_light.png b/app/src/main/res/drawable-mdpi/media_repeat_single_light.png new file mode 100644 index 0000000..0e2d292 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_repeat_single_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_rewind_dark.png b/app/src/main/res/drawable-mdpi/media_rewind_dark.png new file mode 100644 index 0000000..e8aa8ae Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_rewind_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_rewind_light.png b/app/src/main/res/drawable-mdpi/media_rewind_light.png new file mode 100644 index 0000000..7d60b0b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_rewind_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_start_dark.png b/app/src/main/res/drawable-mdpi/media_start_dark.png new file mode 100644 index 0000000..84e4175 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_start_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_start_light.png b/app/src/main/res/drawable-mdpi/media_start_light.png new file mode 100644 index 0000000..5338069 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_start_light.png differ diff --git a/app/src/main/res/drawable-mdpi/media_stop_dark.png b/app/src/main/res/drawable-mdpi/media_stop_dark.png new file mode 100644 index 0000000..736267a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_stop_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/media_stop_light.png b/app/src/main/res/drawable-mdpi/media_stop_light.png new file mode 100644 index 0000000..ace9793 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_stop_light.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_close_dark.png b/app/src/main/res/drawable-mdpi/notification_close_dark.png new file mode 100644 index 0000000..55e2bf2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_close_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_close_light.png b/app/src/main/res/drawable-mdpi/notification_close_light.png new file mode 100644 index 0000000..780add4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_close_light.png differ diff --git a/app/src/main/res/drawable-mdpi/playing_dark.png b/app/src/main/res/drawable-mdpi/playing_dark.png new file mode 100644 index 0000000..b5b90e0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playing_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/playing_light.png b/app/src/main/res/drawable-mdpi/playing_light.png new file mode 100644 index 0000000..55a0070 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playing_light.png differ diff --git a/app/src/main/res/drawable-mdpi/stat_notify_playing.png b/app/src/main/res/drawable-mdpi/stat_notify_playing.png new file mode 100644 index 0000000..fc0e806 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/stat_notify_playing.png differ diff --git a/app/src/main/res/drawable-mdpi/stat_notify_sync.png b/app/src/main/res/drawable-mdpi/stat_notify_sync.png new file mode 100644 index 0000000..cbc0b33 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/stat_notify_sync.png differ diff --git a/app/src/main/res/drawable-v21/notification_backward.xml b/app/src/main/res/drawable-v21/notification_backward.xml new file mode 100644 index 0000000..ffebb00 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_backward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_close.xml b/app/src/main/res/drawable-v21/notification_close.xml new file mode 100644 index 0000000..4a93427 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_close.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_fastforward.xml b/app/src/main/res/drawable-v21/notification_fastforward.xml new file mode 100644 index 0000000..d0ab76a --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_fastforward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_forward.xml b/app/src/main/res/drawable-v21/notification_forward.xml new file mode 100644 index 0000000..0d3c93d --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_forward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_pause.xml b/app/src/main/res/drawable-v21/notification_pause.xml new file mode 100644 index 0000000..330260f --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_pause.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_rewind.xml b/app/src/main/res/drawable-v21/notification_rewind.xml new file mode 100644 index 0000000..25a16a0 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_rewind.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_start.xml b/app/src/main/res/drawable-v21/notification_start.xml new file mode 100644 index 0000000..75e23c0 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_start.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png new file mode 100644 index 0000000..3b98cba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png new file mode 100644 index 0000000..13dd7b2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/download_cached.png b/app/src/main/res/drawable-xhdpi/download_cached.png new file mode 100644 index 0000000..58620b7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/download_cached.png differ diff --git a/app/src/main/res/drawable-xhdpi/download_none_dark.png b/app/src/main/res/drawable-xhdpi/download_none_dark.png new file mode 100644 index 0000000..826361e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/download_none_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/download_none_light.png b/app/src/main/res/drawable-xhdpi/download_none_light.png new file mode 100644 index 0000000..58ff3dc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/download_none_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/download_pinned.png b/app/src/main/res/drawable-xhdpi/download_pinned.png new file mode 100644 index 0000000..171c209 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/download_pinned.png differ diff --git a/app/src/main/res/drawable-xhdpi/downloading_dark.png b/app/src/main/res/drawable-xhdpi/downloading_dark.png new file mode 100644 index 0000000..fe7eee7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/downloading_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/downloading_light.png b/app/src/main/res/drawable-xhdpi/downloading_light.png new file mode 100644 index 0000000..4f7b655 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/downloading_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png new file mode 100644 index 0000000..0ba1405 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_add_light.png b/app/src/main/res/drawable-xhdpi/ic_action_add_light.png new file mode 100644 index 0000000..0ccc11a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_add_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_album.png b/app/src/main/res/drawable-xhdpi/ic_action_album.png new file mode 100644 index 0000000..8f01da6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_album.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_artist.png b/app/src/main/res/drawable-xhdpi/ic_action_artist.png new file mode 100644 index 0000000..0a1bc51 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_artist.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png new file mode 100644 index 0000000..5c8e294 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png new file mode 100644 index 0000000..e19361e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png new file mode 100644 index 0000000..0d92ad3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000..a211c0a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png new file mode 100644 index 0000000..f5fdb64 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000..85e960f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png new file mode 100644 index 0000000..b4ad285 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png new file mode 100644 index 0000000..d421f4c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000..3edad0c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png new file mode 100644 index 0000000..2c929e3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_song.png b/app/src/main/res/drawable-xhdpi/ic_action_song.png new file mode 100644 index 0000000..c1e3174 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_song.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png new file mode 100644 index 0000000..31ad283 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png b/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png new file mode 100644 index 0000000..dc2af73 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png new file mode 100644 index 0000000..36a1d13 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000..eac24f7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000..f160fc3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png new file mode 100644 index 0000000..69b4f65 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png new file mode 100644 index 0000000..c623d7e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000..34cdace Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png new file mode 100644 index 0000000..c3ac366 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000..2a757a6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png new file mode 100644 index 0000000..9d21bbd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png new file mode 100644 index 0000000..5def910 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png new file mode 100644 index 0000000..5586e1c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png new file mode 100644 index 0000000..e08f624 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png new file mode 100644 index 0000000..4e17b60 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png new file mode 100644 index 0000000..2b2e796 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png new file mode 100644 index 0000000..fc83844 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png new file mode 100644 index 0000000..da58df5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png new file mode 100644 index 0000000..71834fa Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000..5601f8b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000..80661d5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000..1f2509d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000..39e4a6f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000..8b5f3f1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png new file mode 100644 index 0000000..ca30407 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png new file mode 100644 index 0000000..fe8bad5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000..8defd5d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000..cfcfef9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png new file mode 100644 index 0000000..de35e04 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png new file mode 100644 index 0000000..19486f3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png new file mode 100644 index 0000000..abc05ef Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png new file mode 100644 index 0000000..14e8033 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png new file mode 100644 index 0000000..fa8017a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png new file mode 100644 index 0000000..abef850 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png new file mode 100644 index 0000000..0cfd427 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png new file mode 100644 index 0000000..6aac13f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png new file mode 100644 index 0000000..6983e09 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png new file mode 100644 index 0000000..96bc78b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000..f93ed76 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_number_border.png b/app/src/main/res/drawable-xhdpi/ic_number_border.png new file mode 100644 index 0000000..44e6a3f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_number_border.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_social_person.png b/app/src/main/res/drawable-xhdpi/ic_social_person.png new file mode 100644 index 0000000..f560d2c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_social_person.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png new file mode 100644 index 0000000..e3c26fc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star.png new file mode 100644 index 0000000..e7ef28a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_toggle_star.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000..d8c89bd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000..f6f8234 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000..ecf0d43 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/launch.png b/app/src/main/res/drawable-xhdpi/launch.png new file mode 100644 index 0000000..6b425e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/launch.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_offline_dark.png b/app/src/main/res/drawable-xhdpi/main_offline_dark.png new file mode 100644 index 0000000..03708c8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_offline_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_offline_light.png b/app/src/main/res/drawable-xhdpi/main_offline_light.png new file mode 100644 index 0000000..21b6a14 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_offline_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xhdpi/main_select_server_dark.png new file mode 100644 index 0000000..bb9a236 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_select_server_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_select_server_light.png b/app/src/main/res/drawable-xhdpi/main_select_server_light.png new file mode 100644 index 0000000..a6b1135 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_select_server_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png new file mode 100644 index 0000000..d4dbb69 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/main_select_tabs_light.png b/app/src/main/res/drawable-xhdpi/main_select_tabs_light.png new file mode 100644 index 0000000..ff3e8c8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/main_select_tabs_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_backward_dark.png b/app/src/main/res/drawable-xhdpi/media_backward_dark.png new file mode 100644 index 0000000..11e1e4f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_backward_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_backward_light.png b/app/src/main/res/drawable-xhdpi/media_backward_light.png new file mode 100644 index 0000000..441def2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_backward_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png new file mode 100644 index 0000000..de5a50f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png new file mode 100644 index 0000000..1b47bd0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_forward_dark.png b/app/src/main/res/drawable-xhdpi/media_forward_dark.png new file mode 100644 index 0000000..e520980 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_forward_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_forward_light.png b/app/src/main/res/drawable-xhdpi/media_forward_light.png new file mode 100644 index 0000000..f9b522e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_forward_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_pause_dark.png b/app/src/main/res/drawable-xhdpi/media_pause_dark.png new file mode 100644 index 0000000..f1f9796 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_pause_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_pause_light.png b/app/src/main/res/drawable-xhdpi/media_pause_light.png new file mode 100644 index 0000000..19f6b05 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_pause_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000..0c8ff22 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_all_light.png b/app/src/main/res/drawable-xhdpi/media_repeat_all_light.png new file mode 100644 index 0000000..d7b780a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_all_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png new file mode 100644 index 0000000..6922a06 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_off_light.png b/app/src/main/res/drawable-xhdpi/media_repeat_off_light.png new file mode 100644 index 0000000..bf8f385 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_off_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png new file mode 100644 index 0000000..f3fac9a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_single_light.png b/app/src/main/res/drawable-xhdpi/media_repeat_single_light.png new file mode 100644 index 0000000..bacc1e3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_repeat_single_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png new file mode 100644 index 0000000..7505c15 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_light.png b/app/src/main/res/drawable-xhdpi/media_rewind_light.png new file mode 100644 index 0000000..10bd470 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_rewind_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_start_dark.png b/app/src/main/res/drawable-xhdpi/media_start_dark.png new file mode 100644 index 0000000..0ee9c0d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_start_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_start_light.png b/app/src/main/res/drawable-xhdpi/media_start_light.png new file mode 100644 index 0000000..cd26708 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_start_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_stop_dark.png b/app/src/main/res/drawable-xhdpi/media_stop_dark.png new file mode 100644 index 0000000..4f191b8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_stop_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_stop_light.png b/app/src/main/res/drawable-xhdpi/media_stop_light.png new file mode 100644 index 0000000..38d8cff Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_stop_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_close_dark.png b/app/src/main/res/drawable-xhdpi/notification_close_dark.png new file mode 100644 index 0000000..7da5f03 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_close_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_close_light.png b/app/src/main/res/drawable-xhdpi/notification_close_light.png new file mode 100644 index 0000000..7ccd3d2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_close_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/playing_dark.png b/app/src/main/res/drawable-xhdpi/playing_dark.png new file mode 100644 index 0000000..280a708 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playing_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/playing_light.png b/app/src/main/res/drawable-xhdpi/playing_light.png new file mode 100644 index 0000000..5760838 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playing_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_playing.png b/app/src/main/res/drawable-xhdpi/stat_notify_playing.png new file mode 100644 index 0000000..a926014 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/stat_notify_playing.png differ diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_sync.png b/app/src/main/res/drawable-xhdpi/stat_notify_sync.png new file mode 100644 index 0000000..aecd77c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/stat_notify_sync.png differ diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png new file mode 100644 index 0000000..69f0d3f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png new file mode 100644 index 0000000..de26b6d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/download_cached.png b/app/src/main/res/drawable-xxhdpi/download_cached.png new file mode 100644 index 0000000..aed0d67 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/download_cached.png differ diff --git a/app/src/main/res/drawable-xxhdpi/download_none_dark.png b/app/src/main/res/drawable-xxhdpi/download_none_dark.png new file mode 100644 index 0000000..3dd3411 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/download_none_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/download_none_light.png b/app/src/main/res/drawable-xxhdpi/download_none_light.png new file mode 100644 index 0000000..a675574 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/download_none_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/download_pinned.png b/app/src/main/res/drawable-xxhdpi/download_pinned.png new file mode 100644 index 0000000..07a2bf4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/download_pinned.png differ diff --git a/app/src/main/res/drawable-xxhdpi/downloading_dark.png b/app/src/main/res/drawable-xxhdpi/downloading_dark.png new file mode 100644 index 0000000..3c7e7b6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/downloading_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/downloading_light.png b/app/src/main/res/drawable-xxhdpi/downloading_light.png new file mode 100644 index 0000000..d417b4a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/downloading_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png new file mode 100644 index 0000000..b5a0c30 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png new file mode 100644 index 0000000..d74ec1b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_artist.png b/app/src/main/res/drawable-xxhdpi/ic_action_artist.png new file mode 100644 index 0000000..c1fd3b0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_artist.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png new file mode 100644 index 0000000..69cc227 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png new file mode 100644 index 0000000..23c8910 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png new file mode 100644 index 0000000..55e5149 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000..d3f46a3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png new file mode 100644 index 0000000..1d7d999 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000..c67c9ed Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png new file mode 100644 index 0000000..d916796 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png new file mode 100644 index 0000000..380b706 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000..bc7aabb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png new file mode 100644 index 0000000..0d18d0a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_song.png b/app/src/main/res/drawable-xxhdpi/ic_action_song.png new file mode 100644 index 0000000..0d6c268 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_song.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png new file mode 100644 index 0000000..9fad1f6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png new file mode 100644 index 0000000..ad6903d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png new file mode 100644 index 0000000..c563ff5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000..54e49df Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000..dff1838 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png new file mode 100644 index 0000000..67cc1c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png new file mode 100644 index 0000000..8b9c597 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000..54da810 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png new file mode 100644 index 0000000..cb5e919 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000..7137a47 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png new file mode 100644 index 0000000..5d2c41a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png new file mode 100644 index 0000000..be6572b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png new file mode 100644 index 0000000..52e2f29 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png new file mode 100644 index 0000000..6fe35de Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png new file mode 100644 index 0000000..d1462ad Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png new file mode 100644 index 0000000..951b9c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png new file mode 100644 index 0000000..6ecd8e6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png new file mode 100644 index 0000000..7436e5a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png new file mode 100644 index 0000000..c209086 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000..4ee84ad Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000..b7ac964 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000..0dea37f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000..c27e0bc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000..dc7cb68 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png new file mode 100644 index 0000000..e87a7b8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png new file mode 100644 index 0000000..cdc926c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000..0c730c8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000..d8e060d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png new file mode 100644 index 0000000..e78aa48 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png new file mode 100644 index 0000000..bf48792 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png new file mode 100644 index 0000000..fe8bfde Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png new file mode 100644 index 0000000..6b30493 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png new file mode 100644 index 0000000..93684fc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png new file mode 100644 index 0000000..2e7e993 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png new file mode 100644 index 0000000..a709218 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png new file mode 100644 index 0000000..9bde624 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png new file mode 100644 index 0000000..ffae731 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png new file mode 100644 index 0000000..933b3a3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000..3b67542 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_number_border.png b/app/src/main/res/drawable-xxhdpi/ic_number_border.png new file mode 100644 index 0000000..56d779f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_number_border.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_social_person.png b/app/src/main/res/drawable-xxhdpi/ic_social_person.png new file mode 100644 index 0000000..f38adb8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_social_person.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png new file mode 100644 index 0000000..7a5c5be Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png new file mode 100644 index 0000000..f3b0e8a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000..fda09b8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000..64427ec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000..94def1b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/launch.png b/app/src/main/res/drawable-xxhdpi/launch.png new file mode 100644 index 0000000..7526009 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/launch.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_offline_dark.png b/app/src/main/res/drawable-xxhdpi/main_offline_dark.png new file mode 100644 index 0000000..557028c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_offline_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_offline_light.png b/app/src/main/res/drawable-xxhdpi/main_offline_light.png new file mode 100644 index 0000000..130e3da Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_offline_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png new file mode 100644 index 0000000..e9d64d6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_select_server_light.png b/app/src/main/res/drawable-xxhdpi/main_select_server_light.png new file mode 100644 index 0000000..e6ec9a9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_select_server_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-xxhdpi/main_select_tabs_dark.png new file mode 100644 index 0000000..4895780 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_select_tabs_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png b/app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png new file mode 100644 index 0000000..3f31efa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_backward_dark.png b/app/src/main/res/drawable-xxhdpi/media_backward_dark.png new file mode 100644 index 0000000..0ea774f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_backward_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_backward_light.png b/app/src/main/res/drawable-xxhdpi/media_backward_light.png new file mode 100644 index 0000000..303616f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_backward_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png new file mode 100644 index 0000000..64be1c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png new file mode 100644 index 0000000..f0e0132 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_forward_dark.png b/app/src/main/res/drawable-xxhdpi/media_forward_dark.png new file mode 100644 index 0000000..28d3723 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_forward_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_forward_light.png b/app/src/main/res/drawable-xxhdpi/media_forward_light.png new file mode 100644 index 0000000..c76e116 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_forward_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_pause_dark.png b/app/src/main/res/drawable-xxhdpi/media_pause_dark.png new file mode 100644 index 0000000..2dd83db Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_pause_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_pause_light.png b/app/src/main/res/drawable-xxhdpi/media_pause_light.png new file mode 100644 index 0000000..a31cace Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_pause_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000..70866c8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_all_light.png b/app/src/main/res/drawable-xxhdpi/media_repeat_all_light.png new file mode 100644 index 0000000..e9786d8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_all_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png new file mode 100644 index 0000000..a8a2043 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png b/app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png new file mode 100644 index 0000000..0a96bd4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-xxhdpi/media_repeat_single_dark.png new file mode 100644 index 0000000..a4532de Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_single_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png b/app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png new file mode 100644 index 0000000..c02ab24 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png new file mode 100644 index 0000000..6fcaa15 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png new file mode 100644 index 0000000..ffcf2af Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_start_dark.png b/app/src/main/res/drawable-xxhdpi/media_start_dark.png new file mode 100644 index 0000000..86d60a1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_start_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_start_light.png b/app/src/main/res/drawable-xxhdpi/media_start_light.png new file mode 100644 index 0000000..fe8e549 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_start_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_stop_dark.png b/app/src/main/res/drawable-xxhdpi/media_stop_dark.png new file mode 100644 index 0000000..afd3263 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_stop_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_stop_light.png b/app/src/main/res/drawable-xxhdpi/media_stop_light.png new file mode 100644 index 0000000..4e6b0bd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_stop_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_close_dark.png b/app/src/main/res/drawable-xxhdpi/notification_close_dark.png new file mode 100644 index 0000000..479a90d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_close_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_close_light.png b/app/src/main/res/drawable-xxhdpi/notification_close_light.png new file mode 100644 index 0000000..9cb6f70 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_close_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playing_dark.png b/app/src/main/res/drawable-xxhdpi/playing_dark.png new file mode 100644 index 0000000..5af5088 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playing_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playing_light.png b/app/src/main/res/drawable-xxhdpi/playing_light.png new file mode 100644 index 0000000..6b5e382 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playing_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png b/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png new file mode 100644 index 0000000..b6bd07e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png differ diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png b/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png new file mode 100644 index 0000000..ab1e72d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png new file mode 100644 index 0000000..674c41b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png new file mode 100644 index 0000000..566bea8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/download_none_dark.png b/app/src/main/res/drawable-xxxhdpi/download_none_dark.png new file mode 100644 index 0000000..188af24 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/download_none_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/download_none_light.png b/app/src/main/res/drawable-xxxhdpi/download_none_light.png new file mode 100644 index 0000000..aae6689 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/download_none_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/downloading_dark.png b/app/src/main/res/drawable-xxxhdpi/downloading_dark.png new file mode 100644 index 0000000..96bea87 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/downloading_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/downloading_light.png b/app/src/main/res/drawable-xxxhdpi/downloading_light.png new file mode 100644 index 0000000..22e1047 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/downloading_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png new file mode 100644 index 0000000..12f250a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_add_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_add_light.png new file mode 100644 index 0000000..fe85b5a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_add_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_artist.png b/app/src/main/res/drawable-xxxhdpi/ic_action_artist.png new file mode 100644 index 0000000..3127416 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_artist.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png new file mode 100644 index 0000000..2bba0e0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png new file mode 100644 index 0000000..50d849f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png new file mode 100644 index 0000000..2687439 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000..c3849e5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_light.png new file mode 100644 index 0000000..2a39350 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000..ee1c17d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png new file mode 100644 index 0000000..f49716a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_dark.png new file mode 100644 index 0000000..4b69717 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000..f31fab3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png new file mode 100644 index 0000000..f2a195e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_song.png b/app/src/main/res/drawable-xxxhdpi/ic_action_song.png new file mode 100644 index 0000000..5d836e2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_song.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_dark.png new file mode 100644 index 0000000..0d5d0ba Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000..2678249 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000..a7a645f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_light.png new file mode 100644 index 0000000..052d4be Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_dark.png new file mode 100644 index 0000000..b0d76a4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000..a47931d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_selected.png new file mode 100644 index 0000000..26da543 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_selected.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000..7a4f300 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png new file mode 100644 index 0000000..51443f5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_dark.png new file mode 100644 index 0000000..9dd8460 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_light.png new file mode 100644 index 0000000..ac0f595 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png new file mode 100644 index 0000000..47aa8ad Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_download_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_download_light.png new file mode 100644 index 0000000..4454eda Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_download_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png new file mode 100644 index 0000000..8c3d713 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png new file mode 100644 index 0000000..5b35e18 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png new file mode 100644 index 0000000..1972507 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png new file mode 100644 index 0000000..43402e8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000..6b9aed2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000..19b4d48 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000..286f926 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000..ce19534 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000..f066ec8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_light.png new file mode 100644 index 0000000..c37459a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_dark.png new file mode 100644 index 0000000..4254414 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000..039e2cc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000..5c36b88 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_light.png new file mode 100644 index 0000000..c3cd678 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png new file mode 100644 index 0000000..8038e36 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_save_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_save_light.png new file mode 100644 index 0000000..a9e9a23 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_save_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png new file mode 100644 index 0000000..70196b4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png new file mode 100644 index 0000000..e9c75b8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_dark.png new file mode 100644 index 0000000..62ff8ff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_light.png new file mode 100644 index 0000000..f5287f0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_settings_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png new file mode 100644 index 0000000..df5bd1b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png new file mode 100644 index 0000000..1fa628b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_dark.png new file mode 100644 index 0000000..6d98f5b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000..5feeb31 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_social_person.png b/app/src/main/res/drawable-xxxhdpi/ic_social_person.png new file mode 100644 index 0000000..a7a7c91 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_social_person.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png new file mode 100644 index 0000000..5c8386d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png new file mode 100644 index 0000000..160e2f5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000..d1b9f4c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000..e56e5a1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000..28321f8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_offline_dark.png b/app/src/main/res/drawable-xxxhdpi/main_offline_dark.png new file mode 100644 index 0000000..3a4a38d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_offline_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_offline_light.png b/app/src/main/res/drawable-xxxhdpi/main_offline_light.png new file mode 100644 index 0000000..83d2a5c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_offline_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png new file mode 100644 index 0000000..649e4c0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_server_light.png b/app/src/main/res/drawable-xxxhdpi/main_select_server_light.png new file mode 100644 index 0000000..328bb64 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_select_server_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png new file mode 100644 index 0000000..093f950 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png new file mode 100644 index 0000000..49af1f8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_backward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_backward_dark.png new file mode 100644 index 0000000..9ce790c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_backward_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_backward_light.png b/app/src/main/res/drawable-xxxhdpi/media_backward_light.png new file mode 100644 index 0000000..b000b34 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_backward_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png new file mode 100644 index 0000000..f51569f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png new file mode 100644 index 0000000..0b0ec88 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_forward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_forward_dark.png new file mode 100644 index 0000000..10496b1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_forward_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_forward_light.png b/app/src/main/res/drawable-xxxhdpi/media_forward_light.png new file mode 100644 index 0000000..e70dec6 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_forward_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_pause_dark.png b/app/src/main/res/drawable-xxxhdpi/media_pause_dark.png new file mode 100644 index 0000000..74f5378 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_pause_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_pause_light.png b/app/src/main/res/drawable-xxxhdpi/media_pause_light.png new file mode 100644 index 0000000..ec0ce23 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_pause_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000..0bb7d11 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png new file mode 100644 index 0000000..92dea83 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_off_dark.png new file mode 100644 index 0000000..c852f0f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_off_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_off_light.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_off_light.png new file mode 100644 index 0000000..1fac4a3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_off_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_single_dark.png new file mode 100644 index 0000000..01ad0d9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_single_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_single_light.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_single_light.png new file mode 100644 index 0000000..0acc6c5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_repeat_single_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png new file mode 100644 index 0000000..e9f8df4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png new file mode 100644 index 0000000..339acda Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_start_dark.png b/app/src/main/res/drawable-xxxhdpi/media_start_dark.png new file mode 100644 index 0000000..ac20fff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_start_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_start_light.png b/app/src/main/res/drawable-xxxhdpi/media_start_light.png new file mode 100644 index 0000000..bddff57 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_start_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_stop_dark.png b/app/src/main/res/drawable-xxxhdpi/media_stop_dark.png new file mode 100644 index 0000000..4aa45ec Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_stop_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_stop_light.png b/app/src/main/res/drawable-xxxhdpi/media_stop_light.png new file mode 100644 index 0000000..fbb16bf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_stop_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_close_dark.png b/app/src/main/res/drawable-xxxhdpi/notification_close_dark.png new file mode 100644 index 0000000..42388bf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_close_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_close_light.png b/app/src/main/res/drawable-xxxhdpi/notification_close_light.png new file mode 100644 index 0000000..e40b9d8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_close_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playing_dark.png b/app/src/main/res/drawable-xxxhdpi/playing_dark.png new file mode 100644 index 0000000..d2fae59 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playing_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playing_light.png b/app/src/main/res/drawable-xxxhdpi/playing_light.png new file mode 100644 index 0000000..a12d143 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playing_light.png differ diff --git a/app/src/main/res/drawable/appwidget4x1_preview.png b/app/src/main/res/drawable/appwidget4x1_preview.png new file mode 100644 index 0000000..c026828 Binary files /dev/null and b/app/src/main/res/drawable/appwidget4x1_preview.png differ diff --git a/app/src/main/res/drawable/appwidget4x2_preview.png b/app/src/main/res/drawable/appwidget4x2_preview.png new file mode 100644 index 0000000..8d0ba26 Binary files /dev/null and b/app/src/main/res/drawable/appwidget4x2_preview.png differ diff --git a/app/src/main/res/drawable/appwidget4x3_preview.png b/app/src/main/res/drawable/appwidget4x3_preview.png new file mode 100644 index 0000000..00b0798 Binary files /dev/null and b/app/src/main/res/drawable/appwidget4x3_preview.png differ diff --git a/app/src/main/res/drawable/appwidget4x4_preview.png b/app/src/main/res/drawable/appwidget4x4_preview.png new file mode 100644 index 0000000..c3b4bb8 Binary files /dev/null and b/app/src/main/res/drawable/appwidget4x4_preview.png differ diff --git a/app/src/main/res/drawable/audinaut.png b/app/src/main/res/drawable/audinaut.png new file mode 100644 index 0000000..60eb9c2 Binary files /dev/null and b/app/src/main/res/drawable/audinaut.png differ diff --git a/app/src/main/res/drawable/card_rounded_corners_black.xml b/app/src/main/res/drawable/card_rounded_corners_black.xml new file mode 100644 index 0000000..7592de6 --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_black.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_dark.xml b/app/src/main/res/drawable/card_rounded_corners_dark.xml new file mode 100644 index 0000000..4db7d4b --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_dark.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_light.xml b/app/src/main/res/drawable/card_rounded_corners_light.xml new file mode 100644 index 0000000..5475c3d --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_light.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroller_bubble.xml b/app/src/main/res/drawable/fast_scroller_bubble.xml new file mode 100644 index 0000000..02dfee5 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroller_bubble.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroller_handle.xml b/app/src/main/res/drawable/fast_scroller_handle.xml new file mode 100644 index 0000000..e1744ce --- /dev/null +++ b/app/src/main/res/drawable/fast_scroller_handle.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_backward.xml b/app/src/main/res/drawable/notification_backward.xml new file mode 100644 index 0000000..f5fd965 --- /dev/null +++ b/app/src/main/res/drawable/notification_backward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_close.xml b/app/src/main/res/drawable/notification_close.xml new file mode 100644 index 0000000..67a5696 --- /dev/null +++ b/app/src/main/res/drawable/notification_close.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_divider.xml b/app/src/main/res/drawable/notification_divider.xml new file mode 100644 index 0000000..95d50aa --- /dev/null +++ b/app/src/main/res/drawable/notification_divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_fastforward.xml b/app/src/main/res/drawable/notification_fastforward.xml new file mode 100644 index 0000000..355c6a5 --- /dev/null +++ b/app/src/main/res/drawable/notification_fastforward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_forward.xml b/app/src/main/res/drawable/notification_forward.xml new file mode 100644 index 0000000..5dd1000 --- /dev/null +++ b/app/src/main/res/drawable/notification_forward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_pause.xml b/app/src/main/res/drawable/notification_pause.xml new file mode 100644 index 0000000..c71a997 --- /dev/null +++ b/app/src/main/res/drawable/notification_pause.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_rewind.xml b/app/src/main/res/drawable/notification_rewind.xml new file mode 100644 index 0000000..ab7827a --- /dev/null +++ b/app/src/main/res/drawable/notification_rewind.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_start.xml b/app/src/main/res/drawable/notification_start.xml new file mode 100644 index 0000000..b31b4f8 --- /dev/null +++ b/app/src/main/res/drawable/notification_start.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/download.xml b/app/src/main/res/layout-land/download.xml new file mode 100644 index 0000000..db1660e --- /dev/null +++ b/app/src/main/res/layout-land/download.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-large-land/abstract_fragment_container.xml b/app/src/main/res/layout-large-land/abstract_fragment_container.xml new file mode 100644 index 0000000..3901710 --- /dev/null +++ b/app/src/main/res/layout-large-land/abstract_fragment_container.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/download.xml b/app/src/main/res/layout-large-land/download.xml new file mode 100644 index 0000000..efe2ece --- /dev/null +++ b/app/src/main/res/layout-large-land/download.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-port/download.xml b/app/src/main/res/layout-port/download.xml new file mode 100644 index 0000000..322266e --- /dev/null +++ b/app/src/main/res/layout-port/download.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/abstract_activity.xml b/app/src/main/res/layout/abstract_activity.xml new file mode 100644 index 0000000..56db143 --- /dev/null +++ b/app/src/main/res/layout/abstract_activity.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/abstract_fragment_activity.xml b/app/src/main/res/layout/abstract_fragment_activity.xml new file mode 100644 index 0000000..a34509f --- /dev/null +++ b/app/src/main/res/layout/abstract_fragment_activity.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/abstract_fragment_container.xml b/app/src/main/res/layout/abstract_fragment_container.xml new file mode 100644 index 0000000..f13356c --- /dev/null +++ b/app/src/main/res/layout/abstract_fragment_container.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/abstract_recycler_fragment.xml b/app/src/main/res/layout/abstract_recycler_fragment.xml new file mode 100644 index 0000000..f4f6043 --- /dev/null +++ b/app/src/main/res/layout/abstract_recycler_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/actionbar_spinner.xml b/app/src/main/res/layout/actionbar_spinner.xml new file mode 100644 index 0000000..f719a67 --- /dev/null +++ b/app/src/main/res/layout/actionbar_spinner.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/album_cell_item.xml b/app/src/main/res/layout/album_cell_item.xml new file mode 100644 index 0000000..ff04f06 --- /dev/null +++ b/app/src/main/res/layout/album_cell_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/album_list_header.xml b/app/src/main/res/layout/album_list_header.xml new file mode 100644 index 0000000..e78d0ac --- /dev/null +++ b/app/src/main/res/layout/album_list_header.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/album_list_item.xml b/app/src/main/res/layout/album_list_item.xml new file mode 100644 index 0000000..5e4498f --- /dev/null +++ b/app/src/main/res/layout/album_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/appwidget4x1.xml b/app/src/main/res/layout/appwidget4x1.xml new file mode 100644 index 0000000..5f2536d --- /dev/null +++ b/app/src/main/res/layout/appwidget4x1.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/appwidget4x2.xml b/app/src/main/res/layout/appwidget4x2.xml new file mode 100644 index 0000000..ae61353 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x2.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/appwidget4x3.xml b/app/src/main/res/layout/appwidget4x3.xml new file mode 100644 index 0000000..0ffb9d4 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x3.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/appwidget4x4.xml b/app/src/main/res/layout/appwidget4x4.xml new file mode 100644 index 0000000..d0668c2 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x4.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_art_item.xml b/app/src/main/res/layout/basic_art_item.xml new file mode 100644 index 0000000..d29b93e --- /dev/null +++ b/app/src/main/res/layout/basic_art_item.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/basic_cell_item.xml b/app/src/main/res/layout/basic_cell_item.xml new file mode 100644 index 0000000..dcbb90e --- /dev/null +++ b/app/src/main/res/layout/basic_cell_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_choice_item.xml b/app/src/main/res/layout/basic_choice_item.xml new file mode 100644 index 0000000..e2dc220 --- /dev/null +++ b/app/src/main/res/layout/basic_choice_item.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_count_item.xml b/app/src/main/res/layout/basic_count_item.xml new file mode 100644 index 0000000..ce1aa80 --- /dev/null +++ b/app/src/main/res/layout/basic_count_item.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_header.xml b/app/src/main/res/layout/basic_header.xml new file mode 100644 index 0000000..b1f94b3 --- /dev/null +++ b/app/src/main/res/layout/basic_header.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_list_item.xml b/app/src/main/res/layout/basic_list_item.xml new file mode 100644 index 0000000..1e7db68 --- /dev/null +++ b/app/src/main/res/layout/basic_list_item.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/layout/cache_location_buttons.xml b/app/src/main/res/layout/cache_location_buttons.xml new file mode 100644 index 0000000..31e1264 --- /dev/null +++ b/app/src/main/res/layout/cache_location_buttons.xml @@ -0,0 +1,19 @@ + + + +