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 b0e8b248..390566c7 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" @@ -65,10 +66,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 e91fc007..83a20677 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ChatActivity.java @@ -19,7 +19,9 @@ import android.widget.TextView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 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; @@ -33,6 +35,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) @@ -68,8 +75,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..6b5a4e21 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. @@ -80,6 +69,13 @@ public class MainActivity extends SubsonicTabActivity { super.onCreate(savedInstanceState); + // Determine first run and migrate server settings to DB as early as possible + boolean showWelcomeScreen = Util.isFirstRun(this); + boolean areServersMigrated = serverSettingsModel.getValue().migrateFromPreferences(); + + // If there are any servers in the DB, do not show the welcome screen + showWelcomeScreen &= !areServersMigrated; + if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) { setResult(Constants.RESULT_CLOSE_ALL); @@ -105,7 +101,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 +120,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 +159,6 @@ public class MainActivity extends SubsonicTabActivity } list.setAdapter(adapter); - registerForContextMenu(dummyView); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @@ -189,7 +167,7 @@ public class MainActivity extends SubsonicTabActivity { if (view == serverButton) { - dummyView.showContextMenu(); + showServers(); } else if (view == albumsNewestButton) { @@ -259,7 +237,7 @@ public class MainActivity extends SubsonicTabActivity // Remember the current theme. theme = Util.getTheme(this); - showInfoDialog(shouldShowDialog); + showInfoDialog(showWelcomeScreen); } private void loadSettings() @@ -279,14 +257,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 +287,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 +305,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 +318,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 +372,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 769e7e0c..b146794f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java @@ -35,6 +35,7 @@ import android.widget.TextView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 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; @@ -298,7 +299,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) { @@ -441,14 +442,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)); } } @@ -1011,9 +1012,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); } @@ -1208,7 +1209,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 7b838b22..aa6f66e0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java @@ -35,6 +35,8 @@ import android.widget.TextView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 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; @@ -49,8 +51,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; @@ -90,7 +99,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); } @@ -100,7 +109,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 { @@ -146,7 +155,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) @@ -154,7 +163,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); } @@ -173,8 +182,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) { @@ -239,10 +248,10 @@ 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) + if (musicFolderId == null || musicFolderId.isEmpty()) { menuItem.setChecked(true); } @@ -268,7 +277,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter if (downloadMenuItem != null) { - downloadMenuItem.setVisible(!Util.isOffline(this)); + downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this)); } } @@ -315,7 +324,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 eeeccd2d..3d1ada74 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java @@ -44,6 +44,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 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; @@ -123,7 +124,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; } @@ -144,14 +145,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..411e836e 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. @@ -97,6 +98,7 @@ public class JukeboxMediaPlayer running.set(true); startProcessTasks(); + Log.d(TAG, "Started Jukebox Service"); } public void stopJukeboxService() @@ -108,6 +110,7 @@ public class JukeboxMediaPlayer { serviceThread.interrupt(); } + Log.d(TAG, "Stopped Jukebox Service"); } private void startProcessTasks() @@ -158,7 +161,7 @@ public class JukeboxMediaPlayer try { - if (!Util.isOffline(context)) + if (!ActiveServerProvider.Companion.isOffline(context)) { task = tasks.take(); JukeboxStatus status = task.execute(); @@ -319,6 +322,7 @@ public class JukeboxMediaPlayer public void setEnabled(boolean enabled) { + Log.d(TAG, String.format("Jukebox Service setting enabled to %b", enabled)); this.enabled = enabled; tasks.clear(); 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..4008f7b5 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_FIRST_RUN_EXECUTED = "firstRunExecuted"; // 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..c6c1525d 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 isFirstRun(Context context) + { + SharedPreferences preferences = getPreferences(context); + boolean firstExecuted = preferences.getBoolean(Constants.PREFERENCES_KEY_FIRST_RUN_EXECUTED, false); + if (firstExecuted) return false; + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_RUN_EXECUTED, 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..8956610c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt @@ -0,0 +1,342 @@ +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) + currentServerSetting = ServerSetting() + saveButton!!.setOnClickListener { + if (getFields()) { + serverSettingsModel.saveNewItem(currentServerSetting) + finish() + } + } + } + + testButton!!.setOnClickListener { + if (getFields()) { + testConnection() + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finishActivity() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + finishActivity() + } + + 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("@") || + url.host.isNullOrBlank() + ) { + 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.toString() != "http://" || + !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()) + } + } + + /** + * Finishes the Activity, after confirmation from the user if needed + */ + private fun finishActivity() { + 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() + } + } +} 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..78df5476 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt @@ -0,0 +1,234 @@ +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 kotlinx.coroutines.runBlocking +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" + } + + /** + * This function will try and convert settings from the Preferences to the Database + * @return True, if the migration was executed, False otherwise + */ + fun migrateFromPreferences(): Boolean { + var migrated = true + + runBlocking { + val rowCount = repository.count() + + if (rowCount == null || rowCount == 0) { + // First time load up the server settings from the Preferences + val dbServerList = mutableListOf() + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val serverNum = settings.getInt(PREFERENCES_KEY_ACTIVE_SERVERS, 0) + + if (serverNum != 0) { + var index = 1 + for (x in 1 until serverNum + 1) { + val newServerSetting = loadServerSettingFromPreferences(x, index, settings) + if (newServerSetting != null) { + dbServerList.add(newServerSetting) + repository.insert(newServerSetting) + index++ + Log.i( + TAG, + "Imported server from Preferences to Database:" + + " ${newServerSetting.name}" + ) + } + } + } else { + migrated = false + } + } + } + + return migrated + } + + /** + * Retrieves the list of the configured servers from the database. + * This function is asynchronous, uses LiveData to provide the Setting. + */ + fun getServerList(): LiveData> { + viewModelScope.launch { + val dbServerList = repository.loadAllServerSettings().toMutableList() + + 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.count() ?: 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( + preferenceId: Int, + serverId: Int, + settings: SharedPreferences + ): ServerSetting? { + val url = settings.getString(PREFERENCES_KEY_SERVER_URL + preferenceId, "") + val userName = settings.getString(PREFERENCES_KEY_USERNAME + preferenceId, "") + + if (url.isNullOrEmpty() || userName.isNullOrEmpty()) return null + + return ServerSetting( + preferenceId, + serverId, + settings.getString(PREFERENCES_KEY_SERVER_NAME + preferenceId, "")!!, + url, + userName, + settings.getString(PREFERENCES_KEY_PASSWORD + preferenceId, "")!!, + settings.getBoolean(PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + preferenceId, false), + settings.getBoolean( + PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + preferenceId, + false + ), + settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + preferenceId, false), + settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null) + ) + } +} 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..feb1fa19 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -0,0 +1,171 @@ +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 { + withContext(Dispatchers.IO) { + cachedServer = repository.findById(serverId) + } + Log.d( + TAG, + "getActiveServer retrieved from DataBase, id: $serverId; " + + "cachedServer: $cachedServer" + ) + } + + 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..65c365dd --- /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 count of rows in the table + */ + @Query("SELECT COUNT(*) FROM serverSetting") + suspend fun count(): 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 00000000..276717fc Binary files /dev/null and b/ultrasonic/src/main/res/drawable-hdpi/ic_add_white.png differ 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 00000000..2af8f46f Binary files /dev/null and b/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_dark.png differ diff --git a/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_light.png b/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_light.png new file mode 100644 index 00000000..859e9f07 Binary files /dev/null and b/ultrasonic/src/main/res/drawable-hdpi/ic_more_vert_light.png differ diff --git a/ultrasonic/src/main/res/drawable-ldpi/ic_add_white.png b/ultrasonic/src/main/res/drawable-ldpi/ic_add_white.png new file mode 100644 index 00000000..9832384a Binary files /dev/null and b/ultrasonic/src/main/res/drawable-ldpi/ic_add_white.png differ 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 00000000..bfeb3e5e Binary files /dev/null and b/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_dark.png differ diff --git a/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_light.png b/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_light.png new file mode 100644 index 00000000..bbc17d7b Binary files /dev/null and b/ultrasonic/src/main/res/drawable-ldpi/ic_more_vert_light.png differ 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 00000000..d83a0e1e Binary files /dev/null and b/ultrasonic/src/main/res/drawable-mdpi/ic_add_white.png differ 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 00000000..1adb28df Binary files /dev/null and b/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_dark.png differ 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 00000000..7c319e5a Binary files /dev/null and b/ultrasonic/src/main/res/drawable-mdpi/ic_more_vert_light.png differ diff --git a/ultrasonic/src/main/res/drawable-xhdpi/ic_add_white.png b/ultrasonic/src/main/res/drawable-xhdpi/ic_add_white.png new file mode 100644 index 00000000..9ac0b6c1 Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xhdpi/ic_add_white.png differ 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 00000000..5244b31c Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_dark.png differ diff --git a/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_light.png b/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_light.png new file mode 100644 index 00000000..4667234f Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xhdpi/ic_more_vert_light.png differ diff --git a/ultrasonic/src/main/res/drawable-xxhdpi/ic_add_white.png b/ultrasonic/src/main/res/drawable-xxhdpi/ic_add_white.png new file mode 100644 index 00000000..fa20f29a Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xxhdpi/ic_add_white.png differ diff --git a/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_dark.png b/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_dark.png new file mode 100644 index 00000000..ae482b3e Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_dark.png differ diff --git a/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_light.png b/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_light.png new file mode 100644 index 00000000..ebe3065e Binary files /dev/null and b/ultrasonic/src/main/res/drawable-xxhdpi/ic_more_vert_light.png differ diff --git a/ultrasonic/src/main/res/drawable/default_ripple.xml b/ultrasonic/src/main/res/drawable/default_ripple.xml new file mode 100644 index 00000000..90bf9914 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/default_ripple.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ 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..a8f32ccc --- /dev/null +++ b/ultrasonic/src/main/res/layout/server_edit.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +