From 6721500202fc092ca526f3f641fad13efb51ef97 Mon Sep 17 00:00:00 2001 From: Nite Date: Fri, 18 Sep 2020 09:37:19 +0200 Subject: [PATCH 1/8] Updated Server Settings UI and Storage Updated Koin to latest --- .../api/subsonic/di/SubsonicApiModule.kt | 6 +- dependencies.gradle | 25 +- ultrasonic/build.gradle | 10 +- ultrasonic/src/main/AndroidManifest.xml | 7 +- .../ultrasonic/activity/BookmarkActivity.java | 5 +- .../ultrasonic/activity/ChatActivity.java | 11 +- .../ultrasonic/activity/DownloadActivity.java | 9 +- .../activity/EqualizerActivity.java | 2 +- .../ultrasonic/activity/HelpActivity.java | 3 +- .../ultrasonic/activity/MainActivity.java | 242 ++----------- .../ultrasonic/activity/SearchActivity.java | 5 +- .../activity/SelectAlbumActivity.java | 13 +- .../activity/SelectArtistActivity.java | 33 +- .../activity/SelectPlaylistActivity.java | 7 +- .../activity/ServerSettingsActivity.java | 63 ---- .../activity/SubsonicTabActivity.java | 17 +- .../fragment/ServerSettingsFragment.java | 321 ----------------- .../ultrasonic/fragment/SettingsFragment.java | 76 +--- .../receiver/A2dpIntentReceiver.java | 2 +- .../receiver/MediaButtonIntentReceiver.java | 2 +- .../ultrasonic/service/AudioFocusHandler.java | 2 +- .../service/CachedMusicService.java | 7 +- .../ultrasonic/service/DownloadFile.java | 2 +- .../moire/ultrasonic/service/Downloader.java | 2 +- .../service/JukeboxMediaPlayer.java | 5 +- .../ultrasonic/service/LocalMediaPlayer.java | 3 +- .../service/MediaPlayerControllerImpl.java | 11 +- .../service/MediaPlayerService.java | 4 +- .../service/OfflineMusicService.java | 11 +- .../ultrasonic/service/RESTMusicService.java | 12 +- .../moire/ultrasonic/service/Scrobbler.java | 4 +- .../moire/ultrasonic/util/CacheCleaner.java | 6 +- .../org/moire/ultrasonic/util/Constants.java | 13 +- .../ultrasonic/util/ShufflePlayBuffer.java | 7 +- .../java/org/moire/ultrasonic/util/Util.java | 238 ++----------- .../org/moire/ultrasonic/view/AlbumView.java | 3 +- .../moire/ultrasonic/view/ChatAdapter.java | 9 +- .../org/moire/ultrasonic/view/SongView.java | 47 +-- .../moire/ultrasonic/view/VisualizerView.java | 2 +- .../ultrasonic/activity/EditServerActivity.kt | 327 ++++++++++++++++++ .../ultrasonic/activity/ServerRowAdapter.kt | 202 +++++++++++ .../activity/ServerSelectorActivity.kt | 186 ++++++++++ .../activity/ServerSettingsModel.kt | 203 +++++++++++ .../kotlin/org/moire/ultrasonic/app/UApp.kt | 22 +- .../ultrasonic/data/ActiveServerProvider.kt | 167 +++++++++ .../org/moire/ultrasonic/data/AppDatabase.kt | 16 + .../moire/ultrasonic/data/ServerSetting.kt | 39 +++ .../moire/ultrasonic/data/ServerSettingDao.kt | 57 +++ .../di/AppPermanentStorageModule.kt | 25 +- .../moire/ultrasonic/di/BaseNetworkModule.kt | 2 +- .../org/moire/ultrasonic/di/DiProperties.kt | 5 - .../moire/ultrasonic/di/DirectoriesModule.kt | 3 +- .../moire/ultrasonic/di/FeatureFlagsModule.kt | 5 +- .../moire/ultrasonic/di/MediaPlayerModule.kt | 2 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 102 ++---- .../ultrasonic/service/MusicServiceFactory.kt | 23 +- .../main/res/drawable-hdpi/ic_add_white.png | Bin 0 -> 1647 bytes .../res/drawable-hdpi/ic_more_vert_dark.png | Bin 0 -> 3034 bytes .../res/drawable-hdpi/ic_more_vert_light.png | Bin 0 -> 2196 bytes .../main/res/drawable-ldpi/ic_add_white.png | Bin 0 -> 2450 bytes .../res/drawable-ldpi/ic_more_vert_dark.png | Bin 0 -> 2276 bytes .../res/drawable-ldpi/ic_more_vert_light.png | Bin 0 -> 1794 bytes .../main/res/drawable-mdpi/ic_add_white.png | Bin 0 -> 1505 bytes .../res/drawable-mdpi/ic_more_vert_dark.png | Bin 0 -> 1559 bytes .../res/drawable-mdpi/ic_more_vert_light.png | Bin 0 -> 1524 bytes .../main/res/drawable-xhdpi/ic_add_white.png | Bin 0 -> 1791 bytes .../res/drawable-xhdpi/ic_more_vert_dark.png | Bin 0 -> 1634 bytes .../res/drawable-xhdpi/ic_more_vert_light.png | Bin 0 -> 1592 bytes .../main/res/drawable-xxhdpi/ic_add_white.png | Bin 0 -> 1786 bytes .../res/drawable-xxhdpi/ic_more_vert_dark.png | Bin 0 -> 1799 bytes .../drawable-xxhdpi/ic_more_vert_light.png | Bin 0 -> 1761 bytes .../src/main/res/drawable/default_ripple.xml | 19 + .../list_selector_holo_dark_selected.xml | 28 ++ .../list_selector_holo_light_selected.xml | 29 ++ .../src/main/res/drawable/select_ripple.xml | 19 + .../res/drawable/select_ripple_circle.xml | 19 + .../src/main/res/layout/server_edit.xml | 225 ++++++++++++ ultrasonic/src/main/res/layout/server_row.xml | 51 +++ .../src/main/res/layout/server_selector.xml | 24 ++ ultrasonic/src/main/res/values-de/strings.xml | 17 +- ultrasonic/src/main/res/values-es/strings.xml | 17 +- ultrasonic/src/main/res/values-fr/strings.xml | 17 +- ultrasonic/src/main/res/values-hu/strings.xml | 17 +- ultrasonic/src/main/res/values-nl/strings.xml | 17 +- ultrasonic/src/main/res/values-pl/strings.xml | 17 +- .../src/main/res/values-pt-rBR/strings.xml | 17 +- ultrasonic/src/main/res/values-pt/strings.xml | 17 +- ultrasonic/src/main/res/values/colors.xml | 2 + ultrasonic/src/main/res/values/strings.xml | 17 +- ultrasonic/src/main/res/values/styles.xml | 6 +- ultrasonic/src/main/res/values/themes.xml | 8 + .../src/main/res/xml/server_settings.xml | 69 ---- 92 files changed, 2113 insertions(+), 1172 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/activity/ServerSettingsActivity.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ServerSettingsFragment.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DiProperties.kt create mode 100644 ultrasonic/src/main/res/drawable-hdpi/ic_add_white.png create mode 100644 ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_dark.png create mode 100644 ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_light.png create mode 100644 ultrasonic/src/main/res/drawable-ldpi/ic_add_white.png create mode 100644 ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_dark.png create mode 100644 ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_light.png create mode 100644 ultrasonic/src/main/res/drawable-mdpi/ic_add_white.png create mode 100644 ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_dark.png create mode 100644 ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_light.png create mode 100644 ultrasonic/src/main/res/drawable-xhdpi/ic_add_white.png create mode 100644 ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_dark.png create mode 100644 ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_light.png create mode 100644 ultrasonic/src/main/res/drawable-xxhdpi/ic_add_white.png create mode 100644 ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_dark.png create mode 100644 ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_light.png create mode 100644 ultrasonic/src/main/res/drawable/default_ripple.xml create mode 100644 ultrasonic/src/main/res/drawable/list_selector_holo_dark_selected.xml create mode 100644 ultrasonic/src/main/res/drawable/list_selector_holo_light_selected.xml create mode 100644 ultrasonic/src/main/res/drawable/select_ripple.xml create mode 100644 ultrasonic/src/main/res/drawable/select_ripple_circle.xml create mode 100644 ultrasonic/src/main/res/layout/server_edit.xml create mode 100644 ultrasonic/src/main/res/layout/server_row.xml create mode 100644 ultrasonic/src/main/res/layout/server_selector.xml delete mode 100644 ultrasonic/src/main/res/xml/server_settings.xml diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt index f5820cb7..b56dc097 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt @@ -1,10 +1,8 @@ package org.moire.ultrasonic.api.subsonic.di -import org.koin.dsl.context.ModuleDefinition +import org.koin.dsl.module import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -const val SUBSONIC_API_CLIENT_CONTEXT = "SubsonicApiClientContext" - -fun ModuleDefinition.subsonicApiModule() = module(SUBSONIC_API_CLIENT_CONTEXT) { +val subsonicApiModule = module { single { SubsonicAPIClient(get(), get()) } } diff --git a/dependencies.gradle b/dependencies.gradle index 0aa4162d..66193268 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -12,17 +12,20 @@ ext.versions = [ androidSupport : "28.0.0", androidLegacySupport : "1.0.0", - androidSupportDesign : "1.0.0", + androidSupportDesign : "1.2.1", + constraintLayout : "2.0.1", multidex : "2.0.1", - + room : "2.2.5", kotlin : "1.3.72", + kotlinxCoroutines : "1.3.9", + viewModelKtx : "2.2.0", retrofit : "2.4.0", jackson : "2.9.5", okhttp : "3.10.0", semver : "1.0.0", twitterSerial : "0.1.6", - koin : "1.0.0-beta-3", + koin : "2.1.6", picasso : "2.71828", junit4 : "4.12", @@ -45,15 +48,21 @@ ext.gradlePlugins = [ ] ext.androidSupport = [ - support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport", - design : "com.google.android.material:material:$versions.androidSupportDesign", - annotations: "com.android.support:support-annotations:$versions.androidSupport", - multidex : "androidx.multidex:multidex:$versions.multidex", + support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport", + design : "com.google.android.material:material:$versions.androidSupportDesign", + annotations : "com.android.support:support-annotations:$versions.androidSupport", + multidex : "androidx.multidex:multidex:$versions.multidex", + constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout", + room : "androidx.room:room-compiler:$versions.room", + roomRuntime : "androidx.room:room-runtime:$versions.room", + roomKtx : "androidx.room:room-ktx:$versions.room", + viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx" ] ext.other = [ kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin", kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin", + kotlinxCoroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlinxCoroutines", retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit", gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit", jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit", @@ -62,8 +71,8 @@ ext.other = [ semver : "net.swiftzer.semver:semver:$versions.semver", twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", koinCore : "org.koin:koin-core:$versions.koin", - koinJava : "org.koin:koin-java:$versions.koin", koinAndroid : "org.koin:koin-android:$versions.koin", + koinViewModel : "org.koin:koin-android-viewmodel:$versions.koin", picasso : "com.squareup.picasso:picasso:$versions.picasso", dexter : "com.karumi:dexter:$versions.dexter", ] diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index f70422f9..3c7c07b1 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' apply from: "../gradle_scripts/code_quality.gradle" @@ -66,10 +67,17 @@ dependencies { implementation androidSupport.support implementation androidSupport.design implementation androidSupport.multidex + implementation androidSupport.roomRuntime + implementation androidSupport.roomKtx + implementation androidSupport.viewModelKtx + implementation androidSupport.constraintLayout implementation other.kotlinStdlib + implementation other.kotlinxCoroutines implementation other.koinAndroid - implementation other.koinJava + implementation other.koinViewModel + + kapt androidSupport.room testImplementation other.kotlinReflect testImplementation testing.junit diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 93d6ce3e..14db75f5 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -115,7 +115,12 @@ android:resource="@xml/searchable"/> - + + pinnedCount) ? View.VISIBLE : View.GONE); + pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE); unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE); - downloadButton.setVisibility(enabled && !deleteEnabled && !Util.isOffline(this) ? View.VISIBLE : View.GONE); + downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(this) ? View.VISIBLE : View.GONE); deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java index 2e4bf1c8..a7c9aa6f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java @@ -18,7 +18,9 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode; import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener; import com.handmark.pulltorefresh.library.PullToRefreshListView; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.ChatMessage; +import org.moire.ultrasonic.service.JukeboxMediaPlayer; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.BackgroundTask; @@ -32,6 +34,10 @@ import java.util.List; import java.util.Timer; import java.util.TimerTask; +import kotlin.Lazy; + +import static org.koin.java.KoinJavaComponent.inject; + /** * @author Joshua Bahnsen */ @@ -44,6 +50,7 @@ public final class ChatActivity extends SubsonicTabActivity private Timer timer; private volatile static Long lastChatMessageTime = (long) 0; private volatile static ArrayList messageList = new ArrayList(); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); @Override protected void onCreate(Bundle bundle) @@ -71,8 +78,8 @@ public final class ChatActivity extends SubsonicTabActivity chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); chatListView.setStackFromBottom(true); - String serverName = Util.getServerName(this, Util.getActiveServer(this)); - String userName = Util.getUserName(this, Util.getActiveServer(this)); + String serverName = activeServerProvider.getValue().getActiveServer().getName(); + String userName = activeServerProvider.getValue().getActiveServer().getUserName(); String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName); setActionBarSubtitle(title); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java index 4b6369a9..65fb14b0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java @@ -51,8 +51,9 @@ import android.widget.ViewFlipper; import com.mobeta.android.dslv.DragSortListView; -import org.koin.java.standalone.KoinJavaComponent; +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; @@ -743,7 +744,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi MenuItem bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete); - if (Util.isOffline(this)) + if (ActiveServerProvider.Companion.isOffline(this)) { if (shareMenuItem != null) { @@ -870,7 +871,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } } - if (Util.isOffline(this) || !Util.getShouldUseId3Tags(this)) + if (ActiveServerProvider.Companion.isOffline(this) || !Util.getShouldUseId3Tags(this)) { MenuItem menuItem = menu.findItem(R.id.menu_show_artist); @@ -880,7 +881,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } } - if (Util.isOffline(this)) + if (ActiveServerProvider.Companion.isOffline(this)) { MenuItem menuItem = menu.findItem(R.id.menu_lyrics); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java index 07b48a96..51c39876 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java @@ -39,7 +39,7 @@ import java.util.Map; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * Equalizer controls. diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/HelpActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/HelpActivity.java index a664899f..d2732909 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/HelpActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/HelpActivity.java @@ -36,6 +36,7 @@ import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; @@ -155,7 +156,7 @@ public final class HelpActivity extends ResultActivity implements OnClickListene { super.onPostCreate(bundle); - int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE; + int visibility = ActiveServerProvider.Companion.isOffline(this) ? View.GONE : View.VISIBLE; chatMenuItem.setVisibility(visibility); bookmarksMenuItem.setVisibility(visibility); sharesMenuItem.setVisibility(visibility); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index 66297dba..3c1d9427 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; -import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -34,7 +33,7 @@ import android.widget.ListView; import android.widget.TextView; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.service.MediaPlayerController; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -49,28 +48,18 @@ import java.util.Collections; import kotlin.Lazy; import static java.util.Arrays.asList; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel; +import static org.koin.java.KoinJavaComponent.inject; public class MainActivity extends SubsonicTabActivity { - - private static final int MENU_GROUP_SERVER = 10; - private static final int MENU_ITEM_OFFLINE = 111; - private static final int MENU_ITEM_SERVER_1 = 101; - private static final int MENU_ITEM_SERVER_2 = 102; - private static final int MENU_ITEM_SERVER_3 = 103; - private static final int MENU_ITEM_SERVER_4 = 104; - private static final int MENU_ITEM_SERVER_5 = 105; - private static final int MENU_ITEM_SERVER_6 = 106; - private static final int MENU_ITEM_SERVER_7 = 107; - private static final int MENU_ITEM_SERVER_8 = 108; - private static final int MENU_ITEM_SERVER_9 = 109; - private static final int MENU_ITEM_SERVER_10 = 110; - private static boolean infoDialogDisplayed; private static boolean shouldUseId3; + private static int lastActiveServer; private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); + private Lazy serverSettingsModel = viewModel(this, ServerSettingsModel.class); /** * Called when the activity is first created. @@ -105,7 +94,7 @@ public class MainActivity extends SubsonicTabActivity final View buttons = LayoutInflater.from(this).inflate(R.layout.main_buttons, null); final View serverButton = buttons.findViewById(R.id.main_select_server); - final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2); + final TextView serverTextView = serverButton.findViewById(R.id.main_select_server_2); final View musicTitle = buttons.findViewById(R.id.main_music); final View artistsButton = buttons.findViewById(R.id.main_artists_button); final View albumsButton = buttons.findViewById(R.id.main_albums_button); @@ -124,35 +113,18 @@ public class MainActivity extends SubsonicTabActivity final View albumsAlphaByNameButton = buttons.findViewById(R.id.main_albums_alphaByName); final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist); final View videosButton = buttons.findViewById(R.id.main_videos); - final View dummyView = findViewById(R.id.main_dummy); - boolean shouldShowDialog = false; - - if (!getActiveServerEnabled()) - { - shouldShowDialog = true; - Util.setActiveServer(this, 0); - } - - int instance = Util.getActiveServer(this); - String name = Util.getServerName(this, instance); - - if (name == null) - { - shouldShowDialog = true; - Util.setActiveServer(this, 0); - instance = Util.getActiveServer(this); - name = Util.getServerName(this, instance); - } + lastActiveServer = ActiveServerProvider.Companion.getActiveServerId(this); + String name = activeServerProvider.getValue().getActiveServer().getName(); serverTextView.setText(name); - final ListView list = (ListView) findViewById(R.id.main_list); + final ListView list = findViewById(R.id.main_list); final MergeAdapter adapter = new MergeAdapter(); adapter.addViews(Collections.singletonList(serverButton), true); - if (!Util.isOffline(this)) + if (!ActiveServerProvider.Companion.isOffline(this)) { adapter.addView(musicTitle, false); adapter.addViews(asList(artistsButton, albumsButton, genresButton), true); @@ -180,7 +152,6 @@ public class MainActivity extends SubsonicTabActivity } list.setAdapter(adapter); - registerForContextMenu(dummyView); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @@ -189,7 +160,7 @@ public class MainActivity extends SubsonicTabActivity { if (view == serverButton) { - dummyView.showContextMenu(); + showServers(); } else if (view == albumsNewestButton) { @@ -259,6 +230,9 @@ public class MainActivity extends SubsonicTabActivity // Remember the current theme. theme = Util.getTheme(this); + boolean shouldShowDialog = Util.shouldShowWelcomeScreen(this); + // This will convert the server settings from the Preferences to the DB + if (shouldShowDialog) serverSettingsModel.getValue().getServerList(); showInfoDialog(shouldShowDialog); } @@ -279,14 +253,24 @@ public class MainActivity extends SubsonicTabActivity protected void onResume() { super.onResume(); + boolean shouldRestart = false; boolean id3 = Util.getShouldUseId3Tags(MainActivity.this); + int currentActiveServer = ActiveServerProvider.Companion.getActiveServerId(MainActivity.this); if (id3 != shouldUseId3) { shouldUseId3 = id3; - restart(); + shouldRestart = true; } + + if (currentActiveServer != lastActiveServer) + { + lastActiveServer = currentActiveServer; + shouldRestart = true; + } + + if (shouldRestart) restart(); } @Override @@ -299,148 +283,6 @@ public class MainActivity extends SubsonicTabActivity return true; } - @Override - public void onCreateContextMenu(final ContextMenu menu, final View view, final ContextMenu.ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - - final int activeServer = Util.getActiveServer(this); - boolean checked = false; - - for (int i = 0; i <= Util.getActiveServers(this); i++) - { - final String serverName = Util.getServerName(this, i); - - if (serverName == null) - { - continue; - } - - if (Util.getServerEnabled(this, i)) - { - final int menuItemNum = getMenuItem(i); - - final MenuItem menuItem = menu.add(MENU_GROUP_SERVER, menuItemNum, menuItemNum, serverName); - - if (activeServer == i) - { - checked = true; - menuItem.setChecked(true); - } - } - } - - if (!checked) - { - MenuItem menuItem = menu.findItem(getMenuItem(0)); - - if (menuItem != null) - { - menuItem.setChecked(true); - } - } - - menu.setGroupCheckable(MENU_GROUP_SERVER, true, true); - menu.setHeaderTitle(R.string.main_select_server); - } - - private boolean getActiveServerEnabled() - { - final int activeServer = Util.getActiveServer(this); - boolean activeServerEnabled = false; - - for (int i = 0; i <= Util.getActiveServers(this); i++) - { - if (Util.getServerEnabled(this, i)) - { - if (activeServer == i) - { - activeServerEnabled = true; - } - } - } - - return activeServerEnabled; - } - - private static int getMenuItem(final int serverInstance) - { - switch (serverInstance) - { - case 0: - return MENU_ITEM_OFFLINE; - case 1: - return MENU_ITEM_SERVER_1; - case 2: - return MENU_ITEM_SERVER_2; - case 3: - return MENU_ITEM_SERVER_3; - case 4: - return MENU_ITEM_SERVER_4; - case 5: - return MENU_ITEM_SERVER_5; - case 6: - return MENU_ITEM_SERVER_6; - case 7: - return MENU_ITEM_SERVER_7; - case 8: - return MENU_ITEM_SERVER_8; - case 9: - return MENU_ITEM_SERVER_9; - case 10: - return MENU_ITEM_SERVER_10; - } - - return 0; - } - - @Override - public boolean onContextItemSelected(final MenuItem menuItem) - { - switch (menuItem.getItemId()) - { - case MENU_ITEM_OFFLINE: - setActiveServer(0); - break; - case MENU_ITEM_SERVER_1: - setActiveServer(1); - break; - case MENU_ITEM_SERVER_2: - setActiveServer(2); - break; - case MENU_ITEM_SERVER_3: - setActiveServer(3); - break; - case MENU_ITEM_SERVER_4: - setActiveServer(4); - break; - case MENU_ITEM_SERVER_5: - setActiveServer(5); - break; - case MENU_ITEM_SERVER_6: - setActiveServer(6); - break; - case MENU_ITEM_SERVER_7: - setActiveServer(7); - break; - case MENU_ITEM_SERVER_8: - setActiveServer(8); - break; - case MENU_ITEM_SERVER_9: - setActiveServer(9); - break; - case MENU_ITEM_SERVER_10: - setActiveServer(10); - break; - default: - return super.onContextItemSelected(menuItem); - } - - // Restart activity - restart(); - return true; - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { @@ -459,26 +301,6 @@ public class MainActivity extends SubsonicTabActivity return false; } - private void setActiveServer(final int instance) - { - final MediaPlayerController service = getMediaPlayerController(); - - if (Util.getActiveServer(this) != instance) - { - if (service != null) - { - service.clearIncomplete(); - } - } - - Util.setActiveServer(this, instance); - - if (service != null) - { - service.setJukeboxEnabled(Util.getJukeboxEnabled(this, instance)); - } - } - private void exit() { lifecycleSupport.getValue().onDestroy(); @@ -492,9 +314,9 @@ public class MainActivity extends SubsonicTabActivity { infoDialogDisplayed = true; - if (show || Util.getRestUrl(this, null).contains("yourhost")) + if (show) { - Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text); + Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text_new); } } } @@ -546,7 +368,13 @@ public class MainActivity extends SubsonicTabActivity startActivityForResultWithoutTransition(this, intent); } - /** + private void showServers() + { + final Intent intent = new Intent(this, ServerSelectorActivity.class); + startActivityForResult(intent, 0); + } + + /** * Temporary task to make a ping to server to get it supported api version. */ private static class PingTask extends TabActivityBackgroundTask { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java index 88b7a70a..11bf4078 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java @@ -32,6 +32,7 @@ import android.widget.ListView; import android.widget.TextView; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Artist; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; @@ -214,10 +215,10 @@ public class SearchActivity extends SubsonicTabActivity if (downloadMenuItem != null) { - downloadMenuItem.setVisible(!Util.isOffline(this)); + downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } - if (Util.isOffline(this) || isArtist) + if (ActiveServerProvider.Companion.isOffline(this) || isArtist) { if (shareButton != null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java index a96159a2..5e91390b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java @@ -37,6 +37,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.service.DownloadFile; @@ -329,7 +330,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity } else { - if (!Util.isOffline(SelectAlbumActivity.this) && Util.getShouldUseId3Tags(SelectAlbumActivity.this)) + if (!ActiveServerProvider.Companion.isOffline(SelectAlbumActivity.this) && Util.getShouldUseId3Tags(SelectAlbumActivity.this)) { if (isAlbum) { @@ -472,14 +473,14 @@ public class SelectAlbumActivity extends SubsonicTabActivity if (shareButton != null) { - shareButton.setVisible(!Util.isOffline(this)); + shareButton.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download); if (downloadMenuItem != null) { - downloadMenuItem.setVisible(!Util.isOffline(this)); + downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } } @@ -1042,9 +1043,9 @@ public class SelectAlbumActivity extends SubsonicTabActivity playNowButton.setVisibility(enabled ? View.VISIBLE : View.GONE); playNextButton.setVisibility(enabled ? View.VISIBLE : View.GONE); playLastButton.setVisibility(enabled ? View.VISIBLE : View.GONE); - pinButton.setVisibility((enabled && !Util.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE); + pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE); unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE); - downloadButton.setVisibility(enabled && !deleteEnabled && !Util.isOffline(this) ? View.VISIBLE : View.GONE); + downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(this) ? View.VISIBLE : View.GONE); deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE); } @@ -1239,7 +1240,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity boolean isAlbumList = getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE); playAllButtonVisible = !(isAlbumList || entries.isEmpty()) && !allVideos; - shareButtonVisible = !Util.isOffline(SelectAlbumActivity.this) && songCount > 0; + shareButtonVisible = !ActiveServerProvider.Companion.isOffline(SelectAlbumActivity.this) && songCount > 0; emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java index 48aaef8b..319e6d18 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java @@ -36,6 +36,8 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener; import com.handmark.pulltorefresh.library.PullToRefreshListView; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; +import org.moire.ultrasonic.data.ServerSetting; import org.moire.ultrasonic.domain.Artist; import org.moire.ultrasonic.domain.Indexes; import org.moire.ultrasonic.domain.MusicFolder; @@ -50,8 +52,15 @@ import org.moire.ultrasonic.view.ArtistAdapter; import java.util.ArrayList; import java.util.List; +import kotlin.Lazy; + +import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel; +import static org.koin.java.KoinJavaComponent.inject; + public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener { + private Lazy activeServerProvider = inject(ActiveServerProvider.class); + private Lazy serverSettingsModel = viewModel(this, ServerSettingsModel.class); private static final int MENU_GROUP_MUSIC_FOLDER = 10; @@ -91,7 +100,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2); } - if (!Util.isOffline(this) && !Util.getShouldUseId3Tags(this)) + if (!ActiveServerProvider.Companion.isOffline(this) && !Util.getShouldUseId3Tags(this)) { artistListView.addHeaderView(folderButton); } @@ -101,7 +110,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE); if (title == null) { - setActionBarSubtitle(Util.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label); + setActionBarSubtitle(ActiveServerProvider.Companion.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label); } else { @@ -147,7 +156,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false); MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this); - boolean isOffline = Util.isOffline(SelectArtistActivity.this); + boolean isOffline = ActiveServerProvider.Companion.isOffline(SelectArtistActivity.this); boolean useId3Tags = Util.getShouldUseId3Tags(SelectArtistActivity.this); if (!isOffline && !useId3Tags) @@ -155,7 +164,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this); } - String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this); + String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId(); return !isOffline && useId3Tags ? musicService.getArtists(refresh, SelectArtistActivity.this, this) : musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this); } @@ -174,8 +183,8 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter // Display selected music folder if (musicFolders != null) { - String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this); - if (musicFolderId == null) + String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId(); + if (musicFolderId == null || musicFolderId.equals("")) { if (folderName != null) { @@ -240,7 +249,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter } else if (info.position == 1) { - String musicFolderId = Util.getSelectedMusicFolderId(this); + String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId(); MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders); if (musicFolderId == null) @@ -269,7 +278,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter if (downloadMenuItem != null) { - downloadMenuItem.setVisible(!Util.isOffline(this)); + downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } } @@ -316,7 +325,13 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId()); String musicFolderId = selectedFolder == null ? null : selectedFolder.getId(); String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders) : selectedFolder.getName(); - Util.setSelectedMusicFolderId(this, musicFolderId); + + if (!ActiveServerProvider.Companion.isOffline(this)) { + ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer(); + currentSetting.setMusicFolderId(musicFolderId); + serverSettingsModel.getValue().updateItem(currentSetting); + } + folderName.setText(musicFolderName); refresh(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java index 0ba31f04..9c18b1e9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java @@ -46,6 +46,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshListView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -125,7 +126,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false); List playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this); - if (!Util.isOffline(SelectPlaylistActivity.this)) + if (!ActiveServerProvider.Companion.isOffline(SelectPlaylistActivity.this)) new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists); return playlists; } @@ -146,14 +147,14 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt super.onCreateContextMenu(menu, view, menuInfo); MenuInflater inflater = getMenuInflater(); - if (Util.isOffline(this)) inflater.inflate(R.menu.select_playlist_context_offline, menu); + if (ActiveServerProvider.Companion.isOffline(this)) inflater.inflate(R.menu.select_playlist_context_offline, menu); else inflater.inflate(R.menu.select_playlist_context, menu); MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download); if (downloadMenuItem != null) { - downloadMenuItem.setVisible(!Util.isOffline(this)); + downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ServerSettingsActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ServerSettingsActivity.java deleted file mode 100644 index 9b4027e1..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ServerSettingsActivity.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.moire.ultrasonic.activity; - -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import android.view.MenuItem; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.fragment.ServerSettingsFragment; -import org.moire.ultrasonic.util.Util; - -public class ServerSettingsActivity extends AppCompatActivity { - public static final String ARG_SERVER_ID = "argServerId"; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - applyTheme(); - super.onCreate(savedInstanceState); - - final Bundle extras = getIntent().getExtras(); - if (!extras.containsKey(ARG_SERVER_ID)) { - finish(); - return; - } - - if (savedInstanceState == null) { - configureActionBar(); - - final int serverId = extras.getInt(ARG_SERVER_ID); - getFragmentManager().beginTransaction() - .add(android.R.id.content, ServerSettingsFragment.newInstance(serverId)) - .commit(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void applyTheme() { - String theme = Util.getTheme(this); - - if ("dark".equalsIgnoreCase(theme) || "fullscreen".equalsIgnoreCase(theme)) { - setTheme(R.style.UltraSonicTheme); - } else if ("light".equalsIgnoreCase(theme) || "fullscreenlight".equalsIgnoreCase(theme)) { - setTheme(R.style.UltraSonicTheme_Light); - } - } - - private void configureActionBar() { - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayShowHomeEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index 6d790618..e9aee792 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -37,9 +37,11 @@ import android.view.View.OnTouchListener; import android.widget.*; import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; -import org.koin.java.standalone.KoinJavaComponent; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; + +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; @@ -145,7 +147,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen super.onPostCreate(bundle); instance = this; - int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE; + int visibility = ActiveServerProvider.Companion.isOffline(this) ? View.GONE : View.VISIBLE; chatMenuItem.setVisibility(visibility); bookmarksMenuItem.setVisibility(visibility); sharesMenuItem.setVisibility(visibility); @@ -225,6 +227,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtras(getIntent()); startActivityForResultWithoutTransition(this, intent); + Log.d(TAG, "Restarting activity..."); } @Override @@ -773,7 +776,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { Util.toast(this, R.string.select_album_no_sdcard); } - else if (!Util.isOffline(this) && !Util.isNetworkConnected(this)) + else if (!ActiveServerProvider.Companion.isOffline(this) && !Util.isNetworkConnected(this)) { Util.toast(this, R.string.select_album_no_network); } @@ -892,7 +895,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen List songs = new LinkedList(); MusicDirectory root; - if (!Util.isOffline(SubsonicTabActivity.this) && isArtist && Util.getShouldUseId3Tags(SubsonicTabActivity.this)) + if (!ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && isArtist && Util.getShouldUseId3Tags(SubsonicTabActivity.this)) { getSongsForArtist(id, songs); } @@ -900,7 +903,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { if (isDirectory) { - root = !Util.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(id, name, false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(id, name, false, SubsonicTabActivity.this, this); + root = !ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(id, name, false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(id, name, false, SubsonicTabActivity.this, this); } else if (isShare) { @@ -953,7 +956,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { MusicDirectory root; - root = !Util.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this); + root = !ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this); getSongsRecursively(root, songs); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ServerSettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ServerSettingsFragment.java deleted file mode 100644 index 58e26778..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ServerSettingsFragment.java +++ /dev/null @@ -1,321 +0,0 @@ -package org.moire.ultrasonic.fragment; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import androidx.annotation.Nullable; -import android.util.Log; -import android.view.View; - -import org.moire.ultrasonic.BuildConfig; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.cache.Directories; -import org.moire.ultrasonic.cache.PermanentFileStorage; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.ErrorDialog; -import org.moire.ultrasonic.util.ModalBackgroundTask; -import org.moire.ultrasonic.util.Util; - -import java.net.URL; - -/** - * Settings for Subsonic server. - */ -public class ServerSettingsFragment extends PreferenceFragment - implements Preference.OnPreferenceChangeListener, - Preference.OnPreferenceClickListener { - private static final String LOG_TAG = ServerSettingsFragment.class.getSimpleName(); - private static final String ARG_SERVER_ID = "serverId"; - - private EditTextPreference serverNamePref; - private EditTextPreference serverUrlPref; - private EditTextPreference serverUsernamePref; - private EditTextPreference serverPasswordPref; - private CheckBoxPreference equalizerPref; - private CheckBoxPreference jukeboxPref; - private CheckBoxPreference allowSelfSignedCertificatePref; - private CheckBoxPreference enableLdapUserSupportPref; - private Preference removeServerPref; - private Preference testConnectionPref; - - private int serverId; - private SharedPreferences sharedPreferences; - - public static ServerSettingsFragment newInstance(final int serverId) { - final ServerSettingsFragment fragment = new ServerSettingsFragment(); - final Bundle args = new Bundle(); - args.putInt(ARG_SERVER_ID, serverId); - fragment.setArguments(args); - - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - serverId = getArguments().getInt(ARG_SERVER_ID); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - - addPreferencesFromResource(R.xml.server_settings); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - serverNamePref = (EditTextPreference) findPreference(getString(R.string.settings_server_name)); - serverUrlPref = (EditTextPreference) findPreference(getString(R.string.settings_server_address)); - serverUsernamePref = (EditTextPreference) findPreference(getString(R.string.settings_server_username)); - serverPasswordPref = (EditTextPreference) findPreference(getString(R.string.settings_server_password)); - equalizerPref = (CheckBoxPreference) findPreference(getString(R.string.equalizer_enabled)); - jukeboxPref = (CheckBoxPreference) findPreference(getString(R.string.jukebox_is_default)); - removeServerPref = findPreference(getString(R.string.settings_server_remove_server)); - testConnectionPref = findPreference(getString(R.string.settings_test_connection_title)); - allowSelfSignedCertificatePref = (CheckBoxPreference) findPreference( - getString(R.string.settings_allow_self_signed_certificate)); - enableLdapUserSupportPref = (CheckBoxPreference) findPreference( - getString(R.string.settings_enable_ldap_user_support) - ); - - setupPreferencesValues(); - setupPreferencesListeners(); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == serverNamePref) { - sharedPreferences.edit() - .putString(Constants.PREFERENCES_KEY_SERVER_NAME + serverId, (String) newValue) - .apply(); - updateName(); - return true; - } else if (preference == serverUrlPref) { - final String url = (String) newValue; - try { - new URL(url); - if (!url.equals(url.trim()) || url.contains("@")) { - throw new Exception(); - } - } catch (Exception x) { - new ErrorDialog(getActivity(), R.string.settings_invalid_url, false); - return false; - } - - sharedPreferences.edit() - .putString(Constants.PREFERENCES_KEY_SERVER_URL + serverId, url) - .apply(); - updateUrl(); - return true; - } else if (preference == serverUsernamePref) { - String username = (String) newValue; - if (username == null || !username.equals(username.trim())) { - new ErrorDialog(getActivity(), R.string.settings_invalid_username, false); - return false; - } - - sharedPreferences.edit() - .putString(Constants.PREFERENCES_KEY_USERNAME + serverId, username) - .apply(); - updateUsername(); - return true; - } else if (preference == serverPasswordPref) { - sharedPreferences.edit() - .putString(Constants.PREFERENCES_KEY_PASSWORD + serverId, (String) newValue) - .apply(); - updatePassword(); - return true; - } else if (preference == equalizerPref) { - sharedPreferences.edit() - .putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + serverId, (Boolean) newValue) - .apply(); - return true; - } else if (preference == jukeboxPref) { - sharedPreferences.edit() - .putBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + serverId, (Boolean) newValue) - .apply(); - return true; - } else if (preference == allowSelfSignedCertificatePref) { - sharedPreferences.edit() - .putBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + serverId, (Boolean) newValue) - .apply(); - return true; - } else if (preference == enableLdapUserSupportPref) { - sharedPreferences.edit() - .putBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + serverId, (Boolean) newValue) - .apply(); - return true; - } - return false; - } - - @Override - public boolean onPreferenceClick(Preference preference) { - if (preference == removeServerPref) { - removeServer(); - return true; - } else if (preference == testConnectionPref) { - testConnection(); - return true; - } - return false; - } - - private void setupPreferencesValues() { - updateName(); - updateUrl(); - updateUsername(); - updatePassword(); - - if (!sharedPreferences.contains(Constants.PREFERENCES_KEY_SERVER_ENABLED + serverId)) { - sharedPreferences.edit() - .putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + serverId, true) - .apply(); - } - equalizerPref.setChecked(sharedPreferences - .getBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + serverId, true)); - - jukeboxPref.setChecked(sharedPreferences - .getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + serverId, false)); - - allowSelfSignedCertificatePref.setChecked(sharedPreferences - .getBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + serverId, false)); - - enableLdapUserSupportPref.setChecked(sharedPreferences - .getBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + serverId, false)); - } - - private void updatePassword() { - serverPasswordPref.setText(sharedPreferences - .getString(Constants.PREFERENCES_KEY_PASSWORD + serverId, - "")); - } - - private void updateUsername() { - serverUsernamePref.setText(sharedPreferences - .getString(Constants.PREFERENCES_KEY_USERNAME + serverId, - "")); - } - - private void updateUrl() { - final String serverUrl = sharedPreferences - .getString(Constants.PREFERENCES_KEY_SERVER_URL + serverId, - "http://"); - serverUrlPref.setText(serverUrl); - serverUrlPref.setSummary(serverUrl); - } - - private void updateName() { - final String serverName = sharedPreferences - .getString(Constants.PREFERENCES_KEY_SERVER_NAME + serverId, - ""); - serverNamePref.setText(serverName); - serverNamePref.setSummary(serverName); - } - - private void setupPreferencesListeners() { - serverNamePref.setOnPreferenceChangeListener(this); - serverUrlPref.setOnPreferenceChangeListener(this); - serverUsernamePref.setOnPreferenceChangeListener(this); - serverPasswordPref.setOnPreferenceChangeListener(this); - equalizerPref.setOnPreferenceChangeListener(this); - jukeboxPref.setOnPreferenceChangeListener(this); - allowSelfSignedCertificatePref.setOnPreferenceChangeListener(this); - enableLdapUserSupportPref.setOnPreferenceChangeListener(this); - - removeServerPref.setOnPreferenceClickListener(this); - testConnectionPref.setOnPreferenceClickListener(this); - } - - private void testConnection() { - ModalBackgroundTask task = new ModalBackgroundTask(getActivity(), false) { - private int previousInstance; - - @Override - protected Boolean doInBackground() throws Throwable { - updateProgress(R.string.settings_testing_connection); - - final Context context = getActivity(); - previousInstance = Util.getActiveServer(context); - Util.setActiveServer(context, serverId); - try { - MusicService musicService = MusicServiceFactory.getMusicService(context); - musicService.ping(context, this); - return musicService.isLicenseValid(context, null); - } finally { - Util.setActiveServer(context, previousInstance); - } - } - - @Override - protected void done(Boolean licenseValid) { - if (licenseValid) { - Util.toast(getActivity(), R.string.settings_testing_ok); - } else { - Util.toast(getActivity(), R.string.settings_testing_unlicensed); - } - } - - @Override - protected void cancel() { - super.cancel(); - Util.setActiveServer(getActivity(), previousInstance); - } - - @Override - protected void error(Throwable error) { - Log.w(LOG_TAG, error.toString(), error); - new ErrorDialog(getActivity(), String.format("%s %s", getResources().getString(R.string.settings_connection_failure), getErrorMessage(error)), false); - } - }; - task.execute(); - } - - private void removeServer() { - int activeServers = sharedPreferences - .getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0); - - // Clear permanent storage - final String storageServerId = MusicServiceFactory.getServerId(); - final Directories directories = MusicServiceFactory.getDirectories(); - final PermanentFileStorage fileStorage = new PermanentFileStorage( - directories, - storageServerId, - BuildConfig.DEBUG - ); - fileStorage.clearAll(); - - // Reset values to null so when we ask for them again they are new - sharedPreferences.edit() - .remove(Constants.PREFERENCES_KEY_SERVER_NAME + serverId) - .remove(Constants.PREFERENCES_KEY_SERVER_URL + serverId) - .remove(Constants.PREFERENCES_KEY_USERNAME + serverId) - .remove(Constants.PREFERENCES_KEY_PASSWORD + serverId) - .remove(Constants.PREFERENCES_KEY_SERVER_ENABLED + serverId) - .remove(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + serverId) - .remove(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + serverId) - .apply(); - - if (serverId < activeServers) { - int activeServer = Util.getActiveServer(getActivity()); - for (int i = serverId; i <= activeServers; i++) { - Util.removeInstanceName(getActivity(), i, activeServer); - } - } - - activeServers--; - - sharedPreferences.edit() - .putInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, activeServers) - .apply(); - - getActivity().finish(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index b621d8d5..3edebcd7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -9,9 +9,10 @@ import android.provider.SearchRecentSuggestions; import androidx.annotation.Nullable; import android.util.Log; import android.view.View; -import org.koin.java.standalone.KoinJavaComponent; + +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.ServerSettingsActivity; +import org.moire.ultrasonic.activity.ServerSelectorActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.FeatureStorage; @@ -25,7 +26,8 @@ import java.io.File; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; +import static org.moire.ultrasonic.activity.ServerSelectorActivity.SERVER_SELECTOR_MANAGE_MODE; /** * Shows main app settings. @@ -63,9 +65,7 @@ public class SettingsFragment extends PreferenceFragment private TimeSpanPreference sharingDefaultExpiration; private PreferenceCategory serversCategory; - private int maxServerCount = 10; private SharedPreferences settings; - private int activeServers; private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); @@ -271,77 +271,25 @@ public class SettingsFragment extends PreferenceFragment } private void setupServersCategory() { - activeServers = settings.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0); final Preference addServerPreference = new Preference(getActivity()); - addServerPreference.setKey(Constants.PREFERENCES_KEY_ADD_SERVER); addServerPreference.setPersistent(false); - addServerPreference.setTitle(getResources().getString(R.string.settings_server_add_server)); - addServerPreference.setEnabled(activeServers < maxServerCount); + addServerPreference.setTitle(getResources().getString(R.string.settings_server_manage_servers)); + addServerPreference.setEnabled(true); + // TODO new server management here serversCategory.removeAll(); serversCategory.addPreference(addServerPreference); - for (int i = 1; i <= activeServers; i++) { - final int serverId = i; - Preference preference = new Preference(getActivity()); - preference.setPersistent(false); - preference.setTitle(settings.getString(Constants.PREFERENCES_KEY_SERVER_NAME + serverId, - getString(R.string.settings_server_name))); - preference.setSummary(settings.getString(Constants.PREFERENCES_KEY_SERVER_URL + serverId, - getString(R.string.settings_server_address_unset))); - preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - final Intent intent = new Intent(getActivity(), ServerSettingsActivity.class); - intent.putExtra(ServerSettingsActivity.ARG_SERVER_ID, serverId); - startActivity(intent); - return true; - } - }); - serversCategory.addPreference(preference); - } - addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - if (activeServers == maxServerCount) { - return false; - } - - activeServers++; - - settings.edit() - .putInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, activeServers) - .apply(); - - Preference addServerPreference = findPreference(Constants.PREFERENCES_KEY_ADD_SERVER); - - if (addServerPreference != null) { - serversCategory.removePreference(addServerPreference); - } - - Preference newServerPrefs = new Preference(getActivity()); - newServerPrefs.setTitle(getString(R.string.settings_server_name)); - newServerPrefs.setSummary(getString(R.string.settings_server_address_unset)); - newServerPrefs.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - final Intent intent = new Intent(getActivity(), ServerSettingsActivity.class); - intent.putExtra(ServerSettingsActivity.ARG_SERVER_ID, activeServers); - startActivity(intent); - return true; - } - }); - serversCategory.addPreference(newServerPrefs); - - if (addServerPreference != null) { - serversCategory.addPreference(addServerPreference); - addServerPreference.setEnabled(activeServers < maxServerCount); - } - + final Intent intent = new Intent(getActivity(), ServerSelectorActivity.class); + intent.putExtra(SERVER_SELECTOR_MANAGE_MODE, true); + startActivityForResult(intent, 0); return true; } }); + } private void update() { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java index 9bc2ad66..52274108 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java @@ -9,7 +9,7 @@ import org.moire.ultrasonic.service.MediaPlayerController; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; public class A2dpIntentReceiver extends BroadcastReceiver { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java index 51b9db57..e4a219f9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java @@ -31,7 +31,7 @@ import org.moire.ultrasonic.util.Util; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * @author Sindre Mehus diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java index 282b1061..d96429ff 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java @@ -11,7 +11,7 @@ import org.moire.ultrasonic.util.Util; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; public class AudioFocusHandler { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.java index d3ed1276..173604c1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.java @@ -21,6 +21,7 @@ package org.moire.ultrasonic.service; import android.content.Context; import android.graphics.Bitmap; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; @@ -48,13 +49,17 @@ import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; +import kotlin.Lazy; import kotlin.Pair; +import static org.koin.java.KoinJavaComponent.inject; + /** * @author Sindre Mehus */ public class CachedMusicService implements MusicService { + private Lazy activeServerProvider = inject(ActiveServerProvider.class); private static final int MUSIC_DIR_CACHE_SIZE = 100; @@ -365,7 +370,7 @@ public class CachedMusicService implements MusicService private void checkSettingsChanged(Context context) { - String newUrl = Util.getRestUrl(context, null); + String newUrl = activeServerProvider.getValue().getRestUrl(null); if (!Util.equals(newUrl, restUrl)) { cachedMusicFolders.clear(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java index 4bfa4277..3b441ab6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -44,7 +44,7 @@ import kotlin.Pair; import static android.content.Context.POWER_SERVICE; import static android.os.PowerManager.ON_AFTER_RELEASE; import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * @author Sindre Mehus diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java index 285870c3..ce016bd1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; import static org.moire.ultrasonic.domain.PlayerState.STARTED; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java index 80860352..132ab3cd 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java @@ -30,6 +30,7 @@ import android.widget.Toast; import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.JukeboxStatus; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.util.Util; @@ -47,7 +48,7 @@ import java.util.concurrent.atomic.AtomicLong; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * Provides an asynchronous interface to the remote jukebox on the Subsonic server. @@ -158,7 +159,7 @@ public class JukeboxMediaPlayer try { - if (!Util.isOffline(context)) + if (!ActiveServerProvider.Companion.isOffline(context)) { task = tasks.take(); JukeboxStatus status = task.execute(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java index d10cf796..6b9aba0e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.VisualizerController; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; @@ -968,7 +969,7 @@ public class LocalMediaPlayer { setPlayerState(DOWNLOADING); - while (!bufferComplete() && !Util.isOffline(context)) + while (!bufferComplete() && !ActiveServerProvider.Companion.isOffline(context)) { Util.sleepQuietly(1000L); if (isCancelled()) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java index b7e9f034..bbe29d7e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java @@ -22,9 +22,10 @@ import android.content.Context; import android.content.Intent; import android.util.Log; -import org.koin.java.standalone.KoinJavaComponent; +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.VisualizerController; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; @@ -40,7 +41,7 @@ import java.util.List; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * The implementation of the Media Player Controller. @@ -63,6 +64,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController private Context context; private Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); private final DownloadQueueSerializer downloadQueueSerializer; private final ExternalStorageMonitor externalStorageMonitor; private final Downloader downloader; @@ -93,8 +95,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController } }); - int instance = Util.getActiveServer(context); - setJukeboxEnabled(Util.getJukeboxEnabled(context, instance)); + setJukeboxEnabled(activeServerProvider.getValue().getActiveServer().getJukeboxByDefault()); created = true; Log.i(TAG, "MediaPlayerControllerImpl created"); @@ -519,7 +520,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController { try { - String username = Util.getUserName(context, Util.getActiveServer(context)); + String username = activeServerProvider.getValue().getActiveServer().getUserName(); UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context, null); return user.getJukeboxRole(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 7c50d0e6..f5a09d03 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -18,7 +18,7 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import org.koin.java.standalone.KoinJavaComponent; +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; @@ -38,7 +38,7 @@ import org.moire.ultrasonic.util.Util; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; import static org.moire.ultrasonic.domain.PlayerState.IDLE; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index 97e3d125..c8bf71f4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -25,6 +25,7 @@ import android.util.Log; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.cache.PermanentFileStorage; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Artist; import org.moire.ultrasonic.domain.Genre; import org.moire.ultrasonic.domain.Indexes; @@ -60,6 +61,10 @@ import java.util.SortedSet; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import kotlin.Lazy; + +import static org.koin.java.KoinJavaComponent.inject; + /** * @author Sindre Mehus */ @@ -68,7 +73,9 @@ public class OfflineMusicService extends RESTMusicService private static final String TAG = OfflineMusicService.class.getSimpleName(); private static final Pattern COMPILE = Pattern.compile(" "); - public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) { + private Lazy activeServerProvider = inject(ActiveServerProvider.class); + + public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) { super(subsonicAPIClient, storage); } @@ -626,7 +633,7 @@ public class OfflineMusicService extends RESTMusicService @Override public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { - File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context), name); + File playlistFile = FileUtil.getPlaylistFile(context, activeServerProvider.getValue().getActiveServer().getName(), name); FileWriter fw = new FileWriter(playlistFile); BufferedWriter bw = new BufferedWriter(fw); try diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index eaf191f3..b5dd7bc9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -63,6 +63,7 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.api.subsonic.response.VideosResponse; import org.moire.ultrasonic.cache.PermanentFileStorage; import org.moire.ultrasonic.cache.serializers.DomainSerializers; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.APIAlbumConverter; import org.moire.ultrasonic.domain.APIArtistConverter; import org.moire.ultrasonic.domain.APIBookmarkConverter; @@ -109,15 +110,20 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import kotlin.Lazy; import kotlin.Pair; import retrofit2.Response; +import static org.koin.java.KoinJavaComponent.inject; + /** * @author Sindre Mehus */ public class RESTMusicService implements MusicService { private static final String TAG = RESTMusicService.class.getSimpleName(); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); + private static final String MUSIC_FOLDER_STORAGE_NAME = "music_folder"; private static final String INDEXES_STORAGE_NAME = "indexes"; private static final String ARTISTS_STORAGE_NAME = "artists"; @@ -306,7 +312,7 @@ public class RESTMusicService implements MusicService { Context context, ProgressListener progressListener) throws Exception { try { - return !Util.isOffline(context) && + return !ActiveServerProvider.Companion.isOffline(context) && Util.getShouldUseId3Tags(context) ? search3(criteria, context, progressListener) : search2(criteria, context, progressListener); @@ -388,7 +394,7 @@ public class RESTMusicService implements MusicService { private void savePlaylist(String name, Context context, MusicDirectory playlist) throws IOException { - File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context), name); + File playlistFile = FileUtil.getPlaylistFile(context, activeServerProvider.getValue().getActiveServer().getName(), name); FileWriter fw = new FileWriter(playlistFile); BufferedWriter bw = new BufferedWriter(fw); try { @@ -629,7 +635,7 @@ public class RESTMusicService implements MusicService { synchronized (entry) { // Use cached file, if existing. Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size, highQuality); - boolean serverScaling = Util.isServerScalingEnabled(context); + boolean serverScaling = ActiveServerProvider.Companion.isServerScalingEnabled(context); if (bitmap == null) { Log.d(TAG, "Loading cover art for: " + entry); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java index 80bdc035..40bd791f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Scrobbler.java @@ -3,6 +3,7 @@ package org.moire.ultrasonic.service; import android.content.Context; import android.util.Log; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.util.Util; /** @@ -13,7 +14,6 @@ import org.moire.ultrasonic.util.Util; */ public class Scrobbler { - private static final String TAG = Scrobbler.class.getSimpleName(); private String lastSubmission; @@ -21,7 +21,7 @@ public class Scrobbler public void scrobble(final Context context, final DownloadFile song, final boolean submission) { - if (song == null || !Util.isScrobblingEnabled(context)) + if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled(context)) { return; } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index 830eb882..2382712b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -5,6 +5,7 @@ import android.os.AsyncTask; import android.os.StatFs; import android.util.Log; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.Downloader; @@ -21,7 +22,7 @@ import java.util.SortedSet; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * @author Sindre Mehus @@ -39,6 +40,7 @@ public class CacheCleaner private final Context context; private Lazy downloader = inject(Downloader.class); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); public CacheCleaner(Context context) { @@ -301,7 +303,7 @@ public class CacheCleaner try { Thread.currentThread().setName("BackgroundPlaylistsCleanup"); - String server = Util.getServerName(context); + String server = activeServerProvider.getValue().getActiveServer().getName(); SortedSet playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server)); List playlists = params[0]; for (Playlist playlist : playlists) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 303bc680..b1a83133 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -73,20 +73,8 @@ public final class Constants public static final int NOTIFICATION_ID_PLAYING = 100; // Preferences keys. - public static final String PREFERENCES_KEY_SERVER = "server"; - public static final String PREFERENCES_KEY_SERVER_ENABLED = "serverEnabled"; - public static final String PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled"; 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_SERVERS_KEY = "serversKey"; - public static final String PREFERENCES_KEY_ADD_SERVER = "addServer"; - public static final String PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers"; - 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_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate"; - public static final String PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport"; public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime"; public static final String PREFERENCES_KEY_THEME = "theme"; public static final String PREFERENCES_KEY_DISPLAY_BITRATE_WITH_ARTIST = "displayBitrateWithArtist"; @@ -142,6 +130,7 @@ public final class Constants public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader"; public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating"; public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory"; + public static final String PREFERENCES_KEY_WELCOME_SCREEN_SHOWN = "welcomeScreenShown"; // Number of free trial days for non-licensed servers. public static final int FREE_TRIAL_DAYS = 30; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java index 9c84d6a0..e45bee66 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java @@ -21,6 +21,7 @@ package org.moire.ultrasonic.util; import android.content.Context; import android.util.Log; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -97,7 +98,7 @@ public class ShufflePlayBuffer // Check if active server has changed. clearBufferIfNecessary(); - if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !Util.isOffline(context))) + if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !ActiveServerProvider.Companion.isOffline(context))) { return; } @@ -124,9 +125,9 @@ public class ShufflePlayBuffer { synchronized (buffer) { - if (currentServer != Util.getActiveServer(context)) + if (currentServer != ActiveServerProvider.Companion.getActiveServerId(context)) { - currentServer = Util.getActiveServer(context); + currentServer = ActiveServerProvider.Companion.getActiveServerId(context); buffer.clear(); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 8b4108f2..f880787c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -24,6 +24,7 @@ import android.app.PendingIntent; import android.content.*; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -31,7 +32,6 @@ import android.graphics.Canvas; 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.Uri; @@ -42,10 +42,14 @@ import android.os.Parcelable; import android.preference.PreferenceManager; import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.widget.RemoteViews; import android.widget.Toast; + +import androidx.annotation.ColorInt; + import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.MainActivity; @@ -54,8 +58,6 @@ import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.MediaPlayerController; -import org.moire.ultrasonic.service.MusicServiceFactory; import java.io.*; import java.security.MessageDigest; @@ -71,9 +73,8 @@ import java.util.regex.Pattern; * @author Sindre Mehus * @version $Id$ */ -public class Util extends DownloadActivity +public class Util { - private static final String TAG = Util.class.getSimpleName(); private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB"); @@ -107,11 +108,6 @@ public class Util extends DownloadActivity { } - public static boolean isOffline(Context context) - { - return context == null || getActiveServer(context) == 0; - } - public static boolean isScreenLitOnDownload(Context context) { SharedPreferences preferences = getPreferences(context); @@ -132,26 +128,6 @@ public class Util extends DownloadActivity editor.commit(); } - public static boolean isScrobblingEnabled(Context context) - { - if (isOffline(context)) - { - return false; - } - SharedPreferences preferences = getPreferences(context); - return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false); - } - - public static boolean isServerScalingEnabled(Context context) - { - if (isOffline(context)) - { - return false; - } - SharedPreferences preferences = getPreferences(context); - return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_SCALING, false); - } - public static boolean isNotificationEnabled(Context context) { // After API26 foreground services must be used for music playback, and they must have a notification @@ -174,152 +150,6 @@ public class Util extends DownloadActivity return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false); } - public static void setActiveServer( - Context context, - int instance - ) { - MusicServiceFactory.resetMusicService(); - SharedPreferences preferences = getPreferences(context); - SharedPreferences.Editor editor = preferences.edit(); - editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); - editor.apply(); - } - - public static int getActiveServer(Context context) - { - SharedPreferences preferences = getPreferences(context); - return preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); - } - - public static int getActiveServers(Context context) - { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - return settings.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 3); - } - - public static String getServerName(Context context) - { - int instance = getActiveServer(context); - - if (instance == 0) - { - return context.getResources().getString(R.string.main_offline); - } - - SharedPreferences preferences = getPreferences(context); - return preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); - } - - public static String getServerName(Context context, int instance) - { - if (instance == 0) - { - return context.getResources().getString(R.string.main_offline); - } - SharedPreferences preferences = getPreferences(context); - return preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); - } - - public static String getUserName(Context context, int instance) - { - if (instance == 0) - { - return context.getResources().getString(R.string.main_offline); - } - SharedPreferences preferences = getPreferences(context); - return preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); - } - - public static boolean getServerEnabled(Context context, int instance) - { - if (instance == 0) - { - return true; - } - SharedPreferences preferences = getPreferences(context); - return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + instance, true); - } - - public static boolean getJukeboxEnabled(Context context, int instance) - { - if (instance == 0) - { - return false; - } - - SharedPreferences preferences = getPreferences(context); - return preferences.getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + instance, false); - } - - public static void setServerRestVersion(Context context, Version version) - { - SERVER_REST_VERSIONS.put(getActiveServer(context), version); - } - - public static Version getServerRestVersion(Context context) - { - return SERVER_REST_VERSIONS.get(getActiveServer(context)); - } - - public static void removeInstanceName(Context context, int instance, int activeInstance) - { - SharedPreferences preferences = getPreferences(context); - SharedPreferences.Editor editor = preferences.edit(); - - int newInstance = instance + 1; - - String server = preferences.getString(Constants.PREFERENCES_KEY_SERVER + newInstance, null); - String serverName = preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null); - String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); - String userName = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); - String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); - boolean serverEnabled = preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + newInstance, true); - boolean jukeboxEnabled = preferences.getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + newInstance, true); - - editor.putString(Constants.PREFERENCES_KEY_SERVER + 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.putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + instance, serverEnabled); - editor.putBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + instance, jukeboxEnabled); - - editor.putString(Constants.PREFERENCES_KEY_SERVER + 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.putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + newInstance, true); - editor.putBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + newInstance, false); - editor.commit(); - - if (instance == activeInstance) - { - Util.setActiveServer(context, 0); - } - - if (newInstance == activeInstance) - { - Util.setActiveServer(context, instance); - } - } - - public static void setSelectedMusicFolderId(Context context, String musicFolderId) - { - int instance = getActiveServer(context); - SharedPreferences preferences = getPreferences(context); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); - editor.commit(); - } - - public static String getSelectedMusicFolderId(Context context) - { - SharedPreferences preferences = getPreferences(context); - int instance = getActiveServer(context); - return preferences.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); - } - public static String getTheme(Context context) { SharedPreferences preferences = getPreferences(context); @@ -356,35 +186,6 @@ public class Util extends DownloadActivity return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; } - public static String getRestUrl(Context context, String method) - { - StringBuilder builder = new StringBuilder(8192); - - SharedPreferences preferences = getPreferences(context); - - int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); - String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); - String username = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); - String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); - - // Slightly obfuscate password - password = "enc:" + Util.utf8HexEncode(password); - - builder.append(serverUrl); - if (builder.charAt(builder.length() - 1) != '/') - { - builder.append('/'); - } - - builder.append("rest/").append(method).append(".view"); - builder.append("?u=").append(username); - builder.append("&p=").append(password); - builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION); - builder.append("&c=").append(Constants.REST_CLIENT_ID); - - return builder.toString(); - } - public static SharedPreferences getPreferences(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } @@ -1619,4 +1420,31 @@ public class Util extends DownloadActivity SharedPreferences preferences = getPreferences(context); return Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY, "5")); } + + public static @ColorInt int getColorFromAttribute(Context context, int resId) + { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(resId, typedValue, true); + return typedValue.data; + } + + public static int getResourceFromAttribute(Context context, int resId) + { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(resId, typedValue, true); + return typedValue.resourceId; + } + + public static boolean shouldShowWelcomeScreen(Context context) + { + SharedPreferences preferences = getPreferences(context); + boolean shown = preferences.getBoolean(Constants.PREFERENCES_KEY_WELCOME_SCREEN_SHOWN, false); + if (shown) return false; + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_WELCOME_SCREEN_SHOWN, true); + editor.apply(); + return true; + } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java index 1884e201..a3a3e181 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java @@ -26,6 +26,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -127,7 +128,7 @@ public class AlbumView extends UpdateView viewHolder.artist.setVisibility(artist == null ? View.GONE : View.VISIBLE); viewHolder.star.setImageDrawable(starred ? starDrawable : starHollowDrawable); - if (Util.isOffline(this.context) || "-1".equals(album.getId())) + if (ActiveServerProvider.Companion.isOffline(this.context) || "-1".equals(album.getId())) { viewHolder.star.setVisibility(View.GONE); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java index 0361e81f..235dccda 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java @@ -10,6 +10,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.SubsonicTabActivity; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.util.ImageLoader; import org.moire.ultrasonic.util.Util; @@ -19,6 +20,10 @@ import java.util.Date; import java.util.List; import java.util.regex.Pattern; +import kotlin.Lazy; + +import static org.koin.java.KoinJavaComponent.inject; + public class ChatAdapter extends ArrayAdapter { private final SubsonicTabActivity activity; @@ -27,6 +32,8 @@ public class ChatAdapter extends ArrayAdapter private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; private static final Pattern phoneMatcher = Pattern.compile(phoneRegex); + private Lazy activeServerProvider = inject(ActiveServerProvider.class); + public ChatAdapter(SubsonicTabActivity activity, List messages) { super(activity, R.layout.chat_item, messages); @@ -62,7 +69,7 @@ public class ChatAdapter extends ArrayAdapter Date messageTime = new java.util.Date(message.getTime()); String messageText = message.getMessage(); - String me = Util.getUserName(activity, Util.getActiveServer(activity)); + String me = activeServerProvider.getValue().getActiveServer().getUserName(); layout = messageUser.equals(me) ? R.layout.chat_item_reverse : R.layout.chat_item; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java index b12b438a..8c2a879c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java @@ -31,8 +31,9 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -import org.koin.java.standalone.KoinJavaComponent; +import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.FeatureStorage; @@ -47,7 +48,7 @@ import java.io.File; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * Used to display songs in a {@code ListView}. @@ -73,8 +74,6 @@ public class SongView extends UpdateView implements Checkable private ImageType previousLeftImageType; private ImageType previousRightImageType; private ImageType leftImageType; - private ImageType rightImageType; - private Drawable rightImage; private DownloadFile downloadFile; private boolean playing; private EntryAdapter.SongViewHolder viewHolder; @@ -134,20 +133,20 @@ public class SongView extends UpdateView implements Checkable { inflater.inflate(song.isVideo() ? R.layout.video_list_item : R.layout.song_list_item, this, true); viewHolder = new EntryAdapter.SongViewHolder(); - viewHolder.check = (CheckedTextView) findViewById(R.id.song_check); - viewHolder.rating = (LinearLayout) findViewById(R.id.song_rating); - viewHolder.fiveStar1 = (ImageView) findViewById(R.id.song_five_star_1); - viewHolder.fiveStar2 = (ImageView) findViewById(R.id.song_five_star_2); - viewHolder.fiveStar3 = (ImageView) findViewById(R.id.song_five_star_3); - viewHolder.fiveStar4 = (ImageView) findViewById(R.id.song_five_star_4); - viewHolder.fiveStar5 = (ImageView) findViewById(R.id.song_five_star_5); - viewHolder.star = (ImageView) findViewById(R.id.song_star); - viewHolder.drag = (ImageView) findViewById(R.id.song_drag); - viewHolder.track = (TextView) findViewById(R.id.song_track); - viewHolder.title = (TextView) findViewById(R.id.song_title); - viewHolder.artist = (TextView) findViewById(R.id.song_artist); - viewHolder.duration = (TextView) findViewById(R.id.song_duration); - viewHolder.status = (TextView) findViewById(R.id.song_status); + viewHolder.check = findViewById(R.id.song_check); + viewHolder.rating = findViewById(R.id.song_rating); + viewHolder.fiveStar1 = findViewById(R.id.song_five_star_1); + viewHolder.fiveStar2 = findViewById(R.id.song_five_star_2); + viewHolder.fiveStar3 = findViewById(R.id.song_five_star_3); + viewHolder.fiveStar4 = findViewById(R.id.song_five_star_4); + viewHolder.fiveStar5 = findViewById(R.id.song_five_star_5); + viewHolder.star = findViewById(R.id.song_star); + viewHolder.drag = findViewById(R.id.song_drag); + viewHolder.track = findViewById(R.id.song_track); + viewHolder.title = findViewById(R.id.song_title); + viewHolder.artist = findViewById(R.id.song_artist); + viewHolder.duration = findViewById(R.id.song_duration); + viewHolder.status = findViewById(R.id.song_status); setTag(viewHolder); } @@ -248,7 +247,7 @@ public class SongView extends UpdateView implements Checkable viewHolder.drag.setVisibility(draggable ? View.VISIBLE : View.GONE); } - if (Util.isOffline(this.context)) + if (ActiveServerProvider.Companion.isOffline(this.context)) { viewHolder.star.setVisibility(View.GONE); viewHolder.rating.setVisibility(View.GONE); @@ -338,6 +337,8 @@ public class SongView extends UpdateView implements Checkable this.leftImage = null; } + ImageType rightImageType; + Drawable rightImage; if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) { if (this.viewHolder.status != null) @@ -345,13 +346,13 @@ public class SongView extends UpdateView implements Checkable this.viewHolder.status.setText(Util.formatLocalizedBytes(partialFile.length(), this.context)); } - this.rightImageType = ImageType.downloading; - this.rightImage = downloadingImage; + rightImageType = ImageType.downloading; + rightImage = downloadingImage; } else { - this.rightImageType = ImageType.none; - this.rightImage = null; + rightImageType = ImageType.none; + rightImage = null; if (this.viewHolder.status != null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java index d08e062f..e4cc2cdf 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java @@ -31,7 +31,7 @@ import org.moire.ultrasonic.service.MediaPlayerController; import kotlin.Lazy; -import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.koin.java.KoinJavaComponent.inject; /** * A simple class that draws waveform data received from a diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt new file mode 100644 index 00000000..cd77c6e6 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt @@ -0,0 +1,327 @@ +package org.moire.ultrasonic.activity + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.widget.Button +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import com.google.android.material.switchmaterial.SwitchMaterial +import com.google.android.material.textfield.TextInputLayout +import java.io.IOException +import java.net.MalformedURLException +import java.net.URL +import org.koin.android.viewmodel.ext.android.viewModel +import org.moire.ultrasonic.BuildConfig +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions +import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.service.SubsonicRESTException +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.ErrorDialog +import org.moire.ultrasonic.util.ModalBackgroundTask +import org.moire.ultrasonic.util.Util +import retrofit2.Response + +/** + * This Activity provides a Form which can be used to edit the properties of a Server Setting. + * It can also be used to create a Server Setting from scratch. + * Contains functions for testing the configured Server Setting + */ +internal class EditServerActivity : AppCompatActivity() { + + companion object { + private val TAG = EditServerActivity::class.simpleName + const val EDIT_SERVER_INTENT_INDEX = "index" + } + + private val serverSettingsModel: ServerSettingsModel by viewModel() + private var currentServerSetting: ServerSetting? = null + + private var serverNameEditText: TextInputLayout? = null + private var serverAddressEditText: TextInputLayout? = null + private var userNameEditText: TextInputLayout? = null + private var passwordEditText: TextInputLayout? = null + private var selfSignedSwitch: SwitchMaterial? = null + private var ldapSwitch: SwitchMaterial? = null + private var jukeboxSwitch: SwitchMaterial? = null + private var saveButton: Button? = null + private var testButton: Button? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + applyTheme() + if (savedInstanceState == null) configureActionBar() + + setContentView(R.layout.server_edit) + + serverNameEditText = findViewById(R.id.edit_server_name) + serverAddressEditText = findViewById(R.id.edit_server_address) + userNameEditText = findViewById(R.id.edit_server_username) + passwordEditText = findViewById(R.id.edit_server_password) + selfSignedSwitch = findViewById(R.id.edit_self_signed) + ldapSwitch = findViewById(R.id.edit_ldap) + jukeboxSwitch = findViewById(R.id.edit_jukebox) + saveButton = findViewById(R.id.edit_save) + testButton = findViewById(R.id.edit_test) + + val index = intent.getIntExtra(EDIT_SERVER_INTENT_INDEX, -1) + + if (index != -1) { + // Editing an existing server + setTitle(R.string.server_editor_label) + val serverSetting = serverSettingsModel.getServerSetting(index) + serverSetting.observe( + this, + Observer { t -> + if (t != null) { + currentServerSetting = t + setFields() + } + } + ) + saveButton!!.setOnClickListener { + if (currentServerSetting != null) { + if (getFields()) { + serverSettingsModel.updateItem(currentServerSetting) + finish() + } + } + } + } else { + // Creating a new server + setTitle(R.string.server_editor_new_label) + saveButton!!.setOnClickListener { + currentServerSetting = ServerSetting() + if (getFields()) { + serverSettingsModel.saveNewItem(currentServerSetting) + finish() + } + } + } + + testButton!!.setOnClickListener { + if (getFields()) { + testConnection() + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + if (areFieldsChanged()) { + AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.common_confirm) + .setMessage(R.string.server_editor_leave_confirmation) + .setPositiveButton(R.string.common_ok) { dialog, _ -> + dialog.dismiss() + finish() + } + .setNegativeButton(R.string.common_cancel) { dialog, _ -> + dialog.dismiss() + } + .show() + } else { + finish() + } + return true + } + return super.onOptionsItemSelected(item) + } + + private fun applyTheme() { + val theme = Util.getTheme(this) + if ( + "dark".equals(theme, ignoreCase = true) || + "fullscreen".equals(theme, ignoreCase = true) + ) { + setTheme(R.style.UltraSonicTheme) + } else if ( + "light".equals(theme, ignoreCase = true) || + "fullscreenlight".equals(theme, ignoreCase = true) + ) { + setTheme(R.style.UltraSonicTheme_Light) + } + } + + private fun configureActionBar() { + val actionBar: ActionBar? = supportActionBar + if (actionBar != null) { + actionBar.setDisplayShowHomeEnabled(true) + actionBar.setDisplayHomeAsUpEnabled(true) + } + } + + /** + * Sets the values of the Form from the current Server Setting instance + */ + private fun setFields() { + if (currentServerSetting == null) return + + serverNameEditText!!.editText?.setText(currentServerSetting!!.name) + serverAddressEditText!!.editText?.setText(currentServerSetting!!.url) + userNameEditText!!.editText?.setText(currentServerSetting!!.userName) + passwordEditText!!.editText?.setText(currentServerSetting!!.password) + selfSignedSwitch!!.isChecked = currentServerSetting!!.allowSelfSignedCertificate + ldapSwitch!!.isChecked = currentServerSetting!!.ldapSupport + jukeboxSwitch!!.isChecked = currentServerSetting!!.jukeboxByDefault + } + + /** + * Retrieves the values in the Form to the current Server Setting instance + * This function also does some basic validation on the fields + */ + private fun getFields(): Boolean { + if (currentServerSetting == null) return false + var isValid = true + var url: URL? = null + + if (serverAddressEditText!!.editText?.text.isNullOrBlank()) { + serverAddressEditText!!.error = getString(R.string.server_editor_required) + isValid = false + } else { + try { + val urlString = serverAddressEditText!!.editText?.text.toString() + url = URL(urlString) + if (urlString != urlString.trim(' ') || urlString.contains("@")) { + throw MalformedURLException() + } + serverAddressEditText!!.error = null + } catch (exception: MalformedURLException) { + serverAddressEditText!!.error = getString(R.string.settings_invalid_url) + isValid = false + } + } + + if (serverNameEditText!!.editText?.text.isNullOrBlank()) { + if (isValid && url != null) { + serverNameEditText!!.editText?.setText(url.host) + } + } + + if (userNameEditText!!.editText?.text.isNullOrBlank()) { + userNameEditText!!.error = getString(R.string.server_editor_required) + isValid = false + } else { + userNameEditText!!.error = null + } + + if (isValid) { + currentServerSetting!!.name = serverNameEditText!!.editText?.text.toString() + currentServerSetting!!.url = serverAddressEditText!!.editText?.text.toString() + currentServerSetting!!.userName = userNameEditText!!.editText?.text.toString() + currentServerSetting!!.password = passwordEditText!!.editText?.text.toString() + currentServerSetting!!.allowSelfSignedCertificate = selfSignedSwitch!!.isChecked + currentServerSetting!!.ldapSupport = ldapSwitch!!.isChecked + currentServerSetting!!.jukeboxByDefault = jukeboxSwitch!!.isChecked + } + + return isValid + } + + /** + * Checks whether any value in the fields are changed according to their original values. + */ + private fun areFieldsChanged(): Boolean { + if (currentServerSetting == null) { + return !serverNameEditText!!.editText?.text!!.isBlank() || + !serverAddressEditText!!.editText?.text!!.isBlank() || + !userNameEditText!!.editText?.text!!.isBlank() || + !passwordEditText!!.editText?.text!!.isBlank() + } + + return currentServerSetting!!.name != serverNameEditText!!.editText?.text.toString() || + currentServerSetting!!.url != serverAddressEditText!!.editText?.text.toString() || + currentServerSetting!!.userName != userNameEditText!!.editText?.text.toString() || + currentServerSetting!!.password != passwordEditText!!.editText?.text.toString() || + currentServerSetting!!.allowSelfSignedCertificate != selfSignedSwitch!!.isChecked || + currentServerSetting!!.ldapSupport != ldapSwitch!!.isChecked || + currentServerSetting!!.jukeboxByDefault != jukeboxSwitch!!.isChecked + } + + /** + * Tests if the network connection to the entered Server Settings can be made + */ + private fun testConnection() { + val task: ModalBackgroundTask = object : ModalBackgroundTask( + this, + false + ) { + + @Throws(Throwable::class) + override fun doInBackground(): Boolean { + updateProgress(R.string.settings_testing_connection) + val configuration = SubsonicClientConfiguration( + currentServerSetting!!.url, + currentServerSetting!!.userName, + currentServerSetting!!.password, + SubsonicAPIVersions.getClosestKnownClientApiVersion( + Constants.REST_PROTOCOL_VERSION + ), + Constants.REST_CLIENT_ID, + currentServerSetting!!.allowSelfSignedCertificate, + currentServerSetting!!.ldapSupport, + BuildConfig.DEBUG + ) + val subsonicApiClient = SubsonicAPIClient(configuration) + val pingResponse = subsonicApiClient.api.ping().execute() + checkResponseSuccessful(pingResponse) + + val licenseResponse = subsonicApiClient.api.getLicense().execute() + checkResponseSuccessful(licenseResponse) + return licenseResponse.body()!!.license.valid + } + + override fun done(licenseValid: Boolean) { + if (licenseValid) { + Util.toast(activity, R.string.settings_testing_ok) + } else { + Util.toast(activity, R.string.settings_testing_unlicensed) + } + } + + override fun error(error: Throwable) { + Log.w(TAG, error.toString(), error) + ErrorDialog( + activity, + String.format( + "%s %s", + resources.getString(R.string.settings_connection_failure), + getErrorMessage(error) + ), + false + ) + } + } + task.execute() + } + + /** + * Checks the Subsonic Response for application specific errors + */ + private fun checkResponseSuccessful(response: Response) { + if ( + response.isSuccessful && + response.body()!!.status === SubsonicResponse.Status.OK + ) { + return + } + if (!response.isSuccessful) { + throw IOException("Server error, code: " + response.code()) + } else if ( + response.body()!!.status === SubsonicResponse.Status.ERROR && + response.body()!!.error != null + ) { + throw SubsonicRESTException(response.body()!!.error!!) + } else { + throw IOException("Failed to perform request: " + response.code()) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt new file mode 100644 index 00000000..f0baf6ca --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt @@ -0,0 +1,202 @@ +package org.moire.ultrasonic.activity + +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.util.Util + +/** + * Row Adapter to be used in the Server List + * Converts a Server Setting into a displayable Row, and sets up the Row's context menu + * @param manageMode: set to True if the default action by clicking the row is to edit the server + * In Manage Mode the "Offline" setting is not visible, and the servers can be edited by + * clicking the row. + */ +internal class ServerRowAdapter( + private var context: Context, + private var data: Array, + private val model: ServerSettingsModel, + private val activeServerProvider: ActiveServerProvider, + private val manageMode: Boolean, + private val serverDeletedCallback: ((Int) -> Unit), + private val serverEditRequestedCallback: ((Int) -> Unit) +) : BaseAdapter() { + + companion object { + private const val MENU_ID_EDIT = 1 + private const val MENU_ID_DELETE = 2 + private const val MENU_ID_UP = 3 + private const val MENU_ID_DOWN = 4 + } + + var inflater: LayoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + fun setData(data: Array) { + this.data = data + notifyDataSetChanged() + } + + override fun getCount(): Int { + return if (manageMode) data.size - 1 else data.size + } + + override fun getItem(position: Int): Any { + return data[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + /** + * Creates the Row representation of a Server Setting + */ + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + var index = position + // Skip "Offline" in manage mode + if (manageMode) index++ + + var vi: View? = convertView + if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false) + + val text = vi?.findViewById(R.id.server_name) + val description = vi?.findViewById(R.id.server_description) + val layout = vi?.findViewById(R.id.server_layout) + val image = vi?.findViewById(R.id.server_image) + val serverMenu = vi?.findViewById(R.id.server_menu) + + text?.text = data.single { setting -> setting.index == index }.name + description?.text = data.single { setting -> setting.index == index }.url + + // Provide icons for the row + if (index == 0) { + serverMenu?.visibility = View.INVISIBLE + image?.setImageDrawable(Util.getDrawableFromAttribute(context, R.attr.screen_on_off)) + } else { + image?.setImageDrawable(Util.getDrawableFromAttribute(context, R.attr.podcasts)) + } + + // Highlight the Active Server's row by changing its background + if (index == activeServerProvider.getActiveServer().index) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple) + } else { + layout?.setBackgroundResource( + Util.getResourceFromAttribute(context, R.attr.list_selector_holo_selected) + ) + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple) + } else { + layout?.setBackgroundResource( + Util.getResourceFromAttribute(context, R.attr.list_selector_holo) + ) + } + } + + // Add the context menu for the row + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + serverMenu?.background = ContextCompat.getDrawable( + context, + R.drawable.select_ripple_circle + ) + } else { + serverMenu?.setBackgroundColor(Color.TRANSPARENT) + } + + serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) } + + return vi + } + + /** + * Builds the Context Menu of a row when the "more" icon is clicked + */ + private fun serverMenuClick(view: View, position: Int) { + val menu = PopupMenu(context, view) + val firstServer = 1 + var lastServer = count - 1 + + if (!manageMode) { + menu.menu.add( + Menu.NONE, + MENU_ID_EDIT, + Menu.NONE, + context.getString(R.string.server_menu_edit) + ) + } else { + lastServer++ + } + + menu.menu.add( + Menu.NONE, + MENU_ID_DELETE, + Menu.NONE, + context.getString(R.string.server_menu_delete) + ) + + if (position != firstServer) { + menu.menu.add( + Menu.NONE, + MENU_ID_UP, + Menu.NONE, + context.getString(R.string.server_menu_move_up) + ) + } + + if (position != lastServer) { + menu.menu.add( + Menu.NONE, + MENU_ID_DOWN, + Menu.NONE, + context.getString(R.string.server_menu_move_down) + ) + } + + menu.show() + + menu.setOnMenuItemClickListener { menuItem -> popupMenuItemClick(menuItem, position) } + } + + /** + * Handles the click on a context menu item + */ + private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean { + when (menuItem.itemId) { + MENU_ID_EDIT -> { + serverEditRequestedCallback.invoke(position) + return true + } + MENU_ID_DELETE -> { + serverDeletedCallback.invoke(position) + return true + } + MENU_ID_UP -> { + model.moveItemUp(position) + return true + } + MENU_ID_DOWN -> { + model.moveItemDown(position) + return true + } + else -> return false + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt new file mode 100644 index 00000000..434392a3 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt @@ -0,0 +1,186 @@ +package org.moire.ultrasonic.activity + +import android.app.AlertDialog +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.widget.AdapterView +import android.widget.ListView +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject +import org.koin.android.viewmodel.ext.android.viewModel +import org.moire.ultrasonic.R +import org.moire.ultrasonic.activity.EditServerActivity.Companion.EDIT_SERVER_INTENT_INDEX +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.util.Util + +/** + * This Activity can be used to display all the configured Server Setting items. + * It also contains a FAB to add a new server. + * It has a Manage Mode and a Select Mode. In Select Mode, clicking the List Items will select + * the server, and a server can be edited using the context menu. In Manage Mode the default + * action when a List Item is clicked is to edit the server. + */ +internal class ServerSelectorActivity : AppCompatActivity() { + + companion object { + private val TAG = ServerSelectorActivity::class.simpleName + const val SERVER_SELECTOR_MANAGE_MODE = "manageMode" + } + + private var listView: ListView? = null + private val serverSettingsModel: ServerSettingsModel by viewModel() + private val service: MediaPlayerController by inject() + private val activeServerProvider: ActiveServerProvider by inject() + private var serverRowAdapter: ServerRowAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + applyTheme() + if (savedInstanceState == null) configureActionBar() + + setContentView(R.layout.server_selector) + + val manageMode = intent.getBooleanExtra(SERVER_SELECTOR_MANAGE_MODE, false) + if (manageMode) { + setTitle(R.string.settings_server_manage_servers) + } else { + setTitle(R.string.server_selector_label) + } + + listView = findViewById(R.id.server_list) + serverRowAdapter = ServerRowAdapter( + this, + arrayOf(), + serverSettingsModel, + activeServerProvider, + manageMode, + { + i -> + onServerDeleted(i) + }, + { + i -> + editServer(i) + } + ) + + listView?.adapter = serverRowAdapter + + listView?.onItemClickListener = AdapterView.OnItemClickListener { + _, _, position, _ -> + if (manageMode) { + editServer(position + 1) + } else { + setActiveServer(position) + finish() + } + } + + val fab = findViewById(R.id.server_add_fab) + fab.setOnClickListener { + editServer(-1) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + val serverList = serverSettingsModel.getServerList() + serverList.observe( + this, + Observer { t -> + serverRowAdapter!!.setData(t.toTypedArray()) + } + ) + } + private fun applyTheme() { + val theme = Util.getTheme(this) + if ( + "dark".equals(theme, ignoreCase = true) || + "fullscreen".equals(theme, ignoreCase = true) + ) { + setTheme(R.style.UltraSonicTheme) + } else if ( + "light".equals(theme, ignoreCase = true) || + "fullscreenlight".equals(theme, ignoreCase = true) + ) { + setTheme(R.style.UltraSonicTheme_Light) + } + } + + private fun configureActionBar() { + val actionBar: ActionBar? = supportActionBar + if (actionBar != null) { + actionBar.setDisplayShowHomeEnabled(true) + actionBar.setDisplayHomeAsUpEnabled(true) + } + } + + /** + * Sets the active server when a list item is clicked + */ + private fun setActiveServer(index: Int) { + // TODO this is still a blocking call - we shouldn't leave this activity before the active server is updated. + // Maybe this can be refactored by using LiveData, or this can be made more user friendly with a ProgressDialog + runBlocking { + withContext(Dispatchers.IO) { + if (activeServerProvider.getActiveServer().index != index) { + service.clearIncomplete() + activeServerProvider.setActiveServerByIndex(index) + } + service.isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault + } + } + Log.i(TAG, "Active server was set to: $index") + } + + /** + * This Callback handles the deletion of a Server Setting + */ + private fun onServerDeleted(index: Int) { + AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.server_menu_delete) + .setMessage(R.string.server_selector_delete_confirmation) + .setPositiveButton(R.string.common_delete) { dialog, _ -> + dialog.dismiss() + + val activeServerIndex = activeServerProvider.getActiveServer().index + // If the currently active server is deleted, go offline + if (index == activeServerIndex) setActiveServer(-1) + + serverSettingsModel.deleteItem(index) + Log.i(TAG, "Server deleted: $index") + } + .setNegativeButton(R.string.common_cancel) { dialog, _ -> + dialog.dismiss() + } + .show() + } + + /** + * Starts the Edit Server Activity to edit the details of a server + */ + private fun editServer(index: Int) { + val intent = Intent(this, EditServerActivity::class.java) + intent.putExtra(EDIT_SERVER_INTENT_INDEX, index) + startActivityForResult(intent, 0) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt new file mode 100644 index 00000000..0c7fbca9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt @@ -0,0 +1,203 @@ +package org.moire.ultrasonic.activity + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.data.ServerSettingDao + +/** + * ViewModel to be used in Activities which will handle Server Settings + */ +class ServerSettingsModel( + private val repository: ServerSettingDao, + private val activeServerProvider: ActiveServerProvider, + private val context: Context +) : ViewModel() { + private var serverList: MutableLiveData> = MutableLiveData() + + companion object { + private val TAG = ServerSettingsModel::class.simpleName + // These constants were removed from Constants.java as they are deprecated and only used here + private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled" + private const val PREFERENCES_KEY_SERVER_NAME = "serverName" + private const val PREFERENCES_KEY_SERVER_URL = "serverUrl" + private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers" + private const val PREFERENCES_KEY_USERNAME = "username" + private const val PREFERENCES_KEY_PASSWORD = "password" + private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate" + private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport" + private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId" + } + + /** + * Retrieves the list of the configured servers from the database. + * This function will also try to convert existing settings from the Preferences + * This function is asynchronous, uses LiveData to provide the Setting. + */ + fun getServerList(): LiveData> { + viewModelScope.launch { + val dbServerList = repository.loadAllServerSettings().toMutableList() + + if (dbServerList.isEmpty()) { + // First time load up the server settings from the Preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val serverNum = settings.getInt(PREFERENCES_KEY_ACTIVE_SERVERS, 0) + + if (serverNum != 0) { + for (x in 1 until serverNum + 1) { + val newServerSetting = loadServerSettingFromPreferences(x, settings) + dbServerList.add(newServerSetting) + repository.insert(newServerSetting) + Log.i( + TAG, + "Imported server from Preferences to Database: ${newServerSetting.name}" + ) + } + } + } + + dbServerList.add(0, ServerSetting(context.getString(R.string.main_offline), "")) + serverList.value = dbServerList + } + return serverList + } + + /** + * Retrieves a single Server Setting by its index + * This function is asynchronous, uses LiveData to provide the Setting. + */ + fun getServerSetting(index: Int): LiveData { + val result = MutableLiveData() + viewModelScope.launch { + val dbServer = repository.findByIndex(index) + result.value = dbServer + Log.d(TAG, "getServerSetting($index) returning $dbServer") + } + return result + } + + /** + * Moves a Setting up in the Server List by decreasing its index + */ + fun moveItemUp(index: Int) { + if (index == 1) return + + val itemToBeMoved = serverList.value?.single { setting -> setting.index == index } + val previousItem = serverList.value?.single { setting -> setting.index == index - 1 } + + itemToBeMoved?.index = previousItem!!.index + previousItem.index = index + + viewModelScope.launch { + repository.update(itemToBeMoved!!, previousItem) + } + + activeServerProvider.invalidateCache() + // Notify the observers of the changed values + serverList.value = serverList.value + } + + /** + * Moves a Setting down in the Server List by increasing its index + */ + fun moveItemDown(index: Int) { + if (index == (serverList.value!!.size - 1)) return + + val itemToBeMoved = serverList.value?.single { setting -> setting.index == index } + val nextItem = serverList.value?.single { setting -> setting.index == index + 1 } + + itemToBeMoved?.index = nextItem!!.index + nextItem.index = index + + viewModelScope.launch { + repository.update(itemToBeMoved!!, nextItem) + } + + activeServerProvider.invalidateCache() + // Notify the observers of the changed values + serverList.value = serverList.value + } + + /** + * Removes a Setting from the database + */ + fun deleteItem(index: Int) { + if (index == 0) return + + val newList = serverList.value!!.toMutableList() + val itemToBeDeleted = newList.single { setting -> setting.index == index } + newList.remove(itemToBeDeleted) + + for (x in index + 1 until newList.size + 1) { + newList.single { setting -> setting.index == x }.index-- + } + + viewModelScope.launch { + repository.delete(itemToBeDeleted) + for (x in index until newList.size) { + repository.update(newList.single { setting -> setting.index == x }) + } + } + + activeServerProvider.invalidateCache() + serverList.value = newList + Log.d(TAG, "deleteItem deleted index: $index") + } + + /** + * Updates a Setting in the database + */ + fun updateItem(serverSetting: ServerSetting?) { + if (serverSetting == null) return + + viewModelScope.launch { + repository.update(serverSetting) + activeServerProvider.invalidateCache() + Log.d(TAG, "updateItem updated server setting: $serverSetting") + } + } + + /** + * Inserts a new Setting into the database + */ + fun saveNewItem(serverSetting: ServerSetting?) { + if (serverSetting == null) return + + viewModelScope.launch { + serverSetting.index = (repository.getMaxIndex() ?: 0) + 1 + serverSetting.id = serverSetting.index + repository.insert(serverSetting) + Log.d(TAG, "saveNewItem saved server setting: $serverSetting") + } + } + + /** + * Reads up a Server Setting stored in the obsolete Preferences + */ + private fun loadServerSettingFromPreferences( + id: Int, + settings: SharedPreferences + ): ServerSetting { + return ServerSetting( + id, + id, + settings.getString(PREFERENCES_KEY_SERVER_NAME + id, "")!!, + settings.getString(PREFERENCES_KEY_SERVER_URL + id, "")!!, + settings.getString(PREFERENCES_KEY_USERNAME + id, "")!!, + settings.getString(PREFERENCES_KEY_PASSWORD + id, "")!!, + settings.getBoolean(PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + id, false), + settings.getBoolean(PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + id, false), + settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + id, false), + settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + id, "")!! + ) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index fe97c592..8a9e1af0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -1,8 +1,10 @@ package org.moire.ultrasonic.app import androidx.multidex.MultiDexApplication -import org.koin.android.ext.android.startKoin -import org.moire.ultrasonic.di.DiProperties +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.Level import org.moire.ultrasonic.di.appPermanentStorage import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.directoriesModule @@ -14,19 +16,21 @@ class UApp : MultiDexApplication() { override fun onCreate() { super.onCreate() - startKoin( - this, - listOf( + startKoin { + // Use Koin Android Logger + // TODO Current version of Koin has a bug, which forces the usage of Level.ERROR + androidLogger(Level.ERROR) + // declare Android context + androidContext(this@UApp) + // declare modules to use + modules( directoriesModule, appPermanentStorage, baseNetworkModule, featureFlagsModule, musicServiceModule, mediaPlayerModule - ), - extraProperties = mapOf( - DiProperties.APP_CONTEXT to applicationContext ) - ) + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt new file mode 100644 index 00000000..f6d1882c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -0,0 +1,167 @@ +package org.moire.ultrasonic.data + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.moire.ultrasonic.R +import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Util + +/** + * This class can be used to retrieve the properties of the Active Server + * It caches the settings read up from the DB to improve performance. + */ +class ActiveServerProvider( + private val repository: ServerSettingDao, + private val context: Context +) { + private var cachedServer: ServerSetting? = null + + /** + * Get the settings of the current Active Server + * @return The Active Server Settings + */ + fun getActiveServer(): ServerSetting { + val serverId = getActiveServerId(context) + + if (serverId > 0) { + if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!! + + // Ideally this is the only call where we block the thread while using the repository + runBlocking { + Log.d(TAG, "getActiveServer retrieving from DataBase, id: $serverId") + withContext(Dispatchers.IO) { + cachedServer = repository.findById(serverId) + } + } + + if (cachedServer != null) return cachedServer!! + setActiveServerId(context, 0) + } + + return ServerSetting( + id = -1, + index = 0, + name = context.getString(R.string.main_offline), + url = "http://localhost", + userName = "", + password = "", + jukeboxByDefault = false, + allowSelfSignedCertificate = false, + ldapSupport = false, + musicFolderId = "" + ) + } + + /** + * Sets the Active Server by the Server Index in the Server Selector List + * @param index: The index of the Active Server in the Server Selector List + */ + fun setActiveServerByIndex(index: Int) { + Log.d(TAG, "setActiveServerByIndex $index") + if (index < 1) { + // Offline mode is selected + setActiveServerId(context, 0) + return + } + + GlobalScope.launch(Dispatchers.IO) { + val serverId = repository.findByIndex(index)!!.id + setActiveServerId(context, serverId) + } + } + + /** + * Invalidates the Active Server Setting cache + * This should be called when the Active Server or one of its properties changes + */ + fun invalidateCache() { + Log.d(TAG, "Cache is invalidated") + cachedServer = null + } + + /** + * Gets the Rest Url of the Active Server + * @param method: The Rest resource to use + * @return The Rest Url of the method on the server + */ + fun getRestUrl(method: String?): String? { + val builder = StringBuilder(8192) + val activeServer = getActiveServer() + val serverUrl: String = activeServer.url + val username: String = activeServer.userName + var password: String = activeServer.password + + // Slightly obfuscate password + password = "enc:" + Util.utf8HexEncode(password) + builder.append(serverUrl) + if (builder[builder.length - 1] != '/') { + builder.append('/') + } + builder.append("rest/").append(method).append(".view") + builder.append("?u=").append(username) + builder.append("&p=").append(password) + builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION) + builder.append("&c=").append(Constants.REST_CLIENT_ID) + return builder.toString() + } + + companion object { + private val TAG = ActiveServerProvider::class.simpleName + + /** + * Queries if the Active Server is the "Offline" mode of Ultrasonic + * @return True, if the "Offline" mode is selected + */ + fun isOffline(context: Context?): Boolean { + return context == null || getActiveServerId(context) < 1 + } + + /** + * Queries the Id of the Active Server + */ + fun getActiveServerId(context: Context): Int { + val preferences = Util.getPreferences(context) + return preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1) + } + + /** + * Sets the Id of the Active Server + */ + fun setActiveServerId(context: Context, serverId: Int) { + resetMusicService() + + val preferences = Util.getPreferences(context) + val editor = preferences.edit() + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, serverId) + editor.apply() + } + + /** + * Queries if Scrobbling is enabled + */ + fun isScrobblingEnabled(context: Context): Boolean { + if (isOffline(context)) { + return false + } + val preferences = Util.getPreferences(context) + return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false) + } + + /** + * Queries if Server Scaling is enabled + */ + fun isServerScalingEnabled(context: Context): Boolean { + if (isOffline(context)) { + return false + } + val preferences = Util.getPreferences(context) + return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_SCALING, false) + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt new file mode 100644 index 00000000..f9704f26 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -0,0 +1,16 @@ +package org.moire.ultrasonic.data + +import androidx.room.Database +import androidx.room.RoomDatabase + +/** + * Room Database to be used to store data for Ultrasonic + */ +@Database(entities = [ServerSetting::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + + /** + * Retrieves the Server Settings DAO for the Database + */ + abstract fun serverSettingDao(): ServerSettingDao +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt new file mode 100644 index 00000000..5c337142 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt @@ -0,0 +1,39 @@ +package org.moire.ultrasonic.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Contains the data model of a Server Setting + * @param id: Unique Id of the Server Setting + * @param index: The index of the Server Setting on the Selection Activity + * @param name: The user readable Name of the Server + * @param url: The Url of the server + * @param userName: The UserName that can be used to connect to the server + * @param password: The Password of the User + * @param jukeboxByDefault: True if the JukeBox mode should be turned on for the server + * @param allowSelfSignedCertificate: True if the server uses self-signed certificate + * @param ldapSupport: True if the server authenticates the user using old Ldap-like way + * @param musicFolderId: The Id of the MusicFolder to be used with the server + */ +@Entity +data class ServerSetting( + @PrimaryKey var id: Int, + @ColumnInfo(name = "index") var index: Int, + @ColumnInfo(name = "name") var name: String, + @ColumnInfo(name = "url") var url: String, + @ColumnInfo(name = "userName") var userName: String, + @ColumnInfo(name = "password") var password: String, + @ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean, + @ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean, + @ColumnInfo(name = "ldapSupport") var ldapSupport: Boolean, + @ColumnInfo(name = "musicFolderId") var musicFolderId: String? +) { + constructor() : this ( + -1, 0, "", "", "", "", false, false, false, null + ) + constructor(name: String, url: String) : this( + -1, 0, name, url, "", "", false, false, false, null + ) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt new file mode 100644 index 00000000..c3638e99 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt @@ -0,0 +1,57 @@ +package org.moire.ultrasonic.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +/** + * Room Dao for the Server Setting table + */ +@Dao +interface ServerSettingDao { + + /** + *Inserts a new Server Setting to the table + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(vararg serverSetting: ServerSetting) + + /** + * Deletes a Server Setting from the table + */ + @Delete + suspend fun delete(serverSetting: ServerSetting) + + /** + * Updates an existing Server Setting in the table + */ + @Update + suspend fun update(vararg serverSetting: ServerSetting) + + /** + * Loads all Server Settings from the table + */ + @Query("SELECT * FROM serverSetting") + suspend fun loadAllServerSettings(): Array + + /** + * Finds a Server Setting by its unique Id + */ + @Query("SELECT * FROM serverSetting WHERE [id] = :id") + suspend fun findById(id: Int): ServerSetting? + + /** + * Finds a Server Setting by its Index in the Select List + */ + @Query("SELECT * FROM serverSetting WHERE [index] = :index") + suspend fun findByIndex(index: Int): ServerSetting? + + /** + * Retrieves the greatest Index in stored in the table + */ + @Query("SELECT MAX([index]) FROM serverSetting") + suspend fun getMaxIndex(): Int? +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index fb9c9fb9..34ae53f2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -1,10 +1,31 @@ package org.moire.ultrasonic.di -import org.koin.dsl.module.module +import androidx.room.Room +import org.koin.android.ext.koin.androidContext +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.moire.ultrasonic.activity.ServerSettingsModel +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.AppDatabase import org.moire.ultrasonic.util.Util const val SP_NAME = "Default_SP" val appPermanentStorage = module { - single(name = SP_NAME) { Util.getPreferences(getProperty(DiProperties.APP_CONTEXT)) } + single(named(SP_NAME)) { Util.getPreferences(androidContext()) } + + single { + Room.databaseBuilder( + androidContext(), + AppDatabase::class.java, + "ultrasonic-database" + ).build() + } + + single { get().serverSettingDao() } + + viewModel { ServerSettingsModel(get(), get(), androidContext()) } + + single { ActiveServerProvider(get(), androidContext()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/BaseNetworkModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/BaseNetworkModule.kt index 7bf4ee73..65287cf8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/BaseNetworkModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/BaseNetworkModule.kt @@ -1,7 +1,7 @@ package org.moire.ultrasonic.di import okhttp3.OkHttpClient -import org.koin.dsl.module.module +import org.koin.dsl.module /** * Provides base network dependencies. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DiProperties.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DiProperties.kt deleted file mode 100644 index ac4748be..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DiProperties.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.moire.ultrasonic.di - -object DiProperties { - const val APP_CONTEXT = "app_context" -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt index 577568ec..231d5f19 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt @@ -1,6 +1,7 @@ package org.moire.ultrasonic.di -import org.koin.dsl.module.module +import org.koin.dsl.bind +import org.koin.dsl.module import org.moire.ultrasonic.cache.AndroidDirectories import org.moire.ultrasonic.cache.Directories diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt index 5d8d484b..16c715e8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt @@ -1,8 +1,9 @@ package org.moire.ultrasonic.di -import org.koin.dsl.module.module +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module import org.moire.ultrasonic.featureflags.FeatureStorage val featureFlagsModule = module { - factory { FeatureStorage(getProperty(DiProperties.APP_CONTEXT)) } + factory { FeatureStorage(androidContext()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index 8553bc2c..bec8a8c0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -1,7 +1,7 @@ package org.moire.ultrasonic.di import org.koin.android.ext.koin.androidContext -import org.koin.dsl.module.module +import org.koin.dsl.module import org.moire.ultrasonic.service.AudioFocusHandler import org.moire.ultrasonic.service.DownloadQueueSerializer import org.moire.ultrasonic.service.Downloader diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 69f38a86..f522925d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -1,16 +1,16 @@ @file:JvmName("MusicServiceModule") package org.moire.ultrasonic.di -import android.content.SharedPreferences -import android.util.Log import kotlin.math.abs -import org.koin.dsl.module.module +import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named +import org.koin.dsl.module import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration -import org.moire.ultrasonic.api.subsonic.di.subsonicApiModule import org.moire.ultrasonic.cache.PermanentFileStorage +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService @@ -18,99 +18,51 @@ import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Constants -internal const val MUSIC_SERVICE_CONTEXT = "CurrentMusicService" internal const val ONLINE_MUSIC_SERVICE = "OnlineMusicService" internal const val OFFLINE_MUSIC_SERVICE = "OfflineMusicService" -private const val DEFAULT_SERVER_INSTANCE = 1 -private const val UNKNOWN_SERVER_URL = "not-exists" -private const val LOG_TAG = "MusicServiceModule" -val musicServiceModule = module(MUSIC_SERVICE_CONTEXT) { - subsonicApiModule() +val musicServiceModule = module { - single(name = "ServerInstance") { - return@single get(SP_NAME).getInt( - Constants.PREFERENCES_KEY_SERVER_INSTANCE, - DEFAULT_SERVER_INSTANCE - ) + single(named("ServerInstance")) { + return@single ActiveServerProvider.getActiveServerId(androidContext()) } - single(name = "ServerID") { - val serverInstance = get(name = "ServerInstance") - val sp: SharedPreferences = get(SP_NAME) - val serverUrl = sp.getString( - Constants.PREFERENCES_KEY_SERVER_URL + serverInstance, - null - ) - return@single if (serverUrl == null) { - UNKNOWN_SERVER_URL - } else { - abs("$serverUrl$serverInstance".hashCode()).toString() - } + single(named("ServerID")) { + val serverInstance = get(named("ServerInstance")) + val serverUrl = get().getActiveServer().url + return@single abs("$serverUrl$serverInstance".hashCode()).toString() } single { - val serverId = get(name = "ServerID") + val serverId = get(named("ServerID")) return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG) } single { - val instance = get(name = "ServerInstance") - val sp: SharedPreferences = get(SP_NAME) - val serverUrl = sp.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null) - val username = sp.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null) - val password = sp.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null) - val allowSelfSignedCertificate = sp.getBoolean( - Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance, - false + return@single SubsonicClientConfiguration( + baseUrl = get().getActiveServer().url, + username = get().getActiveServer().userName, + password = get().getActiveServer().password, + minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( + Constants.REST_PROTOCOL_VERSION + ), + clientID = Constants.REST_CLIENT_ID, + allowSelfSignedCertificate = get() + .getActiveServer().allowSelfSignedCertificate, + enableLdapUserSupport = get().getActiveServer().ldapSupport, + debug = BuildConfig.DEBUG ) - val enableLdapUserSupport = sp.getBoolean( - Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance, - false - ) - - if (serverUrl == null || - username == null || - password == null - ) { - Log.i(LOG_TAG, "Server credentials is not available") - return@single SubsonicClientConfiguration( - baseUrl = "http://localhost", - username = "", - password = "", - minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( - Constants.REST_PROTOCOL_VERSION - ), - clientID = Constants.REST_CLIENT_ID, - allowSelfSignedCertificate = allowSelfSignedCertificate, - enableLdapUserSupport = enableLdapUserSupport, - debug = BuildConfig.DEBUG - ) - } else { - return@single SubsonicClientConfiguration( - baseUrl = serverUrl, - username = username, - password = password, - minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( - Constants.REST_PROTOCOL_VERSION - ), - clientID = Constants.REST_CLIENT_ID, - allowSelfSignedCertificate = allowSelfSignedCertificate, - enableLdapUserSupport = enableLdapUserSupport, - debug = BuildConfig.DEBUG - ) - } } single { SubsonicAPIClient(get()) } - single(name = ONLINE_MUSIC_SERVICE) { + single(named(ONLINE_MUSIC_SERVICE)) { CachedMusicService(RESTMusicService(get(), get())) } - single(name = OFFLINE_MUSIC_SERVICE) { + single(named(OFFLINE_MUSIC_SERVICE)) { OfflineMusicService(get(), get()) } - single { SubsonicImageLoader(getProperty(DiProperties.APP_CONTEXT), get()) } + single { SubsonicImageLoader(androidContext(), get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt index fa10ec1e..f5644eb5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt @@ -19,23 +19,25 @@ package org.moire.ultrasonic.service import android.content.Context -import org.koin.standalone.KoinComponent -import org.koin.standalone.get -import org.koin.standalone.release +import org.koin.core.KoinComponent +import org.koin.core.context.loadKoinModules +import org.koin.core.context.unloadKoinModules +import org.koin.core.get +import org.koin.core.qualifier.named import org.moire.ultrasonic.cache.Directories -import org.moire.ultrasonic.di.MUSIC_SERVICE_CONTEXT +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE -import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.di.musicServiceModule @Deprecated("Use DI way to get MusicService") object MusicServiceFactory : KoinComponent { @JvmStatic fun getMusicService(context: Context): MusicService { - return if (Util.isOffline(context)) { - get(OFFLINE_MUSIC_SERVICE) + return if (ActiveServerProvider.isOffline(context)) { + get(named(OFFLINE_MUSIC_SERVICE)) } else { - get(ONLINE_MUSIC_SERVICE) + get(named(ONLINE_MUSIC_SERVICE)) } } @@ -45,11 +47,12 @@ object MusicServiceFactory : KoinComponent { */ @JvmStatic fun resetMusicService() { - release(MUSIC_SERVICE_CONTEXT) + unloadKoinModules(musicServiceModule) + loadKoinModules(musicServiceModule) } @JvmStatic - fun getServerId() = get(name = "ServerID") + fun getServerId() = get(named("ServerID")) @JvmStatic fun getDirectories() = get() diff --git a/ultrasonic/src/main/res/drawable-hdpi/ic_add_white.png b/ultrasonic/src/main/res/drawable-hdpi/ic_add_white.png new file mode 100644 index 0000000000000000000000000000000000000000..276717fcc4dec6eb8b005179d6b0ad754be6dd59 GIT binary patch literal 1647 zcmV-#29WuQP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=KZjw2}yhW~SlIRZ8qo8wS?NPB}l{=876(n}Gh{8{qq$;Z{N67~{)Kgiv+ zzz~JIrk1T=AeZ|Cw>{{1lsnriY}*iBhEEUX&U4=0Ite+Sa*Eqf?&lf3+fdz# z%az^Y<<>dO-|j)6h*A@56qtLU;L&r~9)zQJqJiU9J-NtA(MnB)K#Ds{Z zolz^;bTb3R10fIVv4lE}3g)_MMm1l21@h*rKTs=WVo({5A~h;BsIRe7g^Q6pD@NwZ zVaY5gXRcv#inG#S1-%vwEGu^haxw7b3$1f1D-`p?hwH#W_GvFrG(< zoVINMA;R7QXOQ-he4-$f$SboFA&v$>1)g(=e1HInaD$T^LrCnDjpyEYMrrA?aN;S( z-2@O4Qc$qL`2wsM1^J`VkV8d+L=j1%VotJ@6jDr*QqnXpxuQX%iY8Sx%~~v2G_hoA zX4y*S9B>CpPFb_%oJ(O>uu*}n0{sHX!ACgaAxAp&up=L(B7JJCQd8AxHP_O(g@$XQ zrKZhVZl#m!xG8$<(o@%NJ@+z@Ya@&}WTc_PMjqv&wyD0WpP)vY8ZS~)PcLeaI@OyA zn%9X;W+29qKwKvQBs5QE-Y7<%*0$DQphSdvsqNzYYCuL-$e`NMrZL@DDMj`)i7@``}F*POI(L$5RR zkTEylkG#Jx|NonzFvf5V|90I^A$+I}n&wB%0004nX+uL$Nkc;*aB^>EX>4Tx0C=2z zkvmAkP!xv$rb>NO9PA*{AwzYtAS&W0RV;#q(pG5I!Q`edXws0RxHt-~1qUCCRRmEM7-$i+r_q{*YfRZ;E;1h}Gm~L3a8^kl4md<&fIK)blLVQj< zZcu~7k6f2se&bwpSm2o5bW zxZDATpLEHP9LY~56bium8GTa@7`g@e*4(+Z&T;wxWN22)H^9LmFjl1Ob&q%Vbk6PH zJFWTs0Epyru<-fUu>b%724YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_ z00007bV*G`2jmG02_XZ4S80*}00B2iL_t(&-tCx6N&`U@MNfq^ex~9#gDfh8OvE*~ z645QV1NRUeh+A+FP%#Fle(1n;(!oX?yv9_Z@Sa&gS67|KdtI*!j4{TTf=RhvEU6Fd z0RrYJrA0AOk<^zomUJU&B5BkJ6(cZ^bgOT!B^~BitCz`P6WDEykAUrx1lEQHEMNf( zSl~~DkM~w@vlV!J2vn_c1q>xcmU`>bJEabr9|5PpL10P+?6(560A@fPnEM2r0aq() z0DtLiax;ncPSF}jYp$1GG&`a-Fayq;6xzw0LUZ5(cvlvN;d!i002ovPDHLkV1gQk>=Xb1 literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_dark.png b/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2af8f46f74773b723c5f394c07ac2a8194209294 GIT binary patch literal 3034 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 WzlMq3zON_%0000 zaB^>EX>4U6ba`-PAZ2)IW&i+q+T~YUmgFc7{bv=k1SF6I%i(#>>|mDPSJ)BxsI1DY z&g!2Y`-E{|5JLCrL0JF$?+*XL#ad&?VnwTJ#FcBV-0)K3b=C8vn9uY2^Y}(@`@{1C zqbs3J*I#S*_=bLYnedd;-}Z-cf8lZqqNm_t2h@TS9Hr72h_F|R}^;>qdOAYH$PU&WIWVH}=##}&}z^biJvyxHEP zjq$aC-Wd7u7T#O``S#BE-0uGL&X#)H2rnOgAbHoHp3Cs-h0{|be)}PhCon%4dGA^F zUf1qgR#;STMjd0*LktFXh)QRg;x_yQ?{@BryTld;AfIu>j(bog1}ZyHrb&%9tNKI@_YK5Ght(vUJ%htFE@ewVig}vUS@oyYBWx?MC&7`!`Ud8#O*i z`8a!`hTdTI5J4VJ#4!V6oC%1VBLG6PV-^eL%#NI67AwOkgrTH3Her$@1_<*amQ7FW zUXc3|Zb8bIaO2OA3l80%KrR5fA94Etwdr|o9KR!X&B_VOmf`s$XIlNXrPJF2j zQ}4oGgeJ?}7@gTn7@1hsH4{rQj3c@-MqU4pA%QhXCX_T^SjL zX34oCeV8m1wz+vW=w@lpQTk>q98$(XXS%Z2CO>SE!fyw7&>!Tlu$dwXR9%xsD1tNE zlD=fO*t%pSIp6jC#6s`e@W}u|dL#Gn-ce2Ah|?lYQE>m=s6n+g#M!Zd+O8+C#Wq)( z$n{|FXFE+=k^I|mmgO_M*FbxL3Im%(;vH#@%wsJtA`uVMLeh^szOc{mTZSlzuHZ7r z40bN0{|RyqY(qu>2y>(4#0EXuy#n;$xj(}*?X8mcM#VN!Cm>hsMUp^Ptks%QErcp3 zbyQa;*c(a(KrWhALm5XqgS#sphm44=p+g@roN!P;ofNYf@wN;ojmqcBF9^dh+u^G; z2czQj3Bo!}mSXI25t#fF_349on5OV#lt*Bldvf4ZN!*nU*$xCQ6XE|OuXFVBPHiyFwVw2wb7-loZDZv8P0HplD&Vsqcxp2U&1CM++Pk zaXNYdh|SxY3o7@50~n*>s8%Dlfoua8fg5`%v+K#U7X69ZuT6KudKHf~BPw!1F(rnC zG*L_ya-(bOILx&ZMhcF0KDjLf7o1PsRnl0Vl_Sy`j(47wH)}pg+0>J=7mp)f;rc}D zfW6`jcIhbp2E+eIz@N6^s{{Z2f>6}swD=eIo;9wkP4&?L00D(*LqkwWLqi~N za&Km7Y-Iodc$|HaJ4nM&6o&t%N_|uu>>$!1Lv^wsD&i2Oo=72N!2u9b5%L@B!lF=%nZ(C7zoUTEuwaa6kTg|DSWu1%yV0sb<#%plX(p zipRukZdDAtq8I&CVMHZn>WTDX2A=Ed9zMR`MR}I@y+7B0k~bOP6N%@TZdk+{#50?g z&Uv3W#7dGvd`>)WP=myeT$f#b<6LxD;F%#KlbRj3Jp3b$KS?f`TxBqF zET9S%lH&*egWt0?3saMBQZNQ|zu5N2C=l2MnswX$KDO=V3E+PQuC%tl+5l!hNw2rH z$PqBO4P0EeHF*!X+yRE4bjgq$$xkH|3c&jreNzq?x&`{y+_|;RaryvcXjaQNz`-Ff zR;288k9YTU&h6hjt@-@`h~#px@cGxV00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliruh7)Tc=c>>uEgRkQ(?IY~8OZ>p}z$SV-g0FxGp*5Q8w`z(Hvbm;AYBm=*3 zQeqNW_L6o(Pm%ts{AqhID%=APvIiYF16$wWupV#Z)22vwNa(5l6 Q+5i9m07*qoM6N<$g6y=J-v9sr literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_dark.png b/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..bfeb3e5ed46388fe8e0c5ad79066e376a0e59fee GIT binary patch literal 2276 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 literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-mdpi/ic_add_white.png b/ultrasonic/src/main/res/drawable-mdpi/ic_add_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d83a0e1e3088717349b55d336af55503a495e4de GIT binary patch literal 1505 zcmV<71s?i|P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=KNa@;HohW~Sl9sx-RiQ`}}Gv5t*{Jz+%vzw&- zn(4MPoyLp-3t5u&2sR1(*Z)TNf{RK{A*p#TIY(Tnq{0;wkLOj6o?<%s;YgTwA&KZ&Rs?e16?Z{TLeN8xIj(?#g_bcSnQa4Y6^w1Vcb#s=g)wz#1+D#V~N0wwBHY0!R*jT%Bsyt83s zZXhvbF2;qMOipPw8f>7~#R04F9zZS$U$M~Pw;cW&IXriQDie$mUioedKU(}q3uCmI zB8q%098cJl6V6FQss^yIf-Nr#?I3>-Z46KSvm6* z<7@&5l`IA}*k6E^lA?YrIclh?Xj0XzrXI9t&5~2roU`S5$W;?drk2gjtyp#O6SO!d@EaRD4&*^w%n|FE3J0!(s4}m z+^u^rz3votoD_T7b122VLx5$o&+zp#D?b z`2Wa-iS8NX0?@tX_71f^f9Kjk?A(Pjr%|x`pi|ic?ikVi>iB8sj~Ytgs=sb2_>)5V zH1t~xy^lHYlS2CU3=x<7F-xI7DWp$Bf6fqv4|D#`7~+#c`U?%oz#kC!M>c!|Tm_yL zxq8D-0004nX+uL$Nkc;*aB^>EX>4Tx0C=2zkvmAkP!xv$rb>NO9PA*{AwzYtAS&W0 zRV;#q(pG5I!Q`edXws0RxHt-~1qUCCRRmEM7-$i+r_q{*Y zfRZ;E;1h}Gm~L3a8^kl4md<&fIK)blLVQj5bWxZDATpLEHP9LY~56bium8GTa@7`g@e z*4(+Z&T;wxWN22)H^9LmFjl1Ob&q%Vbk6PHJFWTs0Epyru<-fUu>b%724YJ`L;(K) z{{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2jmG02_Y8jij8>y006;B zL_t(o!|j$^3c@fDMNdl6C8)R#g@ViII$VsK5(M`kg80~h7QFsc%!jEpRtt761mqt4>r$QFfZc;ungoK zfK}tDmXv$foZA?hf&U|64IP0KAYk9dm+a07ePzx~rq1XF&vuLSBOI?m00000NkvXX Hu0mjf=GV5T literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_dark.png b/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1adb28df37019659e39a034a0c252c842266fbbd GIT binary patch literal 1559 zcmV+y2I%>TP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U1s8mg6W4hTmDmECERf!E!K{b7lv<{Qj|tbE)d; z)O0crGv?S}p@={F1c~cE{vPfxT* z1j2ejFUPBujy4t&~|TkY0Yq2fym!2Epz2LZnq*~=fh561b3&a z<+w{(cQ|Do-F$a<21AybVj*GfLgMjcSqkL13+{|33BiwGn`7_LvE&sD3Ay`vm)`r^ z1l=Y1GIEdEKSvMA=d$ebkjuDogu$m1qz`sEm*Lxmq%? z&sG7uAqx&T|>qsH=pnV5E_e{?g_rMh2P)Q4@wmTbx(7`7c71|LTpOc+LE2Kz)#U1 zTZo*s6#yy1+6pl!{y;vGC`IzdY(&Uo0-yrrEP*!=AXRP<$w4HtGfq6$#%GLnUsjGh z%{ZF?LM4iU4fYpcg`lV(3kfw;R5Yk+RMX@%Yl#v=)EJ}1dB_zL3#Jy$ELpaaB!#3Y zCQB*hR5EA58K^mC%PHqv3cG@Z3St%b7g#plWDA>aakDLLc`Fs=Q$y7ntJPF=J_JaiWtMh;bkgw@Cm6&6Al=6ar6jlbO$qO`!~tI@!2JlNb<&POO7Yc2{ywadVoU z;>N!t=O(&WkaIxyk=rNK+WeiXJF#OI&YXI|?t@R^JD8OWm3W#s zqNp0>3t5*H&Rd+-N{zMp;M1& z32;bRa{vGf6951U69E94oEQKA00(qQO+^Rf2@47eAK_qj?EnA(nMp)JR9M69l(7v0 zF%Sf24LJdQfCOFe_43jObbv%aC5RZ7S|9*76xfp{Pu_`3%}_y9xHs81NODn$SQ002ov JPDHLkV1iQo%0~bI literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_light.png b/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7c319e5a9ce7a63616fa74b5a08956d46a80ab55 GIT binary patch literal 1524 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 literal 0 HcmV?d00001 diff --git a/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_dark.png b/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5244b31c4b4ec9ce2e47c08c4eb2e2dadbe03a27 GIT binary patch literal 1634 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 pFK7TE*b9;*Ns=T zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=KHlI$i7hTmC5mVhLL#Bwm0s_Y=k&kv?O-F+@O zr;?XcH53;MEXn!=JrmBq{v6>C6e^iRQuADLjwq?5!W9#bS1GHfm{z|^-b;9Xk%xPM zArg#oJzCxSE9Cn1!ovm~ukzq~g?%fcTk$F~Ezfy>b`o;F*|6Lm@#})gSE{@Rd5rF3oY&XYlo8Rq zGin8!9%``hK*(jimQf8&F}GCd1|koVZ)f!e4LgUSe$s8gju=NcO|xS054!^qrN zmdwStaFfX?%}Rp}^tw2>H9ms~#C-cg%Whfr8aXU?!d50Io2Y+S!_O9fr-m`w3=u`Y zV+Fr><~7`y*ePrxBZQ~uIwU?DMPciN$ zfKbU|V1x4oSScy;$C4w5s){C6&1&jFi`FbTWz9KTo|jxTv1Dr5%-o7q7f-I9-Q2x+ zEnEb5pq5;$cqye;4i$zfbXDxHkQ{u-kq$lbVTT{(s151UQqz{3HE*TW&RsgLiJrT4 z@1@t1!ik&WOsAgtwA0UW)`46bGSbkIhYcTP)SKF-`c3@}YP6~GA~pBfO%2ju_E16d zI?>4t#5fU%+a!R5=E*ENrNoomWEKnKP$YQ0wz|t}SBMDV#Blg3~vID|JD8OWm3W#sqNp0>3t5*H&Rd+-N{zMp;M1&32;bRa{vGf6951U69E94oEQKA00(qQO+^Rf z2@47ZEZHavGynhr=Sf6CRCwC$+RaKEQ5eSYe^XJW>YFss1%EFVOC;AXCm*6`Tw@bfz12NNpgkd5eC zfK6Z>czMrTr-4;qKBe@ns;aBDUt7Ne!|wwdz^6y->d(NJF5F!G4t&OCAlLY6UC%i$ z8Ne7e0r0BEx;fyT0W|mV)(`Gu*BU@WZ)&a^Hdt!_lkzva3jv%pSo_NWnmej_C-+8c z4WOZY;G)L5-@vf}G&iWZ3-*CiA4N1&Rn;)1^c`3M79Y6m0N5_2{4;>@gm9TsI^yQ( z=K}EJKi~O(fIVQVl=9cVY2!W;=R7sq32WT_IJy8oN-0-Jk|arz2eGjn#OwtH4&Po- z;PCAQ1rFa{P%wKz(VGB!L80sg1+y3AIcKs1uoo1}UQjT5LBR&t3yNbes2w5L3u?uY z*j|t%Ns^>zz+R96Jb3u_f&zzcFDP*M_JV@h3yR(Z*b53}FDRJ3AkR6I9e}-{VD^H7 z*$WCbz+O-sdqM38!Cp`+j>PtYBuSDaNs=T + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/list_selector_holo_dark_selected.xml b/ultrasonic/src/main/res/drawable/list_selector_holo_dark_selected.xml new file mode 100644 index 00000000..778ac9de --- /dev/null +++ b/ultrasonic/src/main/res/drawable/list_selector_holo_dark_selected.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/res/drawable/list_selector_holo_light_selected.xml b/ultrasonic/src/main/res/drawable/list_selector_holo_light_selected.xml new file mode 100644 index 00000000..9436f1b0 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/list_selector_holo_light_selected.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/res/drawable/select_ripple.xml b/ultrasonic/src/main/res/drawable/select_ripple.xml new file mode 100644 index 00000000..746466c4 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/select_ripple.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/select_ripple_circle.xml b/ultrasonic/src/main/res/drawable/select_ripple_circle.xml new file mode 100644 index 00000000..2a270deb --- /dev/null +++ b/ultrasonic/src/main/res/drawable/select_ripple_circle.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/server_edit.xml b/ultrasonic/src/main/res/layout/server_edit.xml new file mode 100644 index 00000000..36c6e7ba --- /dev/null +++ b/ultrasonic/src/main/res/layout/server_edit.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +