Updated Server Settings UI and Storage

Updated Koin to latest
This commit is contained in:
Nite 2020-09-18 09:37:19 +02:00
parent 4d8e7f0631
commit 6721500202
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
92 changed files with 2113 additions and 1172 deletions

View File

@ -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()) }
}

View File

@ -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",
]

View File

@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "../gradle_scripts/code_quality.gradle"
@ -66,10 +67,17 @@ dependencies {
implementation androidSupport.support
implementation androidSupport.design
implementation androidSupport.multidex
implementation androidSupport.roomRuntime
implementation androidSupport.roomKtx
implementation androidSupport.viewModelKtx
implementation androidSupport.constraintLayout
implementation other.kotlinStdlib
implementation other.kotlinxCoroutines
implementation other.koinAndroid
implementation other.koinJava
implementation other.koinViewModel
kapt androidSupport.room
testImplementation other.kotlinReflect
testImplementation testing.junit

View File

@ -115,7 +115,12 @@
android:resource="@xml/searchable"/>
</activity>
<activity android:name=".activity.ServerSettingsActivity" />
<activity
android:name=".activity.ServerSelectorActivity"
android:label="@string/server_selector.label" />
<activity
android:name=".activity.EditServerActivity"
android:label="@string/server_editor.label" />
<service
android:name=".service.MediaPlayerService"

View File

@ -31,6 +31,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.DownloadFile;
@ -326,9 +327,9 @@ public class BookmarkActivity extends SubsonicTabActivity
}
playNowButton.setVisibility(enabled && deleteEnabled ? 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);
}

View File

@ -18,7 +18,9 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.service.JukeboxMediaPlayer;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
@ -32,6 +34,10 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Joshua Bahnsen
*/
@ -44,6 +50,7 @@ public final class ChatActivity extends SubsonicTabActivity
private Timer timer;
private volatile static Long lastChatMessageTime = (long) 0;
private volatile static ArrayList<ChatMessage> messageList = new ArrayList<ChatMessage>();
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
protected void onCreate(Bundle bundle)
@ -71,8 +78,8 @@ public final class ChatActivity extends SubsonicTabActivity
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
chatListView.setStackFromBottom(true);
String serverName = Util.getServerName(this, Util.getActiveServer(this));
String userName = Util.getUserName(this, Util.getActiveServer(this));
String serverName = activeServerProvider.getValue().getActiveServer().getName();
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
setActionBarSubtitle(title);

View File

@ -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);

View File

@ -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.

View File

@ -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);

View File

@ -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<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> serverSettingsModel = viewModel(this, ServerSettingsModel.class);
/**
* Called when the activity is first created.
@ -105,7 +94,7 @@ public class MainActivity extends SubsonicTabActivity
final View buttons = LayoutInflater.from(this).inflate(R.layout.main_buttons, null);
final View serverButton = buttons.findViewById(R.id.main_select_server);
final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
final TextView serverTextView = serverButton.findViewById(R.id.main_select_server_2);
final View musicTitle = buttons.findViewById(R.id.main_music);
final View artistsButton = buttons.findViewById(R.id.main_artists_button);
final View albumsButton = buttons.findViewById(R.id.main_albums_button);
@ -124,35 +113,18 @@ public class MainActivity extends SubsonicTabActivity
final View albumsAlphaByNameButton = buttons.findViewById(R.id.main_albums_alphaByName);
final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist);
final View videosButton = buttons.findViewById(R.id.main_videos);
final View dummyView = findViewById(R.id.main_dummy);
boolean shouldShowDialog = false;
if (!getActiveServerEnabled())
{
shouldShowDialog = true;
Util.setActiveServer(this, 0);
}
int instance = Util.getActiveServer(this);
String name = Util.getServerName(this, instance);
if (name == null)
{
shouldShowDialog = true;
Util.setActiveServer(this, 0);
instance = Util.getActiveServer(this);
name = Util.getServerName(this, instance);
}
lastActiveServer = ActiveServerProvider.Companion.getActiveServerId(this);
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
final ListView list = (ListView) findViewById(R.id.main_list);
final ListView list = findViewById(R.id.main_list);
final MergeAdapter adapter = new MergeAdapter();
adapter.addViews(Collections.singletonList(serverButton), true);
if (!Util.isOffline(this))
if (!ActiveServerProvider.Companion.isOffline(this))
{
adapter.addView(musicTitle, false);
adapter.addViews(asList(artistsButton, albumsButton, genresButton), true);
@ -180,7 +152,6 @@ public class MainActivity extends SubsonicTabActivity
}
list.setAdapter(adapter);
registerForContextMenu(dummyView);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@ -189,7 +160,7 @@ public class MainActivity extends SubsonicTabActivity
{
if (view == serverButton)
{
dummyView.showContextMenu();
showServers();
}
else if (view == albumsNewestButton)
{
@ -259,6 +230,9 @@ public class MainActivity extends SubsonicTabActivity
// Remember the current theme.
theme = Util.getTheme(this);
boolean shouldShowDialog = Util.shouldShowWelcomeScreen(this);
// This will convert the server settings from the Preferences to the DB
if (shouldShowDialog) serverSettingsModel.getValue().getServerList();
showInfoDialog(shouldShowDialog);
}
@ -279,14 +253,24 @@ public class MainActivity extends SubsonicTabActivity
protected void onResume()
{
super.onResume();
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainActivity.this);
int currentActiveServer = ActiveServerProvider.Companion.getActiveServerId(MainActivity.this);
if (id3 != shouldUseId3)
{
shouldUseId3 = id3;
restart();
shouldRestart = true;
}
if (currentActiveServer != lastActiveServer)
{
lastActiveServer = currentActiveServer;
shouldRestart = true;
}
if (shouldRestart) restart();
}
@Override
@ -299,148 +283,6 @@ public class MainActivity extends SubsonicTabActivity
return true;
}
@Override
public void onCreateContextMenu(final ContextMenu menu, final View view, final ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
final int activeServer = Util.getActiveServer(this);
boolean checked = false;
for (int i = 0; i <= Util.getActiveServers(this); i++)
{
final String serverName = Util.getServerName(this, i);
if (serverName == null)
{
continue;
}
if (Util.getServerEnabled(this, i))
{
final int menuItemNum = getMenuItem(i);
final MenuItem menuItem = menu.add(MENU_GROUP_SERVER, menuItemNum, menuItemNum, serverName);
if (activeServer == i)
{
checked = true;
menuItem.setChecked(true);
}
}
}
if (!checked)
{
MenuItem menuItem = menu.findItem(getMenuItem(0));
if (menuItem != null)
{
menuItem.setChecked(true);
}
}
menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
menu.setHeaderTitle(R.string.main_select_server);
}
private boolean getActiveServerEnabled()
{
final int activeServer = Util.getActiveServer(this);
boolean activeServerEnabled = false;
for (int i = 0; i <= Util.getActiveServers(this); i++)
{
if (Util.getServerEnabled(this, i))
{
if (activeServer == i)
{
activeServerEnabled = true;
}
}
}
return activeServerEnabled;
}
private static int getMenuItem(final int serverInstance)
{
switch (serverInstance)
{
case 0:
return MENU_ITEM_OFFLINE;
case 1:
return MENU_ITEM_SERVER_1;
case 2:
return MENU_ITEM_SERVER_2;
case 3:
return MENU_ITEM_SERVER_3;
case 4:
return MENU_ITEM_SERVER_4;
case 5:
return MENU_ITEM_SERVER_5;
case 6:
return MENU_ITEM_SERVER_6;
case 7:
return MENU_ITEM_SERVER_7;
case 8:
return MENU_ITEM_SERVER_8;
case 9:
return MENU_ITEM_SERVER_9;
case 10:
return MENU_ITEM_SERVER_10;
}
return 0;
}
@Override
public boolean onContextItemSelected(final MenuItem menuItem)
{
switch (menuItem.getItemId())
{
case MENU_ITEM_OFFLINE:
setActiveServer(0);
break;
case MENU_ITEM_SERVER_1:
setActiveServer(1);
break;
case MENU_ITEM_SERVER_2:
setActiveServer(2);
break;
case MENU_ITEM_SERVER_3:
setActiveServer(3);
break;
case MENU_ITEM_SERVER_4:
setActiveServer(4);
break;
case MENU_ITEM_SERVER_5:
setActiveServer(5);
break;
case MENU_ITEM_SERVER_6:
setActiveServer(6);
break;
case MENU_ITEM_SERVER_7:
setActiveServer(7);
break;
case MENU_ITEM_SERVER_8:
setActiveServer(8);
break;
case MENU_ITEM_SERVER_9:
setActiveServer(9);
break;
case MENU_ITEM_SERVER_10:
setActiveServer(10);
break;
default:
return super.onContextItemSelected(menuItem);
}
// Restart activity
restart();
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
@ -459,26 +301,6 @@ public class MainActivity extends SubsonicTabActivity
return false;
}
private void setActiveServer(final int instance)
{
final MediaPlayerController service = getMediaPlayerController();
if (Util.getActiveServer(this) != instance)
{
if (service != null)
{
service.clearIncomplete();
}
}
Util.setActiveServer(this, instance);
if (service != null)
{
service.setJukeboxEnabled(Util.getJukeboxEnabled(this, instance));
}
}
private void exit()
{
lifecycleSupport.getValue().onDestroy();
@ -492,9 +314,9 @@ public class MainActivity extends SubsonicTabActivity
{
infoDialogDisplayed = true;
if (show || Util.getRestUrl(this, null).contains("yourhost"))
if (show)
{
Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text);
Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text_new);
}
}
}
@ -546,7 +368,13 @@ public class MainActivity extends SubsonicTabActivity
startActivityForResultWithoutTransition(this, intent);
}
/**
private void showServers()
{
final Intent intent = new Intent(this, ServerSelectorActivity.class);
startActivityForResult(intent, 0);
}
/**
* Temporary task to make a ping to server to get it supported api version.
*/
private static class PingTask extends TabActivityBackgroundTask<Void> {

View File

@ -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)
{

View File

@ -37,6 +37,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.DownloadFile;
@ -329,7 +330,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity
}
else
{
if (!Util.isOffline(SelectAlbumActivity.this) && Util.getShouldUseId3Tags(SelectAlbumActivity.this))
if (!ActiveServerProvider.Companion.isOffline(SelectAlbumActivity.this) && Util.getShouldUseId3Tags(SelectAlbumActivity.this))
{
if (isAlbum)
{
@ -472,14 +473,14 @@ public class SelectAlbumActivity extends SubsonicTabActivity
if (shareButton != null)
{
shareButton.setVisible(!Util.isOffline(this));
shareButton.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!Util.isOffline(this));
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
}
@ -1042,9 +1043,9 @@ public class SelectAlbumActivity extends SubsonicTabActivity
playNowButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
playNextButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
playLastButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
pinButton.setVisibility((enabled && !Util.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE);
downloadButton.setVisibility(enabled && !deleteEnabled && !Util.isOffline(this) ? View.VISIBLE : View.GONE);
downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(this) ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
}
@ -1239,7 +1240,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity
boolean isAlbumList = getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
playAllButtonVisible = !(isAlbumList || entries.isEmpty()) && !allVideos;
shareButtonVisible = !Util.isOffline(SelectAlbumActivity.this) && songCount > 0;
shareButtonVisible = !ActiveServerProvider.Companion.isOffline(SelectAlbumActivity.this) && songCount > 0;
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);

View File

@ -36,6 +36,8 @@ import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.Indexes;
import org.moire.ultrasonic.domain.MusicFolder;
@ -50,8 +52,15 @@ import org.moire.ultrasonic.view.ArtistAdapter;
import java.util.ArrayList;
import java.util.List;
import kotlin.Lazy;
import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel;
import static org.koin.java.KoinJavaComponent.inject;
public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> serverSettingsModel = viewModel(this, ServerSettingsModel.class);
private static final int MENU_GROUP_MUSIC_FOLDER = 10;
@ -91,7 +100,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2);
}
if (!Util.isOffline(this) && !Util.getShouldUseId3Tags(this))
if (!ActiveServerProvider.Companion.isOffline(this) && !Util.getShouldUseId3Tags(this))
{
artistListView.addHeaderView(folderButton);
}
@ -101,7 +110,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE);
if (title == null)
{
setActionBarSubtitle(Util.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
setActionBarSubtitle(ActiveServerProvider.Companion.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
}
else
{
@ -147,7 +156,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
boolean isOffline = Util.isOffline(SelectArtistActivity.this);
boolean isOffline = ActiveServerProvider.Companion.isOffline(SelectArtistActivity.this);
boolean useId3Tags = Util.getShouldUseId3Tags(SelectArtistActivity.this);
if (!isOffline && !useId3Tags)
@ -155,7 +164,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
}
String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
return !isOffline && useId3Tags ? musicService.getArtists(refresh, SelectArtistActivity.this, this) : musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
}
@ -174,8 +183,8 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
// Display selected music folder
if (musicFolders != null)
{
String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
if (musicFolderId == null)
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
if (musicFolderId == null || musicFolderId.equals(""))
{
if (folderName != null)
{
@ -240,7 +249,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
}
else if (info.position == 1)
{
String musicFolderId = Util.getSelectedMusicFolderId(this);
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
if (musicFolderId == null)
@ -269,7 +278,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!Util.isOffline(this));
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
}
@ -316,7 +325,13 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders) : selectedFolder.getName();
Util.setSelectedMusicFolderId(this, musicFolderId);
if (!ActiveServerProvider.Companion.isOffline(this)) {
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
currentSetting.setMusicFolderId(musicFolderId);
serverSettingsModel.getValue().updateItem(currentSetting);
}
folderName.setText(musicFolderName);
refresh();
}

View File

@ -46,6 +46,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
@ -125,7 +126,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
if (!Util.isOffline(SelectPlaylistActivity.this))
if (!ActiveServerProvider.Companion.isOffline(SelectPlaylistActivity.this))
new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists);
return playlists;
}
@ -146,14 +147,14 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getMenuInflater();
if (Util.isOffline(this)) inflater.inflate(R.menu.select_playlist_context_offline, menu);
if (ActiveServerProvider.Companion.isOffline(this)) inflater.inflate(R.menu.select_playlist_context_offline, menu);
else inflater.inflate(R.menu.select_playlist_context, menu);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!Util.isOffline(this));
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
}

View File

@ -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);
}
}
}

View File

@ -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<Entry> songs = new LinkedList<Entry>();
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);
}

View File

@ -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<Boolean> task = new ModalBackgroundTask<Boolean>(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();
}
}

View File

@ -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<MediaPlayerController> 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() {

View File

@ -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
{

View File

@ -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

View File

@ -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
{

View File

@ -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> 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();

View File

@ -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

View File

@ -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;

View File

@ -30,6 +30,7 @@ import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.JukeboxStatus;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.util.Util;
@ -47,7 +48,7 @@ import java.util.concurrent.atomic.AtomicLong;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Provides an asynchronous interface to the remote jukebox on the Subsonic server.
@ -158,7 +159,7 @@ public class JukeboxMediaPlayer
try
{
if (!Util.isOffline(context))
if (!ActiveServerProvider.Companion.isOffline(context))
{
task = tasks.take();
JukeboxStatus status = task.execute();

View File

@ -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())

View File

@ -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> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private Lazy<ActiveServerProvider> 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();
}

View File

@ -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;

View File

@ -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> 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<MusicDirectory.Entry> 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

View File

@ -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> 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);

View File

@ -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;
}

View File

@ -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> downloader = inject(Downloader.class);
private Lazy<ActiveServerProvider> 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<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server));
List<Playlist> playlists = params[0];
for (Playlist playlist : playlists)

View File

@ -73,20 +73,8 @@ public final class Constants
public static final int NOTIFICATION_ID_PLAYING = 100;
// Preferences keys.
public static final String PREFERENCES_KEY_SERVER = "server";
public static final String PREFERENCES_KEY_SERVER_ENABLED = "serverEnabled";
public static final String PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled";
public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
public static final String PREFERENCES_KEY_SERVERS_KEY = "serversKey";
public static final String PREFERENCES_KEY_ADD_SERVER = "addServer";
public static final String PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers";
public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
public static final String PREFERENCES_KEY_USERNAME = "username";
public static final String PREFERENCES_KEY_PASSWORD = "password";
public static final String PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate";
public static final String PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport";
public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
public static final String PREFERENCES_KEY_THEME = "theme";
public static final String PREFERENCES_KEY_DISPLAY_BITRATE_WITH_ARTIST = "displayBitrateWithArtist";
@ -142,6 +130,7 @@ public final class Constants
public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader";
public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating";
public static final String PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory";
public static final String PREFERENCES_KEY_WELCOME_SCREEN_SHOWN = "welcomeScreenShown";
// Number of free trial days for non-licensed servers.
public static final int FREE_TRIAL_DAYS = 30;

View File

@ -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();
}
}

View File

@ -24,6 +24,7 @@ import android.app.PendingIntent;
import android.content.*;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -31,7 +32,6 @@ import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
@ -42,10 +42,14 @@ import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
@ -54,8 +58,6 @@ import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.io.*;
import java.security.MessageDigest;
@ -71,9 +73,8 @@ import java.util.regex.Pattern;
* @author Sindre Mehus
* @version $Id$
*/
public class Util extends DownloadActivity
public class Util
{
private static final String TAG = Util.class.getSimpleName();
private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB");
@ -107,11 +108,6 @@ public class Util extends DownloadActivity
{
}
public static boolean isOffline(Context context)
{
return context == null || getActiveServer(context) == 0;
}
public static boolean isScreenLitOnDownload(Context context)
{
SharedPreferences preferences = getPreferences(context);
@ -132,26 +128,6 @@ public class Util extends DownloadActivity
editor.commit();
}
public static boolean isScrobblingEnabled(Context context)
{
if (isOffline(context))
{
return false;
}
SharedPreferences preferences = getPreferences(context);
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false);
}
public static boolean isServerScalingEnabled(Context context)
{
if (isOffline(context))
{
return false;
}
SharedPreferences preferences = getPreferences(context);
return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_SCALING, false);
}
public static boolean isNotificationEnabled(Context context)
{
// After API26 foreground services must be used for music playback, and they must have a notification
@ -174,152 +150,6 @@ public class Util extends DownloadActivity
return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false);
}
public static void setActiveServer(
Context context,
int instance
) {
MusicServiceFactory.resetMusicService();
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
editor.apply();
}
public static int getActiveServer(Context context)
{
SharedPreferences preferences = getPreferences(context);
return preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
}
public static int getActiveServers(Context context)
{
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
return settings.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 3);
}
public static String getServerName(Context context)
{
int instance = getActiveServer(context);
if (instance == 0)
{
return context.getResources().getString(R.string.main_offline);
}
SharedPreferences preferences = getPreferences(context);
return preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
}
public static String getServerName(Context context, int instance)
{
if (instance == 0)
{
return context.getResources().getString(R.string.main_offline);
}
SharedPreferences preferences = getPreferences(context);
return preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
}
public static String getUserName(Context context, int instance)
{
if (instance == 0)
{
return context.getResources().getString(R.string.main_offline);
}
SharedPreferences preferences = getPreferences(context);
return preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
}
public static boolean getServerEnabled(Context context, int instance)
{
if (instance == 0)
{
return true;
}
SharedPreferences preferences = getPreferences(context);
return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + instance, true);
}
public static boolean getJukeboxEnabled(Context context, int instance)
{
if (instance == 0)
{
return false;
}
SharedPreferences preferences = getPreferences(context);
return preferences.getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + instance, false);
}
public static void setServerRestVersion(Context context, Version version)
{
SERVER_REST_VERSIONS.put(getActiveServer(context), version);
}
public static Version getServerRestVersion(Context context)
{
return SERVER_REST_VERSIONS.get(getActiveServer(context));
}
public static void removeInstanceName(Context context, int instance, int activeInstance)
{
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
int newInstance = instance + 1;
String server = preferences.getString(Constants.PREFERENCES_KEY_SERVER + newInstance, null);
String serverName = preferences.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
String userName = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
boolean serverEnabled = preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + newInstance, true);
boolean jukeboxEnabled = preferences.getBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + newInstance, true);
editor.putString(Constants.PREFERENCES_KEY_SERVER + instance, server);
editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName);
editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl);
editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName);
editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password);
editor.putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + instance, serverEnabled);
editor.putBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + instance, jukeboxEnabled);
editor.putString(Constants.PREFERENCES_KEY_SERVER + newInstance, null);
editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
editor.putBoolean(Constants.PREFERENCES_KEY_SERVER_ENABLED + newInstance, true);
editor.putBoolean(Constants.PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + newInstance, false);
editor.commit();
if (instance == activeInstance)
{
Util.setActiveServer(context, 0);
}
if (newInstance == activeInstance)
{
Util.setActiveServer(context, instance);
}
}
public static void setSelectedMusicFolderId(Context context, String musicFolderId)
{
int instance = getActiveServer(context);
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
editor.commit();
}
public static String getSelectedMusicFolderId(Context context)
{
SharedPreferences preferences = getPreferences(context);
int instance = getActiveServer(context);
return preferences.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
}
public static String getTheme(Context context)
{
SharedPreferences preferences = getPreferences(context);
@ -356,35 +186,6 @@ public class Util extends DownloadActivity
return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
}
public static String getRestUrl(Context context, String method)
{
StringBuilder builder = new StringBuilder(8192);
SharedPreferences preferences = getPreferences(context);
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
String username = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
// Slightly obfuscate password
password = "enc:" + Util.utf8HexEncode(password);
builder.append(serverUrl);
if (builder.charAt(builder.length() - 1) != '/')
{
builder.append('/');
}
builder.append("rest/").append(method).append(".view");
builder.append("?u=").append(username);
builder.append("&p=").append(password);
builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
builder.append("&c=").append(Constants.REST_CLIENT_ID);
return builder.toString();
}
public static SharedPreferences getPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@ -1619,4 +1420,31 @@ public class Util extends DownloadActivity
SharedPreferences preferences = getPreferences(context);
return Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY, "5"));
}
public static @ColorInt int getColorFromAttribute(Context context, int resId)
{
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(resId, typedValue, true);
return typedValue.data;
}
public static int getResourceFromAttribute(Context context, int resId)
{
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(resId, typedValue, true);
return typedValue.resourceId;
}
public static boolean shouldShowWelcomeScreen(Context context)
{
SharedPreferences preferences = getPreferences(context);
boolean shown = preferences.getBoolean(Constants.PREFERENCES_KEY_WELCOME_SCREEN_SHOWN, false);
if (shown) return false;
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(Constants.PREFERENCES_KEY_WELCOME_SCREEN_SHOWN, true);
editor.apply();
return true;
}
}

View File

@ -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);
}

View File

@ -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<ChatMessage>
{
private final SubsonicTabActivity activity;
@ -27,6 +32,8 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
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> activeServerProvider = inject(ActiveServerProvider.class);
public ChatAdapter(SubsonicTabActivity activity, List<ChatMessage> messages)
{
super(activity, R.layout.chat_item, messages);
@ -62,7 +69,7 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
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;

View File

@ -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)
{

View File

@ -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

View File

@ -0,0 +1,327 @@
package org.moire.ultrasonic.activity
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.widget.Button
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputLayout
import java.io.IOException
import java.net.MalformedURLException
import java.net.URL
import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.service.SubsonicRESTException
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ErrorDialog
import org.moire.ultrasonic.util.ModalBackgroundTask
import org.moire.ultrasonic.util.Util
import retrofit2.Response
/**
* This Activity provides a Form which can be used to edit the properties of a Server Setting.
* It can also be used to create a Server Setting from scratch.
* Contains functions for testing the configured Server Setting
*/
internal class EditServerActivity : AppCompatActivity() {
companion object {
private val TAG = EditServerActivity::class.simpleName
const val EDIT_SERVER_INTENT_INDEX = "index"
}
private val serverSettingsModel: ServerSettingsModel by viewModel()
private var currentServerSetting: ServerSetting? = null
private var serverNameEditText: TextInputLayout? = null
private var serverAddressEditText: TextInputLayout? = null
private var userNameEditText: TextInputLayout? = null
private var passwordEditText: TextInputLayout? = null
private var selfSignedSwitch: SwitchMaterial? = null
private var ldapSwitch: SwitchMaterial? = null
private var jukeboxSwitch: SwitchMaterial? = null
private var saveButton: Button? = null
private var testButton: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyTheme()
if (savedInstanceState == null) configureActionBar()
setContentView(R.layout.server_edit)
serverNameEditText = findViewById(R.id.edit_server_name)
serverAddressEditText = findViewById(R.id.edit_server_address)
userNameEditText = findViewById(R.id.edit_server_username)
passwordEditText = findViewById(R.id.edit_server_password)
selfSignedSwitch = findViewById(R.id.edit_self_signed)
ldapSwitch = findViewById(R.id.edit_ldap)
jukeboxSwitch = findViewById(R.id.edit_jukebox)
saveButton = findViewById(R.id.edit_save)
testButton = findViewById(R.id.edit_test)
val index = intent.getIntExtra(EDIT_SERVER_INTENT_INDEX, -1)
if (index != -1) {
// Editing an existing server
setTitle(R.string.server_editor_label)
val serverSetting = serverSettingsModel.getServerSetting(index)
serverSetting.observe(
this,
Observer { t ->
if (t != null) {
currentServerSetting = t
setFields()
}
}
)
saveButton!!.setOnClickListener {
if (currentServerSetting != null) {
if (getFields()) {
serverSettingsModel.updateItem(currentServerSetting)
finish()
}
}
}
} else {
// Creating a new server
setTitle(R.string.server_editor_new_label)
saveButton!!.setOnClickListener {
currentServerSetting = ServerSetting()
if (getFields()) {
serverSettingsModel.saveNewItem(currentServerSetting)
finish()
}
}
}
testButton!!.setOnClickListener {
if (getFields()) {
testConnection()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
if (areFieldsChanged()) {
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.common_confirm)
.setMessage(R.string.server_editor_leave_confirmation)
.setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
finish()
}
.setNegativeButton(R.string.common_cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
} else {
finish()
}
return true
}
return super.onOptionsItemSelected(item)
}
private fun applyTheme() {
val theme = Util.getTheme(this)
if (
"dark".equals(theme, ignoreCase = true) ||
"fullscreen".equals(theme, ignoreCase = true)
) {
setTheme(R.style.UltraSonicTheme)
} else if (
"light".equals(theme, ignoreCase = true) ||
"fullscreenlight".equals(theme, ignoreCase = true)
) {
setTheme(R.style.UltraSonicTheme_Light)
}
}
private fun configureActionBar() {
val actionBar: ActionBar? = supportActionBar
if (actionBar != null) {
actionBar.setDisplayShowHomeEnabled(true)
actionBar.setDisplayHomeAsUpEnabled(true)
}
}
/**
* Sets the values of the Form from the current Server Setting instance
*/
private fun setFields() {
if (currentServerSetting == null) return
serverNameEditText!!.editText?.setText(currentServerSetting!!.name)
serverAddressEditText!!.editText?.setText(currentServerSetting!!.url)
userNameEditText!!.editText?.setText(currentServerSetting!!.userName)
passwordEditText!!.editText?.setText(currentServerSetting!!.password)
selfSignedSwitch!!.isChecked = currentServerSetting!!.allowSelfSignedCertificate
ldapSwitch!!.isChecked = currentServerSetting!!.ldapSupport
jukeboxSwitch!!.isChecked = currentServerSetting!!.jukeboxByDefault
}
/**
* Retrieves the values in the Form to the current Server Setting instance
* This function also does some basic validation on the fields
*/
private fun getFields(): Boolean {
if (currentServerSetting == null) return false
var isValid = true
var url: URL? = null
if (serverAddressEditText!!.editText?.text.isNullOrBlank()) {
serverAddressEditText!!.error = getString(R.string.server_editor_required)
isValid = false
} else {
try {
val urlString = serverAddressEditText!!.editText?.text.toString()
url = URL(urlString)
if (urlString != urlString.trim(' ') || urlString.contains("@")) {
throw MalformedURLException()
}
serverAddressEditText!!.error = null
} catch (exception: MalformedURLException) {
serverAddressEditText!!.error = getString(R.string.settings_invalid_url)
isValid = false
}
}
if (serverNameEditText!!.editText?.text.isNullOrBlank()) {
if (isValid && url != null) {
serverNameEditText!!.editText?.setText(url.host)
}
}
if (userNameEditText!!.editText?.text.isNullOrBlank()) {
userNameEditText!!.error = getString(R.string.server_editor_required)
isValid = false
} else {
userNameEditText!!.error = null
}
if (isValid) {
currentServerSetting!!.name = serverNameEditText!!.editText?.text.toString()
currentServerSetting!!.url = serverAddressEditText!!.editText?.text.toString()
currentServerSetting!!.userName = userNameEditText!!.editText?.text.toString()
currentServerSetting!!.password = passwordEditText!!.editText?.text.toString()
currentServerSetting!!.allowSelfSignedCertificate = selfSignedSwitch!!.isChecked
currentServerSetting!!.ldapSupport = ldapSwitch!!.isChecked
currentServerSetting!!.jukeboxByDefault = jukeboxSwitch!!.isChecked
}
return isValid
}
/**
* Checks whether any value in the fields are changed according to their original values.
*/
private fun areFieldsChanged(): Boolean {
if (currentServerSetting == null) {
return !serverNameEditText!!.editText?.text!!.isBlank() ||
!serverAddressEditText!!.editText?.text!!.isBlank() ||
!userNameEditText!!.editText?.text!!.isBlank() ||
!passwordEditText!!.editText?.text!!.isBlank()
}
return currentServerSetting!!.name != serverNameEditText!!.editText?.text.toString() ||
currentServerSetting!!.url != serverAddressEditText!!.editText?.text.toString() ||
currentServerSetting!!.userName != userNameEditText!!.editText?.text.toString() ||
currentServerSetting!!.password != passwordEditText!!.editText?.text.toString() ||
currentServerSetting!!.allowSelfSignedCertificate != selfSignedSwitch!!.isChecked ||
currentServerSetting!!.ldapSupport != ldapSwitch!!.isChecked ||
currentServerSetting!!.jukeboxByDefault != jukeboxSwitch!!.isChecked
}
/**
* Tests if the network connection to the entered Server Settings can be made
*/
private fun testConnection() {
val task: ModalBackgroundTask<Boolean> = object : ModalBackgroundTask<Boolean>(
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<out SubsonicResponse?>) {
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())
}
}
}

View File

@ -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<ServerSetting>,
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<ServerSetting>) {
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<TextView>(R.id.server_name)
val description = vi?.findViewById<TextView>(R.id.server_description)
val layout = vi?.findViewById<RelativeLayout>(R.id.server_layout)
val image = vi?.findViewById<ImageView>(R.id.server_image)
val serverMenu = vi?.findViewById<ImageButton>(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
}
}
}

View File

@ -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<FloatingActionButton>(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)
}
}

View File

@ -0,0 +1,203 @@
package org.moire.ultrasonic.activity
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.data.ServerSettingDao
/**
* ViewModel to be used in Activities which will handle Server Settings
*/
class ServerSettingsModel(
private val repository: ServerSettingDao,
private val activeServerProvider: ActiveServerProvider,
private val context: Context
) : ViewModel() {
private var serverList: MutableLiveData<List<ServerSetting>> = MutableLiveData()
companion object {
private val TAG = ServerSettingsModel::class.simpleName
// These constants were removed from Constants.java as they are deprecated and only used here
private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled"
private const val PREFERENCES_KEY_SERVER_NAME = "serverName"
private const val PREFERENCES_KEY_SERVER_URL = "serverUrl"
private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers"
private const val PREFERENCES_KEY_USERNAME = "username"
private const val PREFERENCES_KEY_PASSWORD = "password"
private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate"
private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport"
private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"
}
/**
* Retrieves the list of the configured servers from the database.
* This function will also try to convert existing settings from the Preferences
* This function is asynchronous, uses LiveData to provide the Setting.
*/
fun getServerList(): LiveData<List<ServerSetting>> {
viewModelScope.launch {
val dbServerList = repository.loadAllServerSettings().toMutableList()
if (dbServerList.isEmpty()) {
// First time load up the server settings from the Preferences
val settings = PreferenceManager.getDefaultSharedPreferences(context)
val serverNum = settings.getInt(PREFERENCES_KEY_ACTIVE_SERVERS, 0)
if (serverNum != 0) {
for (x in 1 until serverNum + 1) {
val newServerSetting = loadServerSettingFromPreferences(x, settings)
dbServerList.add(newServerSetting)
repository.insert(newServerSetting)
Log.i(
TAG,
"Imported server from Preferences to Database: ${newServerSetting.name}"
)
}
}
}
dbServerList.add(0, ServerSetting(context.getString(R.string.main_offline), ""))
serverList.value = dbServerList
}
return serverList
}
/**
* Retrieves a single Server Setting by its index
* This function is asynchronous, uses LiveData to provide the Setting.
*/
fun getServerSetting(index: Int): LiveData<ServerSetting?> {
val result = MutableLiveData<ServerSetting?>()
viewModelScope.launch {
val dbServer = repository.findByIndex(index)
result.value = dbServer
Log.d(TAG, "getServerSetting($index) returning $dbServer")
}
return result
}
/**
* Moves a Setting up in the Server List by decreasing its index
*/
fun moveItemUp(index: Int) {
if (index == 1) return
val itemToBeMoved = serverList.value?.single { setting -> setting.index == index }
val previousItem = serverList.value?.single { setting -> setting.index == index - 1 }
itemToBeMoved?.index = previousItem!!.index
previousItem.index = index
viewModelScope.launch {
repository.update(itemToBeMoved!!, previousItem)
}
activeServerProvider.invalidateCache()
// Notify the observers of the changed values
serverList.value = serverList.value
}
/**
* Moves a Setting down in the Server List by increasing its index
*/
fun moveItemDown(index: Int) {
if (index == (serverList.value!!.size - 1)) return
val itemToBeMoved = serverList.value?.single { setting -> setting.index == index }
val nextItem = serverList.value?.single { setting -> setting.index == index + 1 }
itemToBeMoved?.index = nextItem!!.index
nextItem.index = index
viewModelScope.launch {
repository.update(itemToBeMoved!!, nextItem)
}
activeServerProvider.invalidateCache()
// Notify the observers of the changed values
serverList.value = serverList.value
}
/**
* Removes a Setting from the database
*/
fun deleteItem(index: Int) {
if (index == 0) return
val newList = serverList.value!!.toMutableList()
val itemToBeDeleted = newList.single { setting -> setting.index == index }
newList.remove(itemToBeDeleted)
for (x in index + 1 until newList.size + 1) {
newList.single { setting -> setting.index == x }.index--
}
viewModelScope.launch {
repository.delete(itemToBeDeleted)
for (x in index until newList.size) {
repository.update(newList.single { setting -> setting.index == x })
}
}
activeServerProvider.invalidateCache()
serverList.value = newList
Log.d(TAG, "deleteItem deleted index: $index")
}
/**
* Updates a Setting in the database
*/
fun updateItem(serverSetting: ServerSetting?) {
if (serverSetting == null) return
viewModelScope.launch {
repository.update(serverSetting)
activeServerProvider.invalidateCache()
Log.d(TAG, "updateItem updated server setting: $serverSetting")
}
}
/**
* Inserts a new Setting into the database
*/
fun saveNewItem(serverSetting: ServerSetting?) {
if (serverSetting == null) return
viewModelScope.launch {
serverSetting.index = (repository.getMaxIndex() ?: 0) + 1
serverSetting.id = serverSetting.index
repository.insert(serverSetting)
Log.d(TAG, "saveNewItem saved server setting: $serverSetting")
}
}
/**
* Reads up a Server Setting stored in the obsolete Preferences
*/
private fun loadServerSettingFromPreferences(
id: Int,
settings: SharedPreferences
): ServerSetting {
return ServerSetting(
id,
id,
settings.getString(PREFERENCES_KEY_SERVER_NAME + id, "")!!,
settings.getString(PREFERENCES_KEY_SERVER_URL + id, "")!!,
settings.getString(PREFERENCES_KEY_USERNAME + id, "")!!,
settings.getString(PREFERENCES_KEY_PASSWORD + id, "")!!,
settings.getBoolean(PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + id, false),
settings.getBoolean(PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + id, false),
settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + id, false),
settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + id, "")!!
)
}
}

View File

@ -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
)
)
}
}
}

View File

@ -0,0 +1,167 @@
package org.moire.ultrasonic.data
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
/**
* This class can be used to retrieve the properties of the Active Server
* It caches the settings read up from the DB to improve performance.
*/
class ActiveServerProvider(
private val repository: ServerSettingDao,
private val context: Context
) {
private var cachedServer: ServerSetting? = null
/**
* Get the settings of the current Active Server
* @return The Active Server Settings
*/
fun getActiveServer(): ServerSetting {
val serverId = getActiveServerId(context)
if (serverId > 0) {
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
// Ideally this is the only call where we block the thread while using the repository
runBlocking {
Log.d(TAG, "getActiveServer retrieving from DataBase, id: $serverId")
withContext(Dispatchers.IO) {
cachedServer = repository.findById(serverId)
}
}
if (cachedServer != null) return cachedServer!!
setActiveServerId(context, 0)
}
return ServerSetting(
id = -1,
index = 0,
name = context.getString(R.string.main_offline),
url = "http://localhost",
userName = "",
password = "",
jukeboxByDefault = false,
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = ""
)
}
/**
* Sets the Active Server by the Server Index in the Server Selector List
* @param index: The index of the Active Server in the Server Selector List
*/
fun setActiveServerByIndex(index: Int) {
Log.d(TAG, "setActiveServerByIndex $index")
if (index < 1) {
// Offline mode is selected
setActiveServerId(context, 0)
return
}
GlobalScope.launch(Dispatchers.IO) {
val serverId = repository.findByIndex(index)!!.id
setActiveServerId(context, serverId)
}
}
/**
* Invalidates the Active Server Setting cache
* This should be called when the Active Server or one of its properties changes
*/
fun invalidateCache() {
Log.d(TAG, "Cache is invalidated")
cachedServer = null
}
/**
* Gets the Rest Url of the Active Server
* @param method: The Rest resource to use
* @return The Rest Url of the method on the server
*/
fun getRestUrl(method: String?): String? {
val builder = StringBuilder(8192)
val activeServer = getActiveServer()
val serverUrl: String = activeServer.url
val username: String = activeServer.userName
var password: String = activeServer.password
// Slightly obfuscate password
password = "enc:" + Util.utf8HexEncode(password)
builder.append(serverUrl)
if (builder[builder.length - 1] != '/') {
builder.append('/')
}
builder.append("rest/").append(method).append(".view")
builder.append("?u=").append(username)
builder.append("&p=").append(password)
builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION)
builder.append("&c=").append(Constants.REST_CLIENT_ID)
return builder.toString()
}
companion object {
private val TAG = ActiveServerProvider::class.simpleName
/**
* Queries if the Active Server is the "Offline" mode of Ultrasonic
* @return True, if the "Offline" mode is selected
*/
fun isOffline(context: Context?): Boolean {
return context == null || getActiveServerId(context) < 1
}
/**
* Queries the Id of the Active Server
*/
fun getActiveServerId(context: Context): Int {
val preferences = Util.getPreferences(context)
return preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1)
}
/**
* Sets the Id of the Active Server
*/
fun setActiveServerId(context: Context, serverId: Int) {
resetMusicService()
val preferences = Util.getPreferences(context)
val editor = preferences.edit()
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, serverId)
editor.apply()
}
/**
* Queries if Scrobbling is enabled
*/
fun isScrobblingEnabled(context: Context): Boolean {
if (isOffline(context)) {
return false
}
val preferences = Util.getPreferences(context)
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
}
/**
* Queries if Server Scaling is enabled
*/
fun isServerScalingEnabled(context: Context): Boolean {
if (isOffline(context)) {
return false
}
val preferences = Util.getPreferences(context)
return preferences.getBoolean(Constants.PREFERENCES_KEY_SERVER_SCALING, false)
}
}
}

View File

@ -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
}

View File

@ -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
)
}

View File

@ -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<ServerSetting>
/**
* Finds a Server Setting by its unique Id
*/
@Query("SELECT * FROM serverSetting WHERE [id] = :id")
suspend fun findById(id: Int): ServerSetting?
/**
* Finds a Server Setting by its Index in the Select List
*/
@Query("SELECT * FROM serverSetting WHERE [index] = :index")
suspend fun findByIndex(index: Int): ServerSetting?
/**
* Retrieves the greatest Index in stored in the table
*/
@Query("SELECT MAX([index]) FROM serverSetting")
suspend fun getMaxIndex(): Int?
}

View File

@ -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<AppDatabase>().serverSettingDao() }
viewModel { ServerSettingsModel(get(), get(), androidContext()) }
single { ActiveServerProvider(get(), androidContext()) }
}

View File

@ -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.

View File

@ -1,5 +0,0 @@
package org.moire.ultrasonic.di
object DiProperties {
const val APP_CONTEXT = "app_context"
}

View File

@ -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

View File

@ -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()) }
}

View File

@ -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

View File

@ -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<SharedPreferences>(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<Int>(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<Int>(named("ServerInstance"))
val serverUrl = get<ActiveServerProvider>().getActiveServer().url
return@single abs("$serverUrl$serverInstance".hashCode()).toString()
}
single {
val serverId = get<String>(name = "ServerID")
val serverId = get<String>(named("ServerID"))
return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG)
}
single {
val instance = get<Int>(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<ActiveServerProvider>().getActiveServer().url,
username = get<ActiveServerProvider>().getActiveServer().userName,
password = get<ActiveServerProvider>().getActiveServer().password,
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
Constants.REST_PROTOCOL_VERSION
),
clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = get<ActiveServerProvider>()
.getActiveServer().allowSelfSignedCertificate,
enableLdapUserSupport = get<ActiveServerProvider>().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<MusicService>(name = ONLINE_MUSIC_SERVICE) {
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
CachedMusicService(RESTMusicService(get(), get()))
}
single<MusicService>(name = OFFLINE_MUSIC_SERVICE) {
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
OfflineMusicService(get(), get())
}
single { SubsonicImageLoader(getProperty(DiProperties.APP_CONTEXT), get()) }
single { SubsonicImageLoader(androidContext(), get()) }
}

View File

@ -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<String>(name = "ServerID")
fun getServerId() = get<String>(named("ServerID"))
@JvmStatic
fun getDirectories() = get<Directories>()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="?android:colorControlHighlight"
tools:targetApi="lollipop">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="?android:colorAccent" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/transparent" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_window_focused="false" android:drawable="@color/selected_color_dark" />
<!-- Even though these two point to the same resource, have two states so the drawable will invalidate itself when coming out of pressed state. -->
<item android:state_focused="true" android:state_enabled="false" android:state_pressed="true" android:drawable="@drawable/list_selector_disabled_holo_dark" />
<item android:state_focused="true" android:state_enabled="false" android:drawable="@drawable/list_selector_disabled_holo_dark" />
<item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_dark" />
<item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_dark" />
<item android:state_focused="true" android:drawable="@drawable/list_focused_holo" />
</selector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_window_focused="false" android:drawable="@color/selected_color_light" />
<!-- Even though these two point to the same resource, have two states so the drawable will invalidate itself when coming out of pressed state. -->
<item android:state_focused="true" android:state_enabled="false" android:state_pressed="true" android:drawable="@drawable/list_selector_disabled_holo_light" />
<item android:state_focused="true" android:state_enabled="false" android:drawable="@drawable/list_selector_disabled_holo_light" />
<item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_light" />
<item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_light" />
<item android:state_focused="true" android:drawable="@drawable/list_focused_holo" />
</selector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="?android:colorControlHighlight"
tools:targetApi="lollipop">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="?android:colorAccent" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="?attr/color_selected" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="?android:colorControlHighlight"
tools:targetApi="lollipop">
<item android:id="@android:id/mask">
<shape android:shape="oval">
<solid android:color="?android:colorAccent" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@color/transparent" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:fillViewport="true" >
<androidx.constraintlayout.widget.ConstraintLayout
a:layout_width="match_parent"
a:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
a:id="@+id/edit_server_name"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:hint="@string/settings.server_name"
app:layout_constraintBottom_toTopOf="@+id/edit_server_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:inputType="text"
a:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
a:id="@+id/edit_server_address"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:layout_marginBottom="20dp"
a:hint="@string/settings.server_address"
app:layout_constraintBottom_toTopOf="@+id/edit_authentication_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_server_name">
<com.google.android.material.textfield.TextInputEditText
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:inputType="textWebEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
a:id="@+id/edit_authentication_header"
style="@style/MenuDrawer.Widget.Category"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:text="@string/server_editor.authentication"
app:layout_constraintBottom_toTopOf="@+id/edit_server_username"
app:layout_constraintStart_toStartOf="@+id/edit_server_username"
app:layout_constraintTop_toBottomOf="@+id/edit_server_address" />
<com.google.android.material.textfield.TextInputLayout
a:id="@+id/edit_server_username"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:hint="@string/settings.server_username"
app:layout_constraintBottom_toTopOf="@+id/edit_server_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_authentication_header">
<com.google.android.material.textfield.TextInputEditText
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:inputType="text"
a:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
a:id="@+id/edit_server_password"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:layout_marginBottom="21dp"
a:hint="@string/settings.server_password"
app:endIconMode="password_toggle"
app:layout_constraintBottom_toTopOf="@+id/edit_advanced_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_server_username">
<com.google.android.material.textfield.TextInputEditText
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
a:id="@+id/edit_advanced_header"
style="@style/MenuDrawer.Widget.Category"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginBottom="8dp"
a:text="@string/server_editor.advanced"
app:layout_constraintBottom_toTopOf="@+id/edit_self_signed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_server_password" />
<TextView
a:id="@+id/edit_self_signed_title"
style="@style/Widget.AppCompat.CompoundButton.Switch"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="5dp"
a:layout_marginLeft="5dp"
a:text="@string/settings.title.allow_self_signed_certificate"
app:layout_constraintBottom_toTopOf="@+id/edit_ldap_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_advanced_header" />
<com.google.android.material.switchmaterial.SwitchMaterial
a:id="@+id/edit_self_signed"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="8dp"
a:layout_marginLeft="8dp"
a:layout_marginEnd="5dp"
a:layout_marginRight="5dp"
a:layout_marginBottom="8dp"
a:checked="false"
app:layout_constraintBottom_toTopOf="@+id/edit_ldap_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_self_signed_title"
app:layout_constraintTop_toBottomOf="@+id/edit_advanced_header" />
<TextView
a:id="@+id/edit_ldap_title"
style="@style/Widget.AppCompat.CompoundButton.Switch"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="5dp"
a:layout_marginLeft="5dp"
a:text="@string/settings.title.enable_ldap_users_support"
app:layout_constraintBottom_toTopOf="@+id/edit_ldap_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_self_signed" />
<TextView
a:id="@+id/edit_ldap_description"
style="@style/TextAppearance.AppCompat.Small"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="5dp"
a:layout_marginLeft="5dp"
a:text="@string/settings.summary.enable_ldap_users_support"
app:layout_constraintBottom_toTopOf="@+id/edit_jukebox"
app:layout_constraintEnd_toStartOf="@+id/edit_ldap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_ldap_title" />
<com.google.android.material.switchmaterial.SwitchMaterial
a:id="@+id/edit_ldap"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginEnd="5dp"
a:layout_marginRight="5dp"
a:layout_marginLeft="8dp"
a:layout_marginStart="8dp"
a:checked="false"
app:layout_constraintBottom_toBottomOf="@+id/edit_ldap_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_ldap_description"
app:layout_constraintTop_toTopOf="@+id/edit_ldap_title" />
<TextView
a:id="@+id/edit_jukebox_title"
style="@style/Widget.AppCompat.CompoundButton.Switch"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="5dp"
a:layout_marginLeft="5dp"
a:layout_marginTop="8dp"
a:text="@string/jukebox.is_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_ldap_description" />
<com.google.android.material.switchmaterial.SwitchMaterial
a:id="@+id/edit_jukebox"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="8dp"
a:layout_marginLeft="8dp"
a:layout_marginTop="8dp"
a:layout_marginEnd="5dp"
a:layout_marginRight="5dp"
a:checked="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_jukebox_title"
app:layout_constraintTop_toBottomOf="@+id/edit_ldap_description" />
<Button
a:id="@+id/edit_test"
style="?attr/materialButtonOutlinedStyle"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginTop="8dp"
a:text="@string/settings.test_connection_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/edit_save"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_jukebox"
app:layout_constraintVertical_bias="1.0" />
<Button
a:id="@+id/edit_save"
style="?attr/materialButtonOutlinedStyle"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginTop="8dp"
a:text="@string/common.save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/edit_test"
app:layout_constraintTop_toBottomOf="@id/edit_jukebox"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/server_layout"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:descendantFocusability="blocksDescendants"
a:orientation="horizontal">
<ImageView
a:id="@+id/server_image"
a:layout_width="wrap_content"
a:layout_height="match_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:layout_centerVertical="true"
a:paddingHorizontal="15dp" />
<TextView
a:id="@+id/server_name"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceListItem"
a:paddingTop="10dp"
a:layout_toRightOf="@id/server_image"
a:layout_toEndOf="@id/server_image" />
<TextView
a:id="@+id/server_description"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:layout_below="@id/server_name"
a:paddingBottom="10dp"
a:layout_toRightOf="@id/server_image"
a:layout_toEndOf="@id/server_image" />
<ImageButton
a:id="@+id/server_menu"
a:layout_width="wrap_content"
a:layout_height="match_parent"
a:layout_alignParentEnd="true"
a:layout_alignParentRight="true"
a:layout_centerVertical="true"
a:layout_gravity="end"
a:layout_marginEnd="15dp"
a:layout_marginRight="15dp"
a:focusable="false"
a:src="?attr/more_vert"
a:padding="5dp" />
</RelativeLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="vertical"
a:layout_width="match_parent"
a:layout_height="match_parent">
<ListView
a:id="@+id/server_list"
a:layout_width="fill_parent"
a:layout_height="fill_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
a:id="@+id/server_add_fab"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_alignRight="@id/server_list"
a:layout_alignEnd="@id/server_list"
a:layout_alignBottom="@id/server_list"
a:layout_margin="16dp"
a:clickable="true"
a:focusable="true"
a:src="@drawable/ic_add_white" />
</RelativeLayout>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Mit Stern</string>
<string name="main.songs_title">Titel</string>
<string name="main.videos">Filme</string>
<string name="main.welcome_text">Willkommen bei UltraSonic! Die App ist zurzeit unkonfiguriert. Nachdem du deinen Server konfiguriert hast (siehe <b>subsonic.org</b>), bitte <i>Server hinzufügen</i> in den <b>Einstellungen</b> klicken um die Verbindun herzustellen.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Willkommen</string>
<string name="menu.about">Über</string>
<string name="menu.common">Allgemein</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Album Cover über Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Wiedergabe-Benachrichtigungen über Bluetooth senden</string>
<string name="settings.send_bluetooth_notification">Bluetooth-Benachrichtigung</string>
<string name="settings.server_add_server">Server hinzufügen</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Server Adresse</string>
<string name="settings.server_name">Name</string>
<string name="settings.server_password">Kennwort</string>
@ -419,6 +419,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 Titel</item>
<item quantity="other">%d Titel</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Me gusta</string>
<string name="main.songs_title">Canciones</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Te damos la bienvenida a UltraSonic! La aplicación no está configurada actualmente. Despues de que hayas configurado tu servidor personal (disponible desde <b>subsonic.org</b>), por favor haz click en <i>Añadir servidor</i> en <b>Configuración</b> para conectarte con él.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">¡Saludos!</string>
<string name="menu.about">Acerca de</string>
<string name="menu.common">Común</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Carátula del Álbum vía Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Enviar notificaciones de reproducción vía Bluetooth</string>
<string name="settings.send_bluetooth_notification">Enviar notificaciones Bluetooth</string>
<string name="settings.server_add_server">Añadir servidor</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Dirección del servidor</string>
<string name="settings.server_name">Nombre</string>
<string name="settings.server_password">Contraseña</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Usar predeterminado</string>
<string name="filepicker.available_drives">Unidades disponibles:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 canción</item>
<item quantity="other">%d canciones</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Favoris</string>
<string name="main.songs_title">Titres</string>
<string name="main.videos">Vidéos</string>
<string name="main.welcome_text">Bienvenue dans UltraSonic ! L\'application n\'est pas configurée. Après avoir configuré votre serveur personnel (disponible à partir de <b>subsonic.org</b>), veuillez accéder aux <b>Paramètres</b> et modifier la configuration pour vous y connecter.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Bienvenue!</string>
<string name="menu.about">À propos</string>
<string name="menu.common">Général</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Pochette de l\'album via Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Envoyer des notifications lecture via Bluetooth</string>
<string name="settings.send_bluetooth_notification">Envoyer une notification Bluetooth</string>
<string name="settings.server_add_server">Ajouter un serveur</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Adresse du serveur</string>
<string name="settings.server_name">Nom</string>
<string name="settings.server_password">Mot de passe</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">Un titre</item>
<item quantity="other">%d titres</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Csillaggal megjelölt</string>
<string name="main.songs_title">Dalok</string>
<string name="main.videos">Videók</string>
<string name="main.welcome_text">Üdvözli az UltraSonic! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját (elérhető: <b>subsonic.org</b>), húzza balról jobbra az oldalsávot, lépjen be a <b>Beállítások</b> menüpontba, és adja meg csatlakozási adatokat!</string>
<string name="main.welcome_text_new">Üdvözli az UltraSonic! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját (elérhető: <b>subsonic.org</b>), húzza balról jobbra az oldalsávot, lépjen be a <b>Beállítások</b> menüpontba, és adja meg csatlakozási adatokat!</string>
<string name="main.welcome_title">Üdvözlet!</string>
<string name="menu.about">Névjegy</string>
<string name="menu.common">Általános</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Albumborító Bluetooth-on</string>
<string name="settings.send_bluetooth_notification_summary">Lejátszási értesítések küldése Bluetooth-on.</string>
<string name="settings.send_bluetooth_notification">Bluetooth értesítések küldése</string>
<string name="settings.server_add_server">Kiszolgáló hozzáadása</string>
<string name="settings.server_manage_servers">Kiszolgálók kezelése</string>
<string name="settings.server_address">Kiszolgáló címe</string>
<string name="settings.server_name">Név</string>
<string name="settings.server_password">Jelszó</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Alapért.</string>
<string name="filepicker.available_drives">Elérhető tárolók:</string>
<string name="server_selector.label">Beállított szerverek</string>
<string name="server_selector.delete_confirmation">Biztosan törölni szeretnéd a szervert?</string>
<string name="server_editor.label">Szerver szerkesztése</string>
<string name="server_editor.new_label">Szerver létrehozása</string>
<string name="server_editor.leave_confirmation">Biztosan visszalépsz és elveszíted a változtatásokat?</string>
<string name="server_editor.required">Kötelező mező</string>
<string name="server_menu.edit">Szerkeszt</string>
<string name="server_menu.delete">Töröl</string>
<string name="server_menu.move_up">Feljebb mozgat</string>
<string name="server_menu.move_down">Lejjebb mozgat</string>
<string name="server_editor.authentication">Bejelentkezés</string>
<string name="server_editor.advanced">Haladó beállítások</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 dal</item>
<item quantity="other">%d dal</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Favorieten</string>
<string name="main.songs_title">Nummers</string>
<string name="main.videos">Video\'s</string>
<string name="main.welcome_text">Welkom bij UltraSonic! De app is nog niet ingesteld. Nadat je je persoonlijke server hebt opgezet (beschikbaar op <b>subsonic.org</b>), kun je naar de <b>Instellingen</b> gaan en drukken op <i>Server toevoegen</i>.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Welkom!</string>
<string name="menu.about">Over</string>
<string name="menu.common">Algemeen</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Albumhoezen versturen via Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Afspeelmeldingen versturen via Bluetooth</string>
<string name="settings.send_bluetooth_notification">Bluetooth-melding versturen</string>
<string name="settings.server_add_server">Server toevoegen</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Serveradres</string>
<string name="settings.server_name">Naam</string>
<string name="settings.server_password">Wachtwoord</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 nummer</item>
<item quantity="other">%d nummers</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Ulubione</string>
<string name="main.songs_title">Utwory</string>
<string name="main.videos">Klipy wideo</string>
<string name="main.welcome_text">Witaj w UltraSonic! Obecnie aplikacja nie jest skonfigurowana. Jeśli masz uruchomiony własny serwer (dostępny na <b>subsonic.org</b>), proszę wybrać <i>Dodaj serwer</i> w <b>Ustawieniach</b> aby się z nim połączyć.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Witaj!</string>
<string name="menu.about">O aplikacji</string>
<string name="menu.common">Wspólne</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Okładki przez Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Wysyła powiadomienia o odtwarzaniu przez Bluetooth</string>
<string name="settings.send_bluetooth_notification">Wysyłaj powiadomienia Bluetooth</string>
<string name="settings.server_add_server">Dodaj serwer</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Adres serwera</string>
<string name="settings.server_name">Nazwa</string>
<string name="settings.server_password">Hasło</string>
@ -420,6 +420,19 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 utwór</item>
<item quantity="few">%d utwory</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Favoritas</string>
<string name="main.songs_title">Músicas</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Bem-vindo ao UltraSonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em <b>subsonic.org</b>), clique em <i>Adicionar Servidor</i> em <b>Configurações</b> para a conexão.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Bem-vindo!</string>
<string name="menu.about">Sobre</string>
<string name="menu.common">Comum</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Arte do Álbum via Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Envia notificações de reprodução via Bluetooth</string>
<string name="settings.send_bluetooth_notification">Notificações via Bluetooth</string>
<string name="settings.server_add_server">Adicionar Servidor</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Endereço do Servidor</string>
<string name="settings.server_name">Nome</string>
<string name="settings.server_password">Senha</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 música</item>
<item quantity="other">%d músicas</item>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Favoritas</string>
<string name="main.songs_title">Músicas</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Bem-vindo ao UltraSonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em <b>subsonic.org</b>), clique em <i>Adicionar Servidor</i> em <b>Configurações</b> para a conexão.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Bem-vindo!</string>
<string name="menu.about">Sobre</string>
<string name="menu.common">Comum</string>
@ -277,7 +277,7 @@
<string name="settings.send_bluetooth_album_art">Arte do Álbum via Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Envia notificações de reprodução via Bluetooth</string>
<string name="settings.send_bluetooth_notification">Notificações via Bluetooth</string>
<string name="settings.server_add_server">Adicionar Servidor</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Endereço do Servidor</string>
<string name="settings.server_name">Nome</string>
<string name="settings.server_password">Senha</string>
@ -420,6 +420,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">%d música</item>
<item quantity="other">%d músicas</item>

