├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── xyz │ │ └── gianlu │ │ └── librespot │ │ └── android │ │ ├── LibrespotApp.java │ │ ├── LibrespotHolder.java │ │ ├── LoginActivity.java │ │ ├── MainActivity.java │ │ └── Utils.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_login.xml │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-night │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── backup_descriptor.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── librespot-android-decoder-tremolo ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── .gitignore │ ├── AndroidManifest.xml │ ├── java │ └── xyz │ │ └── gianlu │ │ └── librespot │ │ └── player │ │ └── decoders │ │ ├── TremoloVorbisDecoder.java │ │ └── tremolo │ │ ├── OggDecodingInputStream.java │ │ └── SeekableInputStream.java │ ├── jni │ ├── Android.mk │ ├── Application.mk │ └── libtremolo │ │ ├── Android.mk │ │ ├── asm_arm.h │ │ ├── bitwise.c │ │ ├── bitwiseARM.s │ │ ├── codebook.c │ │ ├── codebook.h │ │ ├── codec_internal.h │ │ ├── config_types.h │ │ ├── dpen.s │ │ ├── dsp.c │ │ ├── floor0.c │ │ ├── floor1.c │ │ ├── floor1ARM.s │ │ ├── floor_lookup.c │ │ ├── framing.c │ │ ├── hide.c │ │ ├── hide.h │ │ ├── ivorbiscodec.h │ │ ├── ivorbisfile.h │ │ ├── lsp_lookup.h │ │ ├── mapping0.c │ │ ├── md5.c │ │ ├── md5.h │ │ ├── mdct.c │ │ ├── mdct.h │ │ ├── mdctARM.s │ │ ├── mdct_lookup.h │ │ ├── misc.c │ │ ├── misc.h │ │ ├── ogg.h │ │ ├── os.h │ │ ├── os_types.h │ │ ├── res012.c │ │ ├── treminfo.c │ │ ├── tremolo-jni.c │ │ ├── vorbisfile.c │ │ └── window_lookup.h │ └── libs │ ├── arm64-v8a │ └── libtremolo.so │ └── armeabi-v7a │ └── libtremolo.so ├── librespot-android-decoder ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── xyz │ └── gianlu │ └── librespot │ └── player │ └── decoders │ └── AndroidNativeDecoder.java ├── librespot-android-sink ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── xyz │ │ └── gianlu │ │ └── librespot │ │ └── android │ │ └── sink │ │ ├── AudioStreamInstrumentedTest.java │ │ └── ToneGenerator.java │ └── main │ ├── AndroidManifest.xml │ └── java │ └── xyz │ └── gianlu │ └── librespot │ └── android │ └── sink │ └── AndroidSinkOutput.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /.idea 3 | .DS_Store 4 | /build 5 | /captures 6 | .cxx 7 | 8 | # Built application files 9 | *.apk 10 | *.aar 11 | *.ap_ 12 | *.aab 13 | 14 | # Files for the ART/Dalvik VM 15 | *.dex 16 | 17 | # Java class files 18 | *.class 19 | 20 | # Generated files 21 | bin/ 22 | gen/ 23 | out/ 24 | # Uncomment the following line in case you need and you don't have the release build type files in your app 25 | # release/ 26 | 27 | # Gradle files 28 | .gradle/ 29 | build/ 30 | 31 | # Local configuration file (sdk path, etc) 32 | local.properties 33 | 34 | # Proguard folder generated by Eclipse 35 | proguard/ 36 | 37 | # Log Files 38 | *.log 39 | 40 | # Android Studio Navigation editor temp files 41 | .navigation/ 42 | 43 | # Android Studio captures folder 44 | captures/ 45 | 46 | # IntelliJ 47 | *.iml 48 | .idea/workspace.xml 49 | .idea/tasks.xml 50 | .idea/gradle.xml 51 | .idea/assetWizardSettings.xml 52 | .idea/dictionaries 53 | .idea/libraries 54 | # Android Studio 3 in .gitignore file. 55 | .idea/caches 56 | .idea/modules.xml 57 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 58 | .idea/navEditor.xml 59 | 60 | # Keystore files 61 | # Uncomment the following lines if you do not want to check your keystore files in. 62 | #*.jks 63 | #*.keystore 64 | 65 | # External native build folder generated in Android Studio 2.2 and later 66 | .externalNativeBuild 67 | .cxx/ 68 | 69 | # Google Services (e.g. APIs or Firebase) 70 | # google-services.json 71 | 72 | # Freeline 73 | freeline.py 74 | freeline/ 75 | freeline_project_description.json 76 | 77 | # fastlane 78 | fastlane/report.xml 79 | fastlane/Preview.html 80 | fastlane/screenshots 81 | fastlane/test_output 82 | fastlane/readme.md 83 | 84 | # Version control 85 | vcs.xml 86 | 87 | # lint 88 | lint/intermediates/ 89 | lint/generated/ 90 | lint/outputs/ 91 | lint/tmp/ 92 | # lint/reports/ 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # librespot-android 2 | 3 | This is a demo application to demonstrate that it is possible to run [librespot-java](https://github.com/librespot-org/librespot-java) on an Android device. The app provides basic functionalities to login and then to play a custom URI, pause/resume, skip next and previous, but all features could be implemented. 4 | 5 | This repo also contains some useful modules that contain Android-compatible sinks and decoders that you might want to use in your app. 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 31 7 | buildToolsVersion "30.0.3" 8 | 9 | defaultConfig { 10 | applicationId "xyz.gianlu.librespot.android" 11 | minSdkVersion 23 12 | targetSdkVersion 31 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | buildFeatures { 25 | viewBinding true 26 | } 27 | 28 | compileOptions { 29 | // Flag to enable support for the new language APIs 30 | coreLibraryDesugaringEnabled true 31 | 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | packagingOptions { 37 | exclude 'log4j2.xml' 38 | exclude 'META-INF/DEPENDENCIES' 39 | } 40 | } 41 | 42 | dependencies { 43 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' 44 | 45 | implementation 'androidx.appcompat:appcompat:1.4.0' 46 | implementation 'com.google.android.material:material:1.4.0' 47 | 48 | implementation('xyz.gianlu.librespot:librespot-player:1.6.2:thin') { 49 | exclude group: 'xyz.gianlu.librespot', module: 'librespot-sink' 50 | exclude group: 'com.lmax', module: 'disruptor' 51 | exclude group: 'org.apache.logging.log4j' 52 | } 53 | 54 | implementation project(':librespot-android-decoder') 55 | implementation project(':librespot-android-decoder-tremolo') 56 | implementation project(':librespot-android-sink') 57 | implementation 'uk.uuid.slf4j:slf4j-android:1.7.30-0' 58 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/gianlu/librespot/android/LibrespotApp.java: -------------------------------------------------------------------------------- 1 | package xyz.gianlu.librespot.android; 2 | 3 | import android.app.Application; 4 | import android.os.Build; 5 | import android.util.Log; 6 | 7 | import xyz.gianlu.librespot.audio.decoders.Decoders; 8 | import xyz.gianlu.librespot.audio.format.SuperAudioFormat; 9 | import xyz.gianlu.librespot.player.decoders.AndroidNativeDecoder; 10 | import xyz.gianlu.librespot.player.decoders.TremoloVorbisDecoder; 11 | 12 | public final class LibrespotApp extends Application { 13 | private static final String TAG = LibrespotApp.class.getSimpleName(); 14 | 15 | static { 16 | Decoders.registerDecoder(SuperAudioFormat.VORBIS, 0, AndroidNativeDecoder.class); 17 | Decoders.registerDecoder(SuperAudioFormat.MP3, 0, AndroidNativeDecoder.class); 18 | 19 | if (isArm()) { 20 | Decoders.registerDecoder(SuperAudioFormat.VORBIS, 0, TremoloVorbisDecoder.class); 21 | Log.i(TAG, "Using ARM optimized Vorbis decoder"); 22 | } 23 | } 24 | 25 | private static boolean isArm() { 26 | for (String abi : Build.SUPPORTED_ABIS) 27 | if (abi.contains("arm")) 28 | return true; 29 | 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/gianlu/librespot/android/LibrespotHolder.java: -------------------------------------------------------------------------------- 1 | package xyz.gianlu.librespot.android; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.io.IOException; 8 | import java.lang.ref.WeakReference; 9 | 10 | import xyz.gianlu.librespot.core.Session; 11 | import xyz.gianlu.librespot.player.Player; 12 | 13 | public final class LibrespotHolder { 14 | private volatile static WeakReference session; 15 | private volatile static WeakReference player; 16 | 17 | private LibrespotHolder() { 18 | } 19 | 20 | public static void set(@NotNull Session session) { 21 | LibrespotHolder.session = new WeakReference<>(session); 22 | } 23 | 24 | public static void set(@NotNull Player player) { 25 | LibrespotHolder.player = new WeakReference<>(player); 26 | } 27 | 28 | public static void clear() { 29 | Session s = getSession(); 30 | Player p = getPlayer(); 31 | if (p != null || s != null) { 32 | new Thread(() -> { 33 | if (p != null) p.close(); 34 | 35 | try { 36 | if (s != null) s.close(); 37 | } catch (IOException ignored) { 38 | } 39 | }).start(); 40 | } 41 | 42 | player = null; 43 | session = null; 44 | } 45 | 46 | @Nullable 47 | public static Session getSession() { 48 | return session != null ? session.get() : null; 49 | } 50 | 51 | @Nullable 52 | public static Player getPlayer() { 53 | return player != null ? player.get() : null; 54 | } 55 | 56 | public static boolean hasSession() { 57 | return getSession() != null; 58 | } 59 | 60 | public static boolean hasPlayer() { 61 | return getPlayer() != null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/gianlu/librespot/android/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package xyz.gianlu.librespot.android; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.util.Log; 8 | import android.widget.Toast; 9 | 10 | import androidx.annotation.UiThread; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | 13 | import com.spotify.connectstate.Connect; 14 | 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.security.GeneralSecurityException; 20 | import java.util.Locale; 21 | 22 | import xyz.gianlu.librespot.android.databinding.ActivityLoginBinding; 23 | import xyz.gianlu.librespot.core.Session; 24 | import xyz.gianlu.librespot.mercury.MercuryClient; 25 | 26 | public final class LoginActivity extends AppCompatActivity { 27 | private static final String TAG = LoginActivity.class.getSimpleName(); 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | ActivityLoginBinding binding = ActivityLoginBinding.inflate(getLayoutInflater()); 33 | setContentView(binding.getRoot()); 34 | 35 | LoginCallback callback = new LoginCallback() { 36 | @Override 37 | public void loggedIn() { 38 | Toast.makeText(LoginActivity.this, R.string.loggedIn, Toast.LENGTH_SHORT).show(); 39 | startActivity(new Intent(LoginActivity.this, MainActivity.class) 40 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); 41 | } 42 | 43 | @Override 44 | public void failedLoggingIn(@NotNull Exception ex) { 45 | Toast.makeText(LoginActivity.this, R.string.failedLoggingIn, Toast.LENGTH_SHORT).show(); 46 | } 47 | }; 48 | 49 | binding.login.setOnClickListener(v -> { 50 | String username = Utils.getText(binding.username); 51 | String password = Utils.getText(binding.password); 52 | if (username.isEmpty() || password.isEmpty()) 53 | return; 54 | 55 | File credentialsFile = Utils.getCredentialsFile(this); 56 | new LoginThread(username, password, credentialsFile, callback).start(); 57 | }); 58 | } 59 | 60 | @UiThread 61 | private interface LoginCallback { 62 | void loggedIn(); 63 | 64 | void failedLoggingIn(@NotNull Exception ex); 65 | } 66 | 67 | private static class LoginThread extends Thread { 68 | private final String username; 69 | private final String password; 70 | private final File credentialsFile; 71 | private final LoginCallback callback; 72 | private final Handler handler; 73 | 74 | LoginThread(String username, String password, File credentialsFile, LoginCallback callback) { 75 | this.username = username; 76 | this.password = password; 77 | this.credentialsFile = credentialsFile; 78 | this.callback = callback; 79 | this.handler = new Handler(Looper.getMainLooper()); 80 | } 81 | 82 | @Override 83 | public void run() { 84 | try { 85 | Session.Configuration conf = new Session.Configuration.Builder() 86 | .setStoreCredentials(true) 87 | .setStoredCredentialsFile(credentialsFile) 88 | .setCacheEnabled(false) 89 | .build(); 90 | 91 | Session.Builder builder = new Session.Builder(conf) 92 | .setPreferredLocale(Locale.getDefault().getLanguage()) 93 | .setDeviceType(Connect.DeviceType.SMARTPHONE) 94 | .setDeviceId(null).setDeviceName("librespot-android"); 95 | 96 | Session session = builder.userPass(username, password).create(); 97 | Log.i(TAG, "Logged in as: " + session.username()); 98 | 99 | LibrespotHolder.set(session); 100 | 101 | handler.post(callback::loggedIn); 102 | } catch (IOException | 103 | GeneralSecurityException | 104 | Session.SpotifyAuthenticationException | 105 | MercuryClient.MercuryException ex) { 106 | Log.e(TAG, "Session creation failed!", ex); 107 | handler.post(() -> callback.failedLoggingIn(ex)); 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java: -------------------------------------------------------------------------------- 1 | package xyz.gianlu.librespot.android; 2 | 3 | import android.content.Intent; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.widget.Toast; 11 | 12 | import androidx.annotation.Nullable; 13 | import androidx.annotation.UiThread; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import com.spotify.connectstate.Connect; 17 | 18 | import org.jetbrains.annotations.NotNull; 19 | 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.security.GeneralSecurityException; 23 | import java.util.Locale; 24 | import java.util.concurrent.ExecutorService; 25 | import java.util.concurrent.Executors; 26 | 27 | import xyz.gianlu.librespot.android.databinding.ActivityMainBinding; 28 | import xyz.gianlu.librespot.android.sink.AndroidSinkOutput; 29 | import xyz.gianlu.librespot.core.Session; 30 | import xyz.gianlu.librespot.mercury.MercuryClient; 31 | import xyz.gianlu.librespot.player.Player; 32 | import xyz.gianlu.librespot.player.PlayerConfiguration; 33 | 34 | public final class MainActivity extends AppCompatActivity { 35 | private static final String TAG = MainActivity.class.getSimpleName(); 36 | private final ExecutorService executorService = Executors.newSingleThreadExecutor(); 37 | 38 | @Override 39 | protected void onDestroy() { 40 | super.onDestroy(); 41 | LibrespotHolder.clear(); 42 | } 43 | 44 | @Override 45 | protected void onCreate(@Nullable Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); 48 | setContentView(binding.getRoot()); 49 | 50 | File credentialsFile = Utils.getCredentialsFile(this); 51 | if (!credentialsFile.exists() || !credentialsFile.canRead()) { 52 | startActivity(new Intent(this, LoginActivity.class) 53 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); 54 | return; 55 | } 56 | 57 | binding.logout.setOnClickListener(v -> { 58 | credentialsFile.delete(); 59 | LibrespotHolder.clear(); 60 | startActivity(new Intent(MainActivity.this, LoginActivity.class) 61 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); 62 | }); 63 | 64 | binding.play.setOnClickListener((v) -> { 65 | String playUri = Utils.getText(binding.playUri); 66 | if (playUri.isEmpty()) 67 | return; 68 | 69 | executorService.execute(new PlayRunnable(playUri, () -> Toast.makeText(MainActivity.this, R.string.playbackStarted, Toast.LENGTH_SHORT).show())); 70 | }); 71 | 72 | binding.resume.setOnClickListener((v) -> 73 | executorService.execute(new ResumeRunnable(() -> 74 | Toast.makeText(this, R.string.resumed, Toast.LENGTH_SHORT).show()))); 75 | 76 | binding.pause.setOnClickListener((v) -> 77 | executorService.execute(new PauseRunnable(() -> 78 | Toast.makeText(this, R.string.paused, Toast.LENGTH_SHORT).show()))); 79 | 80 | binding.prev.setOnClickListener((v) -> 81 | executorService.execute(new PrevRunnable(() -> 82 | Toast.makeText(this, R.string.skippedPrev, Toast.LENGTH_SHORT).show()))); 83 | 84 | binding.next.setOnClickListener((v) -> 85 | executorService.execute(new NextRunnable(() -> 86 | Toast.makeText(this, R.string.skippedNext, Toast.LENGTH_SHORT).show()))); 87 | 88 | executorService.submit(new SetupRunnable(credentialsFile, new SetupCallback() { 89 | @Override 90 | public void playerReady(@NotNull String username) { 91 | Toast.makeText(MainActivity.this, R.string.playerReady, Toast.LENGTH_SHORT).show(); 92 | binding.username.setText(username); 93 | binding.playControls.setVisibility(View.VISIBLE); 94 | } 95 | 96 | @Override 97 | public void notLoggedIn() { 98 | startActivity(new Intent(MainActivity.this, LoginActivity.class) 99 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); 100 | } 101 | 102 | @Override 103 | public void failedGettingReady(@NotNull Exception ex) { 104 | Toast.makeText(MainActivity.this, R.string.somethingWentWrong, Toast.LENGTH_SHORT).show(); 105 | binding.playControls.setVisibility(View.GONE); 106 | } 107 | })); 108 | } 109 | 110 | @UiThread 111 | private interface SetupCallback { 112 | void playerReady(@NotNull String username); 113 | 114 | void notLoggedIn(); 115 | 116 | void failedGettingReady(@NotNull Exception ex); 117 | } 118 | 119 | private interface SimpleCallback { 120 | void done(); 121 | } 122 | 123 | private static class SetupRunnable implements Runnable { 124 | private final File credentialsFile; 125 | private final SetupCallback callback; 126 | private final Handler handler; 127 | 128 | SetupRunnable(@NotNull File credentialsFile, @NotNull SetupCallback callback) { 129 | this.credentialsFile = credentialsFile; 130 | this.callback = callback; 131 | this.handler = new Handler(Looper.getMainLooper()); 132 | } 133 | 134 | @Override 135 | public void run() { 136 | Session session; 137 | if (LibrespotHolder.hasSession()) { 138 | session = LibrespotHolder.getSession(); 139 | if (session == null) throw new IllegalStateException(); 140 | } else if (credentialsFile.exists() && credentialsFile.canRead()) { 141 | try { 142 | Session.Configuration conf = new Session.Configuration.Builder() 143 | .setStoreCredentials(true) 144 | .setStoredCredentialsFile(credentialsFile) 145 | .setCacheEnabled(false) 146 | .build(); 147 | 148 | Session.Builder builder = new Session.Builder(conf) 149 | .setPreferredLocale(Locale.getDefault().getLanguage()) 150 | .setDeviceType(Connect.DeviceType.SMARTPHONE) 151 | .setDeviceId(null).setDeviceName("librespot-android"); 152 | 153 | session = builder.stored(credentialsFile).create(); 154 | Log.i(TAG, "Logged in as: " + session.username()); 155 | 156 | LibrespotHolder.set(session); 157 | } catch (IOException | 158 | GeneralSecurityException | 159 | Session.SpotifyAuthenticationException | 160 | MercuryClient.MercuryException ex) { 161 | Log.e(TAG, "Session creation failed!", ex); 162 | handler.post(() -> callback.failedGettingReady(ex)); 163 | return; 164 | } 165 | } else { 166 | handler.post(callback::notLoggedIn); 167 | return; 168 | } 169 | 170 | Player player; 171 | if (LibrespotHolder.hasPlayer()) { 172 | player = LibrespotHolder.getPlayer(); 173 | if (player == null) throw new IllegalStateException(); 174 | } else { 175 | PlayerConfiguration configuration = new PlayerConfiguration.Builder() 176 | .setOutput(PlayerConfiguration.AudioOutput.CUSTOM) 177 | .setOutputClass(AndroidSinkOutput.class.getName()) 178 | .build(); 179 | 180 | player = new Player(configuration, session); 181 | LibrespotHolder.set(player); 182 | } 183 | 184 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { 185 | while (!player.isReady()) { 186 | try { 187 | //noinspection BusyWait 188 | Thread.sleep(100); 189 | } catch (InterruptedException ex) { 190 | return; 191 | } 192 | } 193 | } else { 194 | try { 195 | player.waitReady(); 196 | } catch (InterruptedException ex) { 197 | LibrespotHolder.clear(); 198 | return; 199 | } 200 | } 201 | 202 | handler.post(() -> callback.playerReady(session.username())); 203 | } 204 | } 205 | 206 | private static class PlayRunnable implements Runnable { 207 | private final String playUri; 208 | private final SimpleCallback callback; 209 | private final Handler handler = new Handler(Looper.getMainLooper()); 210 | 211 | PlayRunnable(@NotNull String playUri, @NotNull SimpleCallback callback) { 212 | this.playUri = playUri; 213 | this.callback = callback; 214 | } 215 | 216 | @Override 217 | public void run() { 218 | Player player = LibrespotHolder.getPlayer(); 219 | if (player == null) return; 220 | 221 | player.load(playUri, true, false); 222 | handler.post(callback::done); 223 | } 224 | } 225 | 226 | private static class ResumeRunnable implements Runnable { 227 | private final SimpleCallback callback; 228 | private final Handler handler = new Handler(Looper.getMainLooper()); 229 | 230 | ResumeRunnable(@NotNull SimpleCallback callback) { 231 | this.callback = callback; 232 | } 233 | 234 | @Override 235 | public void run() { 236 | Player player = LibrespotHolder.getPlayer(); 237 | if (player == null) return; 238 | 239 | player.play(); 240 | handler.post(callback::done); 241 | } 242 | } 243 | 244 | private static class PauseRunnable implements Runnable { 245 | private final SimpleCallback callback; 246 | private final Handler handler = new Handler(Looper.getMainLooper()); 247 | 248 | PauseRunnable(@NotNull SimpleCallback callback) { 249 | this.callback = callback; 250 | } 251 | 252 | @Override 253 | public void run() { 254 | Player player = LibrespotHolder.getPlayer(); 255 | if (player == null) return; 256 | 257 | player.pause(); 258 | handler.post(callback::done); 259 | } 260 | } 261 | 262 | private static class PrevRunnable implements Runnable { 263 | private final SimpleCallback callback; 264 | private final Handler handler = new Handler(Looper.getMainLooper()); 265 | 266 | PrevRunnable(@NotNull SimpleCallback callback) { 267 | this.callback = callback; 268 | } 269 | 270 | @Override 271 | public void run() { 272 | Player player = LibrespotHolder.getPlayer(); 273 | if (player == null) return; 274 | 275 | player.previous(); 276 | handler.post(callback::done); 277 | } 278 | } 279 | 280 | private static class NextRunnable implements Runnable { 281 | private final SimpleCallback callback; 282 | private final Handler handler = new Handler(Looper.getMainLooper()); 283 | 284 | NextRunnable(@NotNull SimpleCallback callback) { 285 | this.callback = callback; 286 | } 287 | 288 | @Override 289 | public void run() { 290 | Player player = LibrespotHolder.getPlayer(); 291 | if (player == null) return; 292 | 293 | player.next(); 294 | handler.post(callback::done); 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/gianlu/librespot/android/Utils.java: -------------------------------------------------------------------------------- 1 | package xyz.gianlu.librespot.android; 2 | 3 | import android.content.Context; 4 | import android.widget.EditText; 5 | 6 | import com.google.android.material.textfield.TextInputLayout; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.File; 11 | 12 | public final class Utils { 13 | 14 | private Utils() { 15 | } 16 | 17 | @NotNull 18 | public static String getText(@NotNull TextInputLayout layout) { 19 | EditText editText = layout.getEditText(); 20 | if (editText == null) throw new IllegalStateException(); 21 | return editText.getText().toString(); 22 | } 23 | 24 | @NotNull 25 | public static File getCredentialsFile(@NotNull Context context) { 26 | return new File(context.getCacheDir(), "credentials.json"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 19 | 20 | 21 | 27 | 28 | 32 | 33 | 34 |