View File

@ -9,4 +9,6 @@
<color name="translucent">#80000000</color>
<color name="background_color_dark">#ff000000</color>
<color name="background_color_light">#fff3f3f3</color>
<color name="selected_color_dark">#424242</color>
<color name="selected_color_light">#B1B1B1</color>
</resources>

View File

@ -101,7 +101,7 @@
<string name="main.songs_starred">Starred</string>
<string name="main.songs_title">Songs</string>
<string name="main.videos">Videos</string>
<string name="main.welcome_text">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Add Server</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_text_new">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Welcome!</string>
<string name="menu.about">About</string>
<string name="menu.common">Common</string>
@ -279,7 +279,7 @@
<string name="settings.send_bluetooth_album_art">Album Art Over Bluetooth</string>
<string name="settings.send_bluetooth_notification_summary">Send playback notifications via Bluetooth</string>
<string name="settings.send_bluetooth_notification">Send Bluetooth Notification</string>
<string name="settings.server_add_server">Add Server</string>
<string name="settings.server_manage_servers">Manage Servers</string>
<string name="settings.server_address">Server Address</string>
<string name="settings.server_name">Name</string>
<string name="settings.server_password">Password</string>
@ -424,6 +424,19 @@
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>
<string name="server_editor.new_label">Create server</string>
<string name="server_editor.leave_confirmation">Are you sure you want to leave and lose your changes?</string>
<string name="server_editor.required">This field is required</string>
<string name="server_menu.edit">Edit</string>
<string name="server_menu.delete">Delete</string>
<string name="server_menu.move_up">Move up</string>
<string name="server_menu.move_down">Move down</string>
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 song</item>
<item quantity="other">%d songs</item>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="listselect" parent="Widget.AppCompat.ListView">
<item name="android:listSelector">@drawable/list_selector_holo_dark</item>
</style>
@ -92,11 +91,14 @@
<attr name="check_mark_on" format="reference"/>
<attr name="button_check_custom" format="reference"/>
<attr name="color_background" format="reference"/>
<attr name="color_selected" format="reference"/>
<attr name="filepicker_create_new_folder" format="reference"/>
<attr name="filepicker_folder" format="reference"/>
<attr name="filepicker_subdirectory_left" format="reference"/>
<attr name="filepicker_subdirectory_up" format="reference"/>
<attr name="filepicker_sd_card" format="reference"/>
<attr name="more_vert" format="reference"/>
<attr name="list_selector_holo" format="reference"/>
<attr name="list_selector_holo_selected" format="reference"/>
</resources>

View File

@ -2,6 +2,7 @@
<resources>
<style name="UltraSonicTheme" parent="Theme.AppCompat">
<item name="color_background">@color/background_color_dark</item>
<item name="color_selected">@color/selected_color_dark</item>
<item name="star_hollow">@drawable/ic_star_hollow_dark</item>
<item name="star_full">@drawable/ic_star_full_dark</item>
<item name="about">@drawable/ic_menu_about_dark</item>
@ -47,10 +48,14 @@
<item name="filepicker_folder">@drawable/ic_folder_dark</item>
<item name="filepicker_subdirectory_up">@drawable/ic_subdirectory_up_dark</item>
<item name="filepicker_sd_card">@drawable/ic_sd_storage_dark</item>
<item name="more_vert">@drawable/ic_more_vert_dark</item>
<item name="list_selector_holo">@drawable/list_selector_holo_dark</item>
<item name="list_selector_holo_selected">@drawable/list_selector_holo_dark_selected</item>
</style>
<style name="UltraSonicTheme.Light" parent="Theme.AppCompat.Light">
<item name="color_background">@color/background_color_light</item>
<item name="color_selected">@color/selected_color_light</item>
<item name="star_hollow">@drawable/ic_star_hollow_light</item>
<item name="star_full">@drawable/ic_star_full_light</item>
<item name="about">@drawable/ic_menu_about_light</item>
@ -96,5 +101,8 @@
<item name="filepicker_folder">@drawable/ic_folder_light</item>
<item name="filepicker_subdirectory_up">@drawable/ic_subdirectory_up_light</item>
<item name="filepicker_sd_card">@drawable/ic_sd_storage_light</item>
<item name="more_vert">@drawable/ic_more_vert_light</item>
<item name="list_selector_holo">@drawable/list_selector_holo_light</item>
<item name="list_selector_holo_selected">@drawable/list_selector_holo_light_selected</item>
</style>
</resources>

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/common.appname"
>
<EditTextPreference
android:key="@string/settings.server_name"
android:persistent="false"
android:title="@string/settings.server_name"
android:defaultValue="@string/settings.server_unused"
android:text="@string/settings.server_unused"
android:summary="@string/settings.server_unused"
/>
<EditTextPreference
android:key="@string/settings.server_address"
android:persistent="false"
android:inputType="textUri"
android:defaultValue="@string/settings.server_address_unset"
android:title="@string/settings.server_address"
android:text="@string/settings.server_address_unset"
android:summary="@string/settings.server_address_unset"
/>
<EditTextPreference
android:key="@string/settings.server_username"
android:persistent="false"
android:title="@string/settings.server_username"
/>
<EditTextPreference
android:key="@string/settings.server_password"
android:persistent="false"
android:inputType="textPassword"
android:title="@string/settings.server_password"
/>
<CheckBoxPreference
android:key="@string/equalizer.enabled"
android:persistent="false"
android:defaultValue="true"
android:title="@string/equalizer.enabled"
/>
<CheckBoxPreference
android:key="@string/settings.allow_self_signed_certificate"
android:persistent="false"
android:defaultValue="false"
android:title="@string/settings.title.allow_self_signed_certificate"
/>
<CheckBoxPreference
android:key="@string/settings.enable_ldap_user_support"
android:persistent="false"
android:defaultValue="false"
android:title="@string/settings.title.enable_ldap_users_support"
android:summary="@string/settings.summary.enable_ldap_users_support"
/>
<CheckBoxPreference
android:key="@string/jukebox.is_default"
android:persistent="false"
android:defaultValue="false"
android:title="@string/jukebox.is_default"
/>
<Preference
android:key="@string/settings.server_remove_server"
android:persistent="false"
android:title="@string/settings.server_remove_server"
/>
<Preference
android:key="@string/settings.test_connection_title"
android:persistent="false"
android:title="@string/settings.test_connection_title"
/>
</PreferenceScreen>