Started refactoring to NavigationUI

Main menu items are refactored, except Now Playing
This commit is contained in:
Nite 2021-02-04 20:15:58 +01:00
parent 4fdab06271
commit 95773c7994
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
62 changed files with 5679 additions and 4226 deletions

View File

@ -18,6 +18,7 @@ buildscript {
classpath gradlePlugins.ktlintGradle
classpath gradlePlugins.detekt
classpath gradlePlugins.jacoco
classpath gradlePlugins.navigationSafeArgs
}
}

View File

@ -4,11 +4,14 @@ ext.versions = [
compileSdk : 29,
gradle : '6.5',
navigation : "2.3.2",
androidTools : "4.0.0",
ktlint : "0.37.1",
ktlintGradle : "9.2.1",
detekt : "1.0.0.RC6-4",
jacoco : "0.8.5",
navigationSafeArgs : "2.3.2",
preferences : "1.1.1",
androidSupport : "28.0.0",
androidLegacySupport : "1.0.0",
@ -42,23 +45,30 @@ ext.versions = [
]
ext.gradlePlugins = [
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco"
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco",
navigationSafeArgs: "androidx.navigation:navigation-safe-args-gradle-plugin:$versions.navigationSafeArgs"
]
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",
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"
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",
navigationFragment : "androidx.navigation:navigation-fragment:$versions.navigation",
navigationUi : "androidx.navigation:navigation-ui:$versions.navigation",
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation",
preferences : "androidx.preference:preference:$versions.preferences",
]
ext.other = [

View File

@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply plugin: 'androidx.navigation.safeargs'
apply from: "../gradle_scripts/code_quality.gradle"
android {
@ -50,6 +51,10 @@ android {
warning 'MissingTranslation'
abortOnError true
}
kotlinOptions {
jvmTarget = "1.8"
}
}
tasks.withType(Test) {
@ -71,6 +76,13 @@ dependencies {
implementation androidSupport.roomKtx
implementation androidSupport.viewModelKtx
implementation androidSupport.constraintLayout
implementation androidSupport.preferences
implementation androidSupport.navigationFragment
implementation androidSupport.navigationUi
implementation androidSupport.navigationFragmentKtx
implementation androidSupport.navigationUiKtx
implementation androidSupport.navigationFeature
implementation other.kotlinStdlib
implementation other.kotlinxCoroutines

View File

@ -23,24 +23,36 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.AppCompat"
android:theme="@style/NoActionBar"
android:name=".app.UApp"
android:label="@string/common.appname"
android:usesCleartextTraffic="true">
<activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="standard">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<nav-graph android:value="@navigation/navigation_graph" />
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<activity
android:name=".activity.MainActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="standard">
<intent-filter>
<!--<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</intent-filter>-->
</activity>
<activity
android:name=".activity.SelectArtistActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="standard"/>
<activity
android:name=".activity.SelectAlbumActivity"
android:configChanges="orientation|keyboardHidden"/>
@ -49,38 +61,11 @@
android:configChanges="orientation|keyboardHidden"
android:label="@string/search.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.SelectPlaylistActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/playlist.label"
android:launchMode="standard"/>
<activity
android:name=".activity.PodcastsActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/podcasts.label"
android:launchMode="standard"/>
<activity
android:name=".activity.BookmarkActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.ShareActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.ChatActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.DownloadActivity"
android:configChanges="keyboardHidden"
android:launchMode="singleTask"
android:exported="true" />
<activity
android:name=".activity.SettingsActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.HelpActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.LyricsActivity"
android:configChanges="orientation|keyboardHidden"
@ -90,10 +75,6 @@
android:configChanges="orientation|keyboardHidden"
android:label="@string/equalizer.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.SelectGenreActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="standard"/>
<activity
android:name=".activity.VoiceQueryReceiverActivity"
android:launchMode="singleTask">
@ -102,7 +83,7 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
<!-- <activity
android:name=".activity.QueryReceiverActivity"
android:launchMode="singleTask">
<intent-filter>
@ -114,7 +95,7 @@
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
-->
<activity
android:name=".activity.ServerSelectorActivity"
android:label="@string/server_selector.label" />

View File

@ -1,485 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.List;
public class BookmarkActivity extends SubsonicTabActivity
{
private SwipeRefreshLayout refreshAlbumListView;
private ListView albumListView;
private View albumButtons;
private View emptyView;
private ImageView playNowButton;
private ImageView pinButton;
private ImageView unpinButton;
private ImageView downloadButton;
private ImageView deleteButton;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_album);
albumButtons = findViewById(R.id.menu_album);
refreshAlbumListView = findViewById(R.id.select_album_entries_refresh);
albumListView = findViewById(R.id.select_album_entries_list);
refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (position >= 0)
{
Entry entry = (Entry) parent.getItemAtPosition(position);
if (entry != null)
{
if (entry.isVideo())
{
playVideo(entry);
}
else
{
enableButtons();
}
}
}
}
});
ImageView selectButton = (ImageView) findViewById(R.id.select_album_select);
playNowButton = (ImageView) findViewById(R.id.select_album_play_now);
ImageView playNextButton = (ImageView) findViewById(R.id.select_album_play_next);
ImageView playLastButton = (ImageView) findViewById(R.id.select_album_play_last);
pinButton = (ImageView) findViewById(R.id.select_album_pin);
unpinButton = (ImageView) findViewById(R.id.select_album_unpin);
downloadButton = (ImageView) findViewById(R.id.select_album_download);
deleteButton = (ImageView) findViewById(R.id.select_album_delete);
ImageView oreButton = (ImageView) findViewById(R.id.select_album_more);
emptyView = findViewById(R.id.select_album_empty);
selectButton.setVisibility(View.GONE);
playNextButton.setVisibility(View.GONE);
playLastButton.setVisibility(View.GONE);
oreButton.setVisibility(View.GONE);
playNowButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
playNow(getSelectedSongs(albumListView));
}
});
selectButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
selectAllOrNone();
}
});
pinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(true);
selectAll(false, false);
}
});
unpinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
unpin();
selectAll(false, false);
}
});
downloadButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(false);
selectAll(false, false);
}
});
deleteButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
delete();
selectAll(false, false);
}
});
registerForContextMenu(albumListView);
enableButtons();
View browseMenuItem = findViewById(R.id.menu_bookmarks);
menuDrawer.setActiveView(browseMenuItem);
getBookmarks();
}
private void getBookmarks()
{
setActionBarSubtitle(R.string.button_bar_bookmarks);
new LoadTask()
{
@Override
protected MusicDirectory load(MusicService service) throws Exception
{
return Util.getSongsFromBookmarks(service.getBookmarks(BookmarkActivity.this, this));
}
}.execute();
}
private void playNow(List<Entry> songs)
{
if (!getSelectedSongs(albumListView).isEmpty())
{
int position = songs.get(0).getBookmarkPosition();
if (getMediaPlayerController() == null) return;
getMediaPlayerController().restore(songs, 0, position, true, true);
selectAll(false, false);
}
}
private static List<MusicDirectory.Entry> getSelectedSongs(ListView albumListView)
{
List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
if (albumListView != null)
{
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (albumListView.isItemChecked(i))
{
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i));
}
}
}
return songs;
}
private void refresh()
{
finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
private void selectAllOrNone()
{
boolean someUnselected = false;
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry)
{
someUnselected = true;
break;
}
}
selectAll(someUnselected, true);
}
private void selectAll(boolean selected, boolean toast)
{
int count = albumListView.getCount();
int selectedCount = 0;
for (int i = 0; i < count; i++)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
if (entry != null && !entry.isDirectory() && !entry.isVideo())
{
albumListView.setItemChecked(i, selected);
selectedCount++;
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast)
{
int toastResId = selected ? R.string.select_album_n_selected : R.string.select_album_n_unselected;
Util.toast(this, getString(toastResId, selectedCount));
}
enableButtons();
}
private void enableButtons()
{
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController == null)
{
return;
}
List<MusicDirectory.Entry> selection = getSelectedSongs(albumListView);
boolean enabled = !selection.isEmpty();
boolean unpinEnabled = false;
boolean deleteEnabled = false;
int pinnedCount = 0;
for (MusicDirectory.Entry song : selection)
{
DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song);
if (downloadFile.isWorkDone())
{
deleteEnabled = true;
}
if (downloadFile.isSaved())
{
pinnedCount++;
unpinEnabled = true;
}
}
playNowButton.setVisibility(enabled && deleteEnabled ? 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 && !ActiveServerProvider.Companion.isOffline(this) ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
}
private void downloadBackground(final boolean save)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
downloadBackground(save, songs);
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
if (getMediaPlayerController() == null)
{
return;
}
Runnable onValid = new Runnable()
{
@Override
public void run()
{
warnIfNetworkOrStorageUnavailable();
getMediaPlayerController().downloadBackground(songs, save);
if (save)
{
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
}
else
{
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
}
}
};
checkLicenseAndTrialPeriod(onValid);
}
private void delete()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
if (getMediaPlayerController() != null)
{
getMediaPlayerController().delete(songs);
}
}
private void unpin()
{
if (getMediaPlayerController() != null)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getMediaPlayerController().unpin(songs);
}
}
private abstract class LoadTask extends TabActivityBackgroundTask<Pair<MusicDirectory, Boolean>>
{
public LoadTask()
{
super(BookmarkActivity.this, true);
}
protected abstract MusicDirectory load(MusicService service) throws Exception;
@Override
protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(BookmarkActivity.this);
MusicDirectory dir = load(musicService);
boolean valid = musicService.isLicenseValid(BookmarkActivity.this, this);
return new Pair<MusicDirectory, Boolean>(dir, valid);
}
@Override
protected void done(Pair<MusicDirectory, Boolean> result)
{
MusicDirectory musicDirectory = result.getFirst();
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
int songCount = 0;
for (MusicDirectory.Entry entry : entries)
{
if (!entry.isDirectory())
{
songCount++;
}
}
final int listSize = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
if (songCount > 0)
{
pinButton.setVisibility(View.VISIBLE);
unpinButton.setVisibility(View.VISIBLE);
downloadButton.setVisibility(View.VISIBLE);
deleteButton.setVisibility(View.VISIBLE);
playNowButton.setVisibility(View.VISIBLE);
}
else
{
pinButton.setVisibility(View.GONE);
unpinButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
deleteButton.setVisibility(View.GONE);
playNowButton.setVisibility(View.GONE);
if (listSize == 0 || result.getFirst().getChildren().size() < listSize)
{
albumButtons.setVisibility(View.GONE);
}
}
enableButtons();
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
albumListView.setAdapter(new EntryAdapter(BookmarkActivity.this, getImageLoader(), entries, true));
licenseValid = result.getSecond();
}
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,310 +0,0 @@
package org.moire.ultrasonic.activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.service.JukeboxMediaPlayer;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ChatAdapter;
import java.util.ArrayList;
import java.util.Collections;
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
*/
public final class ChatActivity extends SubsonicTabActivity
{
private ListView chatListView;
private EditText messageEditText;
private ImageButton sendButton;
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)
{
super.onCreate(bundle);
setContentView(R.layout.chat);
messageEditText = (EditText) findViewById(R.id.chat_edittext);
sendButton = (ImageButton) findViewById(R.id.chat_send);
sendButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
sendMessage();
}
});
chatListView = findViewById(R.id.chat_entries_list);
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
chatListView.setStackFromBottom(true);
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);
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
messageEditText.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void afterTextChanged(Editable editable)
{
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
}
});
messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener()
{
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
{
sendMessage();
return true;
}
return false;
}
});
View chatMenuItem = findViewById(R.id.menu_chat);
menuDrawer.setActiveView(chatMenuItem);
load();
}
@Override
protected void onPostCreate(Bundle bundle)
{
super.onPostCreate(bundle);
timerMethod();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.chat, menu);
super.onCreateOptionsMenu(menu);
return true;
}
/*
* Listen for option item selections so that we receive a notification
* when the user requests a refresh by selecting the refresh action bar item.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Check if user triggered a refresh:
case R.id.menu_refresh:
// Start the refresh background task.
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return true;
}
// User didn't trigger a refresh, let the superclass handle this action
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume()
{
super.onResume();
if (!messageList.isEmpty())
{
ListAdapter chatAdapter = new ChatAdapter(ChatActivity.this, messageList);
chatListView.setAdapter(chatAdapter);
}
if (timer == null)
{
timerMethod();
}
}
@Override
protected void onPause()
{
super.onPause();
if (timer != null)
{
timer.cancel();
timer = null;
}
}
private void timerMethod()
{
int refreshInterval = Util.getChatRefreshInterval(this);
if (refreshInterval > 0)
{
timer = new Timer();
timer.schedule(new TimerTask()
{
@Override
public void run()
{
ChatActivity.this.runOnUiThread(new Runnable()
{
@Override
public void run()
{
load();
}
});
}
}, refreshInterval, refreshInterval);
}
}
private void sendMessage()
{
if (messageEditText != null)
{
final String message;
Editable text = messageEditText.getText();
if (text == null)
{
return;
}
message = text.toString();
if (!Util.isNullOrWhiteSpace(message))
{
messageEditText.setText("");
BackgroundTask<Void> task = new TabActivityBackgroundTask<Void>(ChatActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ChatActivity.this);
musicService.addChatMessage(message, ChatActivity.this, this);
return null;
}
@Override
protected void done(Void result)
{
load();
}
};
task.execute();
}
}
}
private synchronized void load()
{
BackgroundTask<List<ChatMessage>> task = new TabActivityBackgroundTask<List<ChatMessage>>(this, false)
{
@Override
protected List<ChatMessage> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ChatActivity.this);
return musicService.getChatMessages(lastChatMessageTime, ChatActivity.this, this);
}
@Override
protected void done(List<ChatMessage> result)
{
if (result != null && !result.isEmpty())
{
// Reset lastChatMessageTime if we have a newer message
for (ChatMessage message : result)
{
if (message.getTime() > lastChatMessageTime)
{
lastChatMessageTime = message.getTime();
}
}
// Reverse results to show them on the bottom
Collections.reverse(result);
messageList.addAll(result);
ListAdapter chatAdapter = new ChatAdapter(ChatActivity.this, messageList);
chatListView.setAdapter(chatAdapter);
}
}
};
task.execute();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
load();
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
return null;
}
}
}

View File

@ -1404,7 +1404,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
artistTextView.setText(currentSong.getArtist());
downloadTrackTextView.setText(trackFormat);
downloadTotalDurationTextView.setText(duration);
getImageLoader().loadImage(albumArtImageView, currentSong, true, 0, false, true);
imageLoader.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0, false, true);
displaySongRating();
}
@ -1416,7 +1416,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
artistTextView.setText(null);
downloadTrackTextView.setText(null);
downloadTotalDurationTextView.setText(null);
getImageLoader().loadImage(albumArtImageView, null, true, 0, false, true);
imageLoader.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0, false, true);
}
}

View File

@ -1,325 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
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;
/**
* An HTML-based help screen with Back and Done buttons at the bottom.
*
* @author Sindre Mehus
*/
public final class HelpActivity extends ResultActivity implements OnClickListener
{
private WebView webView;
private ImageView backButton;
private ImageView forwardButton;
private static final String STATE_MENUDRAWER = "org.moire.ultrasonic.menuDrawer";
private static final String STATE_ACTIVE_VIEW_ID = "org.moire.ultrasonic.activeViewId";
private static final String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition";
public MenuDrawer menuDrawer;
private int activePosition = 1;
private int menuActiveViewId;
View chatMenuItem;
View bookmarksMenuItem;
View sharesMenuItem;
@Override
protected void onCreate(Bundle bundle)
{
Util.applyTheme(this);
getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
super.onCreate(bundle);
setContentView(R.layout.help);
if (bundle != null)
{
activePosition = bundle.getInt(STATE_ACTIVE_POSITION);
menuActiveViewId = bundle.getInt(STATE_ACTIVE_VIEW_ID);
}
menuDrawer = MenuDrawer.attach(this, MenuDrawer.Type.BEHIND, Position.LEFT, MenuDrawer.MENU_DRAG_WINDOW);
menuDrawer.setMenuView(R.layout.menu_main);
chatMenuItem = findViewById(R.id.menu_chat);
bookmarksMenuItem = findViewById(R.id.menu_bookmarks);
sharesMenuItem = findViewById(R.id.menu_shares);
View aboutMenuItem = findViewById(R.id.menu_about);
findViewById(R.id.menu_home).setOnClickListener(this);
findViewById(R.id.menu_browse).setOnClickListener(this);
findViewById(R.id.menu_search).setOnClickListener(this);
findViewById(R.id.menu_playlists).setOnClickListener(this);
sharesMenuItem.setOnClickListener(this);
chatMenuItem.setOnClickListener(this);
bookmarksMenuItem.setOnClickListener(this);
findViewById(R.id.menu_now_playing).setOnClickListener(this);
findViewById(R.id.menu_settings).setOnClickListener(this);
aboutMenuItem.setOnClickListener(this);
findViewById(R.id.menu_exit).setOnClickListener(this);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setDisplayHomeAsUpEnabled(true);
}
menuDrawer.setActiveView(aboutMenuItem);
webView = (WebView) findViewById(R.id.help_contents);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new HelpClient());
if (bundle != null)
{
webView.restoreState(bundle);
}
else
{
webView.loadUrl(getResources().getString(R.string.help_url));
}
backButton = (ImageView) findViewById(R.id.help_back);
backButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goBack();
}
});
ImageView stopButton = (ImageView) findViewById(R.id.help_stop);
stopButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.stopLoading();
setProgressBarIndeterminateVisibility(false);
}
});
forwardButton = (ImageView) findViewById(R.id.help_forward);
forwardButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goForward();
}
});
}
@Override
protected void onPostCreate(Bundle bundle)
{
super.onPostCreate(bundle);
int visibility = ActiveServerProvider.Companion.isOffline(this) ? View.GONE : View.VISIBLE;
chatMenuItem.setVisibility(visibility);
bookmarksMenuItem.setVisibility(visibility);
sharesMenuItem.setVisibility(visibility);
}
@Override
public void onResume()
{
super.onResume();
}
@Override
protected void onSaveInstanceState(Bundle state)
{
webView.saveState(state);
super.onSaveInstanceState(state);
state.putParcelable(STATE_MENUDRAWER, menuDrawer.saveState());
state.putInt(STATE_ACTIVE_VIEW_ID, menuActiveViewId);
state.putInt(STATE_ACTIVE_POSITION, activePosition);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
if (webView.canGoBack())
{
webView.goBack();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onRestoreInstanceState(Bundle state)
{
super.onRestoreInstanceState(state);
menuDrawer.restoreState(state.getParcelable(STATE_MENUDRAWER));
}
@Override
public void onBackPressed()
{
final int drawerState = menuDrawer.getDrawerState();
if (drawerState == MenuDrawer.STATE_OPEN || drawerState == MenuDrawer.STATE_OPENING)
{
menuDrawer.closeMenu(true);
return;
}
finish();
super.onBackPressed();
}
@Override
public void onClick(View v)
{
menuActiveViewId = v.getId();
menuDrawer.setActiveView(v);
Intent intent;
switch (menuActiveViewId)
{
case R.id.menu_home:
intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_browse:
intent = new Intent(this, SelectArtistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_search:
intent = new Intent(this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_playlists:
intent = new Intent(this, SelectPlaylistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_shares:
intent = new Intent(this, ShareActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_chat:
startActivityForResultWithoutTransition(this, ChatActivity.class);
break;
case R.id.menu_bookmarks:
startActivityForResultWithoutTransition(this, BookmarkActivity.class);
break;
case R.id.menu_now_playing:
startActivityForResultWithoutTransition(this, DownloadActivity.class);
break;
case R.id.menu_settings:
startActivityForResultWithoutTransition(this, SettingsActivity.class);
break;
case R.id.menu_about:
startActivityForResultWithoutTransition(this, HelpActivity.class);
break;
case R.id.menu_exit:
intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
startActivityForResultWithoutTransition(this, intent);
break;
}
menuDrawer.closeMenu(true);
}
private final class HelpClient extends WebViewClient
{
@Override
public void onLoadResource(WebView webView, String url)
{
setProgressBarIndeterminateVisibility(true);
super.onLoadResource(webView, url);
}
@Override
public void onPageFinished(WebView view, String url)
{
setProgressBarIndeterminateVisibility(false);
String versionName = Util.getVersionName(HelpActivity.this);
String title = String.format("%s (%s)", view.getTitle(), versionName);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setSubtitle(title);
}
backButton.setEnabled(view.canGoBack());
forwardButton.setEnabled(view.canGoForward());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
{
Util.toast(HelpActivity.this, description);
}
}
}

View File

@ -67,161 +67,8 @@ public class MainActivity extends SubsonicTabActivity
{
super.onCreate(savedInstanceState);
// Determine first run and migrate server settings to DB as early as possible
boolean showWelcomeScreen = Util.isFirstRun(this);
boolean areServersMigrated = serverSettingsModel.getValue().migrateFromPreferences();
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen &= !areServersMigrated;
if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT))
{
setResult(Constants.RESULT_CLOSE_ALL);
if (getMediaPlayerController() != null)
{
getMediaPlayerController().stopJukeboxService();
}
if (getImageLoader() != null)
{
getImageLoader().stopImageLoader();
}
finish();
exit();
return;
}
setContentView(R.layout.main);
loadSettings();
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 = 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);
final View genresButton = buttons.findViewById(R.id.main_genres_button);
final View videosTitle = buttons.findViewById(R.id.main_videos_title);
final View songsTitle = buttons.findViewById(R.id.main_songs);
final View randomSongsButton = buttons.findViewById(R.id.main_songs_button);
final View songsStarredButton = buttons.findViewById(R.id.main_songs_starred);
final View albumsTitle = buttons.findViewById(R.id.main_albums);
final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
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);
lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
final ListView list = findViewById(R.id.main_list);
final MergeAdapter adapter = new MergeAdapter();
adapter.addViews(Collections.singletonList(serverButton), true);
if (!ActiveServerProvider.Companion.isOffline(this))
{
adapter.addView(musicTitle, false);
adapter.addViews(asList(artistsButton, albumsButton, genresButton), true);
adapter.addView(songsTitle, false);
adapter.addViews(asList(randomSongsButton, songsStarredButton), true);
adapter.addView(albumsTitle, false);
if (Util.getShouldUseId3Tags(MainActivity.this))
{
shouldUseId3 = true;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
else
{
shouldUseId3 = false;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsHighestButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
adapter.addView(videosTitle, false);
adapter.addViews(Collections.singletonList(videosButton), true);
}
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == serverButton)
{
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
}
});
final View homeMenuItem = findViewById(R.id.menu_home);
menuDrawer.setActiveView(homeMenuItem);
@ -230,46 +77,8 @@ public class MainActivity extends SubsonicTabActivity
// Remember the current theme.
theme = Util.getTheme(this);
showInfoDialog(showWelcomeScreen);
}
private void loadSettings()
{
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
final SharedPreferences preferences = Util.getPreferences(this);
if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION))
{
final SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(this).getPath());
editor.apply();
}
}
@Override
protected void onResume()
{
super.onResume();
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainActivity.this);
String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3)
{
shouldUseId3 = id3;
shouldRestart = true;
}
if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{
lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true;
}
if (shouldRestart) restart();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
@ -298,85 +107,4 @@ public class MainActivity extends SubsonicTabActivity
return false;
}
private void exit()
{
lifecycleSupport.getValue().onDestroy();
Util.unregisterMediaButtonEventReceiver(this, false);
finish();
}
private void showInfoDialog(final boolean show)
{
if (!infoDialogDisplayed)
{
infoDialogDisplayed = true;
if (show)
{
Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text);
}
}
}
private void showAlbumList(final String type, final int title)
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums(this));
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
startActivityForResultWithoutTransition(this, intent);
}
private void showStarredSongs()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_STARRED, 1);
startActivityForResultWithoutTransition(this, intent);
}
private void showRandomSongs()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(this));
startActivityForResultWithoutTransition(this, intent);
}
private void showArtists()
{
final Intent intent = new Intent(this, SelectArtistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getResources().getString(R.string.main_artists_title));
startActivityForResultWithoutTransition(this, intent);
}
private void showGenres()
{
final Intent intent = new Intent(this, SelectGenreActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
}
private void showVideos()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
startActivityForResultWithoutTransition(this, intent);
}
private void showServers()
{
final Intent intent = new Intent(this, ServerSelectorActivity.class);
startActivityForResult(intent, 0);
}
private String getActiveServerProperties()
{
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(),
currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(),
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
}

View File

@ -1,106 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.PodcastsChannel;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
import java.util.List;
public class PodcastsActivity extends SubsonicTabActivity {
private View emptyTextView;
SubsonicTabActivity currentActivity = null;
ListView channelItemsListView = null;
Context currentContext = (Context)this;
@Override
public void onCreate(Bundle savedInstanceState)
{
this.currentActivity = this;
super.onCreate(savedInstanceState);
setContentView(R.layout.podcasts);
emptyTextView = findViewById(R.id.select_podcasts_empty);
channelItemsListView = (ListView)findViewById(R.id.podcasts_channels_items_list);
channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
if (pc == null) {
return;
}
Intent intent = new Intent(currentContext, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
startActivityForResultWithoutTransition(PodcastsActivity.this, intent);
}
});
load();
}
private void load()
{
BackgroundTask<List<PodcastsChannel>> task = new TabActivityBackgroundTask<List<PodcastsChannel>>(this, true)
{
@Override
protected List<PodcastsChannel> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(PodcastsActivity.this);
List<PodcastsChannel> channels = musicService.getPodcastsChannels(false,PodcastsActivity.this, this);
/* TODO c'est quoi ce nettoyage de cache ?
if (!Util.isOffline(PodcastsActivity.this))
new CacheCleaner(PodcastsActivity.this, getDownloadService()).cleanPlaylists(playlists);
*/
return channels;
}
@Override
protected void done(List<PodcastsChannel> result)
{
channelItemsListView.setAdapter(new PodcastsChannelsAdapter(currentActivity, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
}

View File

@ -41,6 +41,7 @@ import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
@ -53,6 +54,10 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Performs searches and displays the matching artists, albums and songs.
*
@ -83,6 +88,8 @@ public class SearchActivity extends SubsonicTabActivity
private ListAdapter moreSongsAdapter;
private EntryAdapter songAdapter;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
@Override
public void onCreate(Bundle savedInstanceState)
{
@ -419,7 +426,7 @@ public class SearchActivity extends SubsonicTabActivity
{
mergeAdapter.addView(albumsHeading);
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
albumAdapter = new EntryAdapter(this, getImageLoader(), displayedAlbums, false);
albumAdapter = new EntryAdapter(this, imageLoader.getValue().getImageLoader(), displayedAlbums, false);
mergeAdapter.addAdapter(albumAdapter);
if (albums.size() > DEFAULT_ALBUMS)
{
@ -432,7 +439,7 @@ public class SearchActivity extends SubsonicTabActivity
{
mergeAdapter.addView(songsHeading);
List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
songAdapter = new EntryAdapter(this, getImageLoader(), displayedSongs, false);
songAdapter = new EntryAdapter(this, imageLoader.getValue().getImageLoader(), displayedSongs, false);
mergeAdapter.addAdapter(songAdapter);
if (songs.size() > DEFAULT_SONGS)
{
@ -530,7 +537,7 @@ public class SearchActivity extends SubsonicTabActivity
private void onVideoSelected(MusicDirectory.Entry entry)
{
playVideo(entry);
videoPlayer.getValue().playVideo(entry);
}
private void autoplay()

View File

@ -1,191 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import timber.log.Timber;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.GenreAdapter;
import java.util.ArrayList;
import java.util.List;
public class SelectGenreActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshGenreListView;
private ListView genreListView;
private View emptyView;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_genre);
refreshGenreListView = findViewById(R.id.select_genre_refresh);
genreListView = findViewById(R.id.select_genre_list);
refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
genreListView.setOnItemClickListener(this);
emptyView = findViewById(R.id.select_genre_empty);
registerForContextMenu(genreListView);
View browseMenuItem = findViewById(R.id.menu_browse);
menuDrawer.setActiveView(browseMenuItem);
setActionBarSubtitle(R.string.main_genres_title);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Genre>> task = new TabActivityBackgroundTask<List<Genre>>(this, true)
{
@Override
protected List<Genre> doInBackground() throws Throwable
{
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(SelectGenreActivity.this);
List<Genre> genres = new ArrayList<Genre>();
try
{
genres = musicService.getGenres(refresh, SelectGenreActivity.this, this);
}
catch (Exception x)
{
Timber.e(x, "Failed to load genres");
}
return genres;
}
@Override
protected void done(List<Genre> result)
{
emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
if (result != null)
{
genreListView.setAdapter(new GenreAdapter(SelectGenreActivity.this, result));
}
}
};
task.execute();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Genre genre = (Genre) parent.getItemAtPosition(position);
if (genre != null)
{
Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(this));
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
startActivityForResultWithoutTransition(this, intent);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
Intent intent = new Intent(this, DownloadActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent);
return true;
}
return false;
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,391 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PlaylistAdapter;
import java.util.List;
public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshPlaylistsListView;
private ListView playlistsListView;
private View emptyTextView;
private PlaylistAdapter playlistAdapter;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_playlist);
refreshPlaylistsListView = findViewById(R.id.select_playlist_refresh);
playlistsListView = findViewById(R.id.select_playlist_list);
refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = findViewById(R.id.select_playlist_empty);
playlistsListView.setOnItemClickListener(this);
registerForContextMenu(playlistsListView);
View playlistsMenuItem = findViewById(R.id.menu_playlists);
menuDrawer.setActiveView(playlistsMenuItem);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.playlist_label);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = new Intent(this, SelectPlaylistActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Playlist>> task = new TabActivityBackgroundTask<List<Playlist>>(this, true)
{
@Override
protected List<Playlist> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
if (!ActiveServerProvider.Companion.isOffline(SelectPlaylistActivity.this))
new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists);
return playlists;
}
@Override
protected void done(List<Playlist> result)
{
playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(SelectPlaylistActivity.this, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getMenuInflater();
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(!ActiveServerProvider.Companion.isOffline(this));
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
if (playlist == null)
{
return false;
}
Intent intent;
switch (menuItem.getItemId())
{
case R.id.playlist_menu_pin:
downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
break;
case R.id.playlist_menu_unpin:
downloadPlaylist(playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
break;
case R.id.playlist_menu_download:
downloadPlaylist(playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
break;
case R.id.playlist_menu_play_now:
intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
break;
case R.id.playlist_menu_play_shuffled:
intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
break;
case R.id.playlist_menu_delete:
deletePlaylist(playlist);
break;
case R.id.playlist_info:
displayPlaylistInfo(playlist);
break;
case R.id.playlist_update_info:
updatePlaylistInfo(playlist);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Playlist playlist = (Playlist) parent.getItemAtPosition(position);
if (playlist == null)
{
return;
}
Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
}
private void deletePlaylist(final Playlist playlist)
{
new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(SelectPlaylistActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.deletePlaylist(playlist.getId(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
playlistAdapter.remove(playlist);
playlistAdapter.notifyDataSetChanged();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayPlaylistInfo(final Playlist playlist)
{
final TextView textView = new TextView(this);
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
((playlist.getComment() == null) ? "" : playlist.getComment()) +
"\nSong Count: " + playlist.getSongCount() +
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this).setTitle(playlist.getName()).setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updatePlaylistInfo(final Playlist playlist)
{
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
if (dialogView == null)
{
return;
}
final EditText nameBox = (EditText) dialogView.findViewById(R.id.get_playlist_name);
final EditText commentBox = (EditText) dialogView.findViewById(R.id.get_playlist_comment);
final CheckBox publicBox = (CheckBox) dialogView.findViewById(R.id.get_playlist_public);
nameBox.setText(playlist.getName());
commentBox.setText(playlist.getComment());
Boolean pub = playlist.getPublic();
if (pub == null)
{
publicBox.setEnabled(false);
}
else
{
publicBox.setChecked(pub);
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(SelectPlaylistActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
Editable nameBoxText = nameBox.getText();
Editable commentBoxText = commentBox.getText();
String name = nameBoxText != null ? nameBoxText.toString() : null;
String comment = commentBoxText != null ? commentBoxText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.playlist_updated_info, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,43 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.fragment.SettingsFragment;
public class SettingsActivity extends SubsonicTabActivity {
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setSubtitle(R.string.menu_settings);
}
}
}

View File

@ -1,385 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.TimeSpan;
import org.moire.ultrasonic.util.TimeSpanPicker;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ShareAdapter;
import java.util.List;
public class ShareActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshSharesListView;
private ListView sharesListView;
private View emptyTextView;
private ShareAdapter shareAdapter;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_share);
refreshSharesListView = findViewById(R.id.select_share_refresh);
sharesListView = findViewById(R.id.select_share_list);
refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = findViewById(R.id.select_share_empty);
sharesListView.setOnItemClickListener(this);
registerForContextMenu(sharesListView);
View sharesMenuItem = findViewById(R.id.menu_shares);
menuDrawer.setActiveView(sharesMenuItem);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.button_bar_shares);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = new Intent(this, ShareActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Share>> task = new TabActivityBackgroundTask<List<Share>>(this, true)
{
@Override
protected List<Share> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
return musicService.getShares(refresh, ShareActivity.this, this);
}
@Override
protected void done(List<Share> result)
{
sharesListView.setAdapter(shareAdapter = new ShareAdapter(ShareActivity.this, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_share_context, menu);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Share share = (Share) sharesListView.getItemAtPosition(info.position);
if (share == null)
{
return false;
}
switch (menuItem.getItemId())
{
case R.id.share_menu_pin:
downloadShare(share.getId(), share.getName(), true, true, false, false, true, false, false);
break;
case R.id.share_menu_unpin:
downloadShare(share.getId(), share.getName(), false, false, false, false, true, false, true);
break;
case R.id.share_menu_download:
downloadShare(share.getId(), share.getName(), false, false, false, false, true, false, false);
break;
case R.id.share_menu_play_now:
downloadShare(share.getId(), share.getName(), false, false, true, false, false, false, false);
break;
case R.id.share_menu_play_shuffled:
downloadShare(share.getId(), share.getName(), false, false, true, true, false, false, false);
break;
case R.id.share_menu_delete:
deleteShare(share);
break;
case R.id.share_info:
displayShareInfo(share);
break;
case R.id.share_update_info:
updateShareInfo(share);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Share share = (Share) parent.getItemAtPosition(position);
if (share == null)
{
return;
}
Intent intent = new Intent(ShareActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
startActivityForResultWithoutTransition(ShareActivity.this, intent);
}
private void deleteShare(final Share share)
{
new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(ShareActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
musicService.deleteShare(share.getId(), ShareActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
shareAdapter.remove(share);
shareAdapter.notifyDataSetChanged();
Util.toast(ShareActivity.this, getResources().getString(R.string.menu_deleted_share, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
Util.toast(ShareActivity.this, msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayShareInfo(final Share share)
{
final TextView textView = new TextView(this);
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + share.getUsername() +
"\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
"\nURL: " + share.getUrl() +
"\nEntry Count: " + share.getEntries().size() +
"\nVisit Count: " + share.getVisitCount() +
((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this).setTitle("Share Details").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updateShareInfo(final Share share)
{
View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
if (dialogView == null)
{
return;
}
final EditText shareDescription = (EditText) dialogView.findViewById(R.id.share_description);
final TimeSpanPicker timeSpanPicker = (TimeSpanPicker) dialogView.findViewById(R.id.date_picker);
shareDescription.setText(share.getDescription());
CheckBox hideDialogCheckBox = (CheckBox) dialogView.findViewById(R.id.hide_dialog);
CheckBox saveAsDefaultsCheckBox = (CheckBox) dialogView.findViewById(R.id.save_as_defaults);
CheckBox noExpirationCheckBox = (CheckBox) dialogView.findViewById(R.id.timeSpanDisableCheckBox);
noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
timeSpanPicker.setEnabled(!b);
}
});
noExpirationCheckBox.setChecked(true);
timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
hideDialogCheckBox.setVisibility(View.GONE);
saveAsDefaultsCheckBox.setVisibility(View.GONE);
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(ShareActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
if (millis > 0)
{
millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
}
Editable shareDescriptionText = shareDescription.getText();
String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
musicService.updateShare(share.getId(), description, millis, ShareActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(ShareActivity.this, getResources().getString(R.string.playlist_updated_info, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
Util.toast(ShareActivity.this, msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -49,6 +49,7 @@ import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.service.*;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.SubsonicImageLoaderProxy;
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader;
import org.moire.ultrasonic.util.*;
@ -63,10 +64,9 @@ import kotlin.Lazy;
/**
* @author Sindre Mehus
*/
public class SubsonicTabActivity extends ResultActivity implements OnClickListener
public class SubsonicTabActivity extends ResultActivity
{
private static final Pattern COMPILE = Pattern.compile(":");
protected static ImageLoader IMAGE_LOADER;
protected static String theme;
private static SubsonicTabActivity instance;
@ -79,6 +79,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
protected Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
public MenuDrawer menuDrawer;
private int activePosition = 1;
@ -118,18 +119,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
bookmarksMenuItem = findViewById(R.id.menu_bookmarks);
sharesMenuItem = findViewById(R.id.menu_shares);
findViewById(R.id.menu_home).setOnClickListener(this);
findViewById(R.id.menu_browse).setOnClickListener(this);
findViewById(R.id.menu_search).setOnClickListener(this);
findViewById(R.id.menu_playlists).setOnClickListener(this);
findViewById(R.id.menu_podcasts).setOnClickListener(this);
sharesMenuItem.setOnClickListener(this);
chatMenuItem.setOnClickListener(this);
bookmarksMenuItem.setOnClickListener(this);
findViewById(R.id.menu_now_playing).setOnClickListener(this);
findViewById(R.id.menu_settings).setOnClickListener(this);
findViewById(R.id.menu_about).setOnClickListener(this);
findViewById(R.id.menu_exit).setOnClickListener(this);
setActionBarDisplayHomeAsUp(true);
TextView activeView = (TextView) findViewById(menuActiveViewId);
@ -203,7 +192,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
super.onDestroy();
destroyed = true;
nowPlayingView = null;
clearImageLoader();
imageLoader.getValue().clearImageLoader();
}
@Override
@ -351,7 +340,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
@Override
public void run()
{
getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(context), false, true);
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(context), false, true);
}
});
@ -771,40 +760,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
}
}
public synchronized void clearImageLoader() {
if (IMAGE_LOADER != null &&
IMAGE_LOADER.isRunning()) {
IMAGE_LOADER.clear();
}
IMAGE_LOADER = null;
}
public synchronized ImageLoader getImageLoader() {
if (IMAGE_LOADER == null ||
!IMAGE_LOADER.isRunning()) {
LegacyImageLoader legacyImageLoader = new LegacyImageLoader(
this,
Util.getImageLoaderConcurrency(this)
);
boolean isNewImageLoaderEnabled = KoinJavaComponent.get(FeatureStorage.class)
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER);
if (isNewImageLoaderEnabled) {
IMAGE_LOADER = new SubsonicImageLoaderProxy(
legacyImageLoader,
KoinJavaComponent.get(SubsonicImageLoader.class)
);
} else {
IMAGE_LOADER = legacyImageLoader;
}
IMAGE_LOADER.startImageLoader();
}
return IMAGE_LOADER;
}
void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs)
{
if (getMediaPlayerController() == null)
@ -1024,26 +979,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
task.execute();
}
protected void playVideo(Entry entry)
{
if (!Util.isNetworkConnected(this))
{
Util.toast(this, R.string.select_album_no_network);
return;
}
VideoPlayerType player = Util.getVideoPlayerType(this);
try
{
player.playVideo(this, entry);
}
catch (Exception e)
{
Util.toast(this, e.getMessage(), false);
}
}
protected void checkLicenseAndTrialPeriod(Runnable onValid)
{
if (licenseValid)
@ -1247,72 +1182,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
}
}
@Override
public void onClick(View v)
{
menuActiveViewId = v.getId();
menuDrawer.setActiveView(v);
Intent intent;
switch (menuActiveViewId)
{
case R.id.menu_home:
intent = new Intent(SubsonicTabActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_browse:
intent = new Intent(SubsonicTabActivity.this, SelectArtistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_search:
intent = new Intent(SubsonicTabActivity.this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_playlists:
intent = new Intent(SubsonicTabActivity.this, SelectPlaylistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_podcasts:
intent = new Intent(SubsonicTabActivity.this, PodcastsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_shares:
intent = new Intent(SubsonicTabActivity.this, ShareActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
case R.id.menu_chat:
startActivityForResultWithoutTransition(SubsonicTabActivity.this, ChatActivity.class);
break;
case R.id.menu_bookmarks:
startActivityForResultWithoutTransition(this, BookmarkActivity.class);
break;
case R.id.menu_now_playing:
startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
break;
case R.id.menu_settings:
startActivityForResultWithoutTransition(SubsonicTabActivity.this, SettingsActivity.class);
break;
case R.id.menu_about:
startActivityForResultWithoutTransition(SubsonicTabActivity.this, HelpActivity.class);
break;
case R.id.menu_exit:
intent = new Intent(SubsonicTabActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
startActivityForResultWithoutTransition(SubsonicTabActivity.this, intent);
break;
}
menuDrawer.closeMenu(true);
}
@Override
protected void onRestoreInstanceState(Bundle inState)
{

View File

@ -0,0 +1,147 @@
package org.moire.ultrasonic.fragment;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.util.Util;
import timber.log.Timber;
public class AboutFragment extends Fragment {
private WebView webView;
private ImageView backButton;
private ImageView forwardButton;
private SwipeRefreshLayout swipeRefresh;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.help, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
swipeRefresh = view.findViewById(R.id.help_refresh);
swipeRefresh.setEnabled(false);
webView = view.findViewById(R.id.help_contents);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new HelpClient());
if (savedInstanceState != null)
{
webView.restoreState(savedInstanceState);
}
else
{
webView.loadUrl(getResources().getString(R.string.help_url));
}
backButton = view.findViewById(R.id.help_back);
backButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goBack();
}
});
ImageView stopButton = view.findViewById(R.id.help_stop);
stopButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.stopLoading();
swipeRefresh.setRefreshing(false);
}
});
forwardButton = view.findViewById(R.id.help_forward);
forwardButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goForward();
}
});
// TODO: Nicer Back key handling?
webView.setFocusableInTouchMode(true);
webView.requestFocus();
webView.setOnKeyListener( new View.OnKeyListener()
{
@Override
public boolean onKey( View v, int keyCode, KeyEvent event )
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
if (webView.canGoBack())
{
webView.goBack();
return true;
}
}
return false;
}
} );
}
@Override
public void onSaveInstanceState(Bundle state)
{
webView.saveState(state);
super.onSaveInstanceState(state);
}
private final class HelpClient extends WebViewClient
{
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
swipeRefresh.setRefreshing(true);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url)
{
swipeRefresh.setRefreshing(false);
String versionName = Util.getVersionName(getContext());
String title = String.format("%s (%s)", view.getTitle(), versionName);
FragmentTitle.Companion.setTitle(AboutFragment.this, title);
backButton.setEnabled(view.canGoBack());
forwardButton.setEnabled(view.canGoForward());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
{
Util.toast(getContext(), description);
}
}
}

View File

@ -0,0 +1,475 @@
package org.moire.ultrasonic.fragment;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.List;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
public class BookmarksFragment extends Fragment {
private SwipeRefreshLayout refreshAlbumListView;
private ListView albumListView;
private View albumButtons;
private View emptyView;
private ImageView playNowButton;
private ImageView pinButton;
private ImageView unpinButton;
private ImageView downloadButton;
private ImageView deleteButton;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
private final Lazy<MediaPlayerController> mediaPlayerController = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_album, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
albumButtons = view.findViewById(R.id.menu_album);
refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh);
albumListView = view.findViewById(R.id.select_album_entries_list);
refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (position >= 0)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
if (entry != null)
{
if (entry.isVideo())
{
videoPlayer.getValue().playVideo(entry);
}
else
{
enableButtons();
}
}
}
}
});
ImageView selectButton = view.findViewById(R.id.select_album_select);
playNowButton = view.findViewById(R.id.select_album_play_now);
ImageView playNextButton = view.findViewById(R.id.select_album_play_next);
ImageView playLastButton = view.findViewById(R.id.select_album_play_last);
pinButton = view.findViewById(R.id.select_album_pin);
unpinButton = view.findViewById(R.id.select_album_unpin);
downloadButton = view.findViewById(R.id.select_album_download);
deleteButton = view.findViewById(R.id.select_album_delete);
ImageView oreButton = view.findViewById(R.id.select_album_more);
emptyView = view.findViewById(R.id.select_album_empty);
selectButton.setVisibility(View.GONE);
playNextButton.setVisibility(View.GONE);
playLastButton.setVisibility(View.GONE);
oreButton.setVisibility(View.GONE);
playNowButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
playNow(getSelectedSongs(albumListView));
}
});
selectButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
selectAllOrNone();
}
});
pinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(true);
selectAll(false, false);
}
});
unpinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
unpin();
selectAll(false, false);
}
});
downloadButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(false);
selectAll(false, false);
}
});
deleteButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
delete();
selectAll(false, false);
}
});
registerForContextMenu(albumListView);
FragmentTitle.Companion.setTitle(this, R.string.button_bar_bookmarks);
enableButtons();
getBookmarks();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void getBookmarks()
{
new LoadTask()
{
@Override
protected MusicDirectory load(MusicService service) throws Exception
{
return Util.getSongsFromBookmarks(service.getBookmarks(getContext(), this));
}
}.execute();
}
private void playNow(List<MusicDirectory.Entry> songs)
{
if (!getSelectedSongs(albumListView).isEmpty())
{
int position = songs.get(0).getBookmarkPosition();
mediaPlayerController.getValue().restore(songs, 0, position, true, true);
selectAll(false, false);
}
}
private static List<MusicDirectory.Entry> getSelectedSongs(ListView albumListView)
{
List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
if (albumListView != null)
{
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (albumListView.isItemChecked(i))
{
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i));
}
}
}
return songs;
}
private void refresh()
{
// TODO: create better restart
getView().post(new Runnable() {
public void run() {
Timber.d("Refresh called...");
if (getArguments() == null) {
Bundle bundle = new Bundle();
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
setArguments(bundle);
} else {
getArguments().putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
}
onViewCreated(getView(), null);
}
});
/* finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
*/
}
private void selectAllOrNone()
{
boolean someUnselected = false;
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry)
{
someUnselected = true;
break;
}
}
selectAll(someUnselected, true);
}
private void selectAll(boolean selected, boolean toast)
{
int count = albumListView.getCount();
int selectedCount = 0;
for (int i = 0; i < count; i++)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
if (entry != null && !entry.isDirectory() && !entry.isVideo())
{
albumListView.setItemChecked(i, selected);
selectedCount++;
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast)
{
int toastResId = selected ? R.string.select_album_n_selected : R.string.select_album_n_unselected;
Util.toast(getContext(), getString(toastResId, selectedCount));
}
enableButtons();
}
private void enableButtons()
{
List<MusicDirectory.Entry> selection = getSelectedSongs(albumListView);
boolean enabled = !selection.isEmpty();
boolean unpinEnabled = false;
boolean deleteEnabled = false;
int pinnedCount = 0;
for (MusicDirectory.Entry song : selection)
{
DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song);
if (downloadFile.isWorkDone())
{
deleteEnabled = true;
}
if (downloadFile.isSaved())
{
pinnedCount++;
unpinEnabled = true;
}
}
playNowButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(getContext()) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE);
downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(getContext()) ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
}
private void downloadBackground(final boolean save)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
downloadBackground(save, songs);
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
Runnable onValid = new Runnable()
{
@Override
public void run()
{
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
mediaPlayerController.getValue().downloadBackground(songs, save);
if (save)
{
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
}
else
{
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
}
}
};
onValid.run();
}
private void delete()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
mediaPlayerController.getValue().delete(songs);
}
private void unpin()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
mediaPlayerController.getValue().unpin(songs);
}
private abstract class LoadTask extends TabActivityBackgroundTask<Pair<MusicDirectory, Boolean>>
{
public LoadTask()
{
super(BookmarksFragment.this.getActivity(), true, refreshAlbumListView, cancellationToken);
}
protected abstract MusicDirectory load(MusicService service) throws Exception;
@Override
protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
MusicDirectory dir = load(musicService);
boolean valid = musicService.isLicenseValid(getContext(), this);
return new Pair<>(dir, valid);
}
@Override
protected void done(Pair<MusicDirectory, Boolean> result)
{
MusicDirectory musicDirectory = result.getFirst();
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
int songCount = 0;
for (MusicDirectory.Entry entry : entries)
{
if (!entry.isDirectory())
{
songCount++;
}
}
final int listSize = getArguments() == null? 0 : getArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
if (songCount > 0)
{
pinButton.setVisibility(View.VISIBLE);
unpinButton.setVisibility(View.VISIBLE);
downloadButton.setVisibility(View.VISIBLE);
deleteButton.setVisibility(View.VISIBLE);
playNowButton.setVisibility(View.VISIBLE);
}
else
{
pinButton.setVisibility(View.GONE);
unpinButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
deleteButton.setVisibility(View.GONE);
playNowButton.setVisibility(View.GONE);
if (listSize == 0 || result.getFirst().getChildren().size() < listSize)
{
albumButtons.setVisibility(View.GONE);
}
}
enableButtons();
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
albumListView.setAdapter(new EntryAdapter(getContext(), imageLoader.getValue().getImageLoader(), entries, true));
}
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -0,0 +1,317 @@
package org.moire.ultrasonic.fragment;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ChatAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
public class ChatFragment extends Fragment {
private ListView chatListView;
private EditText messageEditText;
private ImageButton sendButton;
private Timer timer;
private volatile static Long lastChatMessageTime = (long) 0;
private static final ArrayList<ChatMessage> messageList = new ArrayList<ChatMessage>();
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.chat, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
messageEditText = view.findViewById(R.id.chat_edittext);
sendButton = view.findViewById(R.id.chat_send);
sendButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
sendMessage();
}
});
chatListView = view.findViewById(R.id.chat_entries_list);
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
chatListView.setStackFromBottom(true);
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);
FragmentTitle.Companion.setTitle(this, title);
setHasOptionsMenu(true);
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
messageEditText.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void afterTextChanged(Editable editable)
{
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
}
});
messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener()
{
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
{
sendMessage();
return true;
}
return false;
}
});
load();
timerMethod();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.chat, menu);
super.onCreateOptionsMenu(menu, inflater);
}
/*
* Listen for option item selections so that we receive a notification
* when the user requests a refresh by selecting the refresh action bar item.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Check if user triggered a refresh:
case R.id.menu_refresh:
// Start the refresh background task.
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return true;
}
// User didn't trigger a refresh, let the superclass handle this action
return super.onOptionsItemSelected(item);
}
@Override
public void onResume()
{
super.onResume();
if (!messageList.isEmpty())
{
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
chatListView.setAdapter(chatAdapter);
}
if (timer == null)
{
timerMethod();
}
}
@Override
public void onPause()
{
super.onPause();
if (timer != null)
{
timer.cancel();
timer = null;
}
}
private void timerMethod()
{
int refreshInterval = Util.getChatRefreshInterval(getContext());
if (refreshInterval > 0)
{
timer = new Timer();
timer.schedule(new TimerTask()
{
@Override
public void run()
{
getActivity().runOnUiThread(new Runnable()
{
@Override
public void run()
{
load();
}
});
}
}, refreshInterval, refreshInterval);
}
}
private void sendMessage()
{
if (messageEditText != null)
{
final String message;
Editable text = messageEditText.getText();
if (text == null)
{
return;
}
message = text.toString();
if (!Util.isNullOrWhiteSpace(message))
{
messageEditText.setText("");
BackgroundTask<Void> task = new TabActivityBackgroundTask<Void>(getActivity(), false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.addChatMessage(message, getContext(), this);
return null;
}
@Override
protected void done(Void result)
{
load();
}
};
task.execute();
}
}
}
private synchronized void load()
{
BackgroundTask<List<ChatMessage>> task = new TabActivityBackgroundTask<List<ChatMessage>>(getActivity(), false)
{
@Override
protected List<ChatMessage> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getChatMessages(lastChatMessageTime, getContext(), this);
}
@Override
protected void done(List<ChatMessage> result)
{
if (result != null && !result.isEmpty())
{
// Reset lastChatMessageTime if we have a newer message
for (ChatMessage message : result)
{
if (message.getTime() > lastChatMessageTime)
{
lastChatMessageTime = message.getTime();
}
}
// Reverse results to show them on the bottom
Collections.reverse(result);
messageList.addAll(result);
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
chatListView.setAdapter(chatAdapter);
}
}
@Override
protected void error(Throwable error) {
// Stop the timer in case of an error, otherwise it may repeat the error message forever
if (timer != null)
{
timer.cancel();
timer = null;
}
super.error(error);
}
};
task.execute();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
load();
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
return null;
}
}
}

View File

@ -0,0 +1,278 @@
package org.moire.ultrasonic.fragment;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.ServerSelectorActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.Util;
import java.util.Collections;
import kotlin.Lazy;
import static java.util.Arrays.asList;
import static org.koin.java.KoinJavaComponent.inject;
public class MainFragment extends Fragment {
private static boolean shouldUseId3;
private static String lastActiveServerProperties;
private ListView list;
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.main, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
list = view.findViewById(R.id.main_list);
setupMenuList(list);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainFragment.this.getContext());
String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3)
{
shouldUseId3 = id3;
shouldRestart = true;
}
if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{
lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true;
}
if (shouldRestart) {
setupMenuList(list);
}
}
private void setupMenuList(ListView list)
{
final View buttons = getLayoutInflater().inflate(R.layout.main_buttons, null);
final View serverButton = buttons.findViewById(R.id.main_select_server);
final TextView serverTextView = serverButton.findViewById(R.id.main_select_server_2);
lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
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);
final View genresButton = buttons.findViewById(R.id.main_genres_button);
final View videosTitle = buttons.findViewById(R.id.main_videos_title);
final View songsTitle = buttons.findViewById(R.id.main_songs);
final View randomSongsButton = buttons.findViewById(R.id.main_songs_button);
final View songsStarredButton = buttons.findViewById(R.id.main_songs_starred);
final View albumsTitle = buttons.findViewById(R.id.main_albums);
final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
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 MergeAdapter adapter = new MergeAdapter();
adapter.addViews(Collections.singletonList(serverButton), true);
if (!ActiveServerProvider.Companion.isOffline(this.getContext()))
{
adapter.addView(musicTitle, false);
adapter.addViews(asList(artistsButton, albumsButton, genresButton), true);
adapter.addView(songsTitle, false);
adapter.addViews(asList(randomSongsButton, songsStarredButton), true);
adapter.addView(albumsTitle, false);
if (Util.getShouldUseId3Tags(MainFragment.this.getContext()))
{
shouldUseId3 = true;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
else
{
shouldUseId3 = false;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsHighestButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
adapter.addView(videosTitle, false);
adapter.addViews(Collections.singletonList(videosButton), true);
}
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == serverButton)
{
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
}
});
}
private String getActiveServerProperties()
{
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(),
currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(),
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
private void showAlbumList(final String type, final int title)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums(getContext()));
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showStarredSongs()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_STARRED, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showRandomSongs()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(getContext()));
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showArtists()
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getContext().getResources().getString(R.string.main_artists_title));
Navigation.findNavController(getView()).navigate(R.id.selectArtistFragment, bundle);
}
private void showGenres()
{
Navigation.findNavController(getView()).navigate(R.id.mainToSelectGenre);
}
private void showVideos()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showServers()
{
final Intent intent = new Intent(getContext(), ServerSelectorActivity.class);
startActivityForResult(intent, 0);
}
public void startActivityForResultWithoutTransition(Activity currentActivity, Intent intent)
{
startActivityForResult(intent, 0);
Util.disablePendingTransition(currentActivity);
}
}

View File

@ -0,0 +1,393 @@
package org.moire.ultrasonic.fragment;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
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.Playlist;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PlaylistAdapter;
import java.util.List;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
public class PlaylistsFragment extends Fragment {
private SwipeRefreshLayout refreshPlaylistsListView;
private ListView playlistsListView;
private View emptyTextView;
private PlaylistAdapter playlistAdapter;
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_playlist, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh);
playlistsListView = view.findViewById(R.id.select_playlist_list);
refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = view.findViewById(R.id.select_playlist_empty);
playlistsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Playlist playlist = (Playlist) parent.getItemAtPosition(position);
if (playlist == null)
{
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
});
registerForContextMenu(playlistsListView);
FragmentTitle.Companion.setTitle(this, R.string.playlist_label);
load();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void refresh()
{
// TODO: create better restart
getView().post(new Runnable() {
public void run() {
Timber.d("Refresh called...");
if (getArguments() == null) {
Bundle bundle = new Bundle();
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
setArguments(bundle);
} else {
getArguments().putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
}
onViewCreated(getView(), null);
}
});
/* finish();
Intent intent = new Intent(this, SelectPlaylistActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
*/
}
private void load()
{
BackgroundTask<List<Playlist>> task = new TabActivityBackgroundTask<List<Playlist>>(getActivity(), true, refreshPlaylistsListView, cancellationToken)
{
@Override
protected List<Playlist> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
boolean refresh = getArguments() != null && getArguments().getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, false);
List<Playlist> playlists = musicService.getPlaylists(refresh, getContext(), this);
if (!ActiveServerProvider.Companion.isOffline(getContext()))
new CacheCleaner(getContext()).cleanPlaylists(playlists);
return playlists;
}
@Override
protected void done(List<Playlist> result)
{
playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(getContext(), result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getActivity().getMenuInflater();
if (ActiveServerProvider.Companion.isOffline(getContext())) 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(!ActiveServerProvider.Companion.isOffline(getContext()));
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
if (playlist == null)
{
return false;
}
Bundle bundle;
switch (menuItem.getItemId())
{
case R.id.playlist_menu_pin:
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
break;
case R.id.playlist_menu_unpin:
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
break;
case R.id.playlist_menu_download:
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
break;
case R.id.playlist_menu_play_now:
bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
break;
case R.id.playlist_menu_play_shuffled:
bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
break;
case R.id.playlist_menu_delete:
deletePlaylist(playlist);
break;
case R.id.playlist_info:
displayPlaylistInfo(playlist);
break;
case R.id.playlist_update_info:
updatePlaylistInfo(playlist);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
private void deletePlaylist(final Playlist playlist)
{
new AlertDialog.Builder(getContext()).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), false, refreshPlaylistsListView)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.deletePlaylist(playlist.getId(), getContext(), null);
return null;
}
@Override
protected void done(Void result)
{
playlistAdapter.remove(playlist);
playlistAdapter.notifyDataSetChanged();
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayPlaylistInfo(final Playlist playlist)
{
final TextView textView = new TextView(getContext());
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
((playlist.getComment() == null) ? "" : playlist.getComment()) +
"\nSong Count: " + playlist.getSongCount() +
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(getContext()).setTitle(playlist.getName()).setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updatePlaylistInfo(final Playlist playlist)
{
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
if (dialogView == null)
{
return;
}
final EditText nameBox = dialogView.findViewById(R.id.get_playlist_name);
final EditText commentBox = dialogView.findViewById(R.id.get_playlist_comment);
final CheckBox publicBox = dialogView.findViewById(R.id.get_playlist_public);
nameBox.setText(playlist.getName());
commentBox.setText(playlist.getComment());
Boolean pub = playlist.getPublic();
if (pub == null)
{
publicBox.setEnabled(false);
}
else
{
publicBox.setChecked(pub);
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), false, refreshPlaylistsListView)
{
@Override
protected Void doInBackground() throws Throwable
{
Editable nameBoxText = nameBox.getText();
Editable commentBoxText = commentBox.getText();
String name = nameBoxText != null ? nameBoxText.toString() : null;
String comment = commentBoxText != null ? commentBoxText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked(), getContext(), null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -0,0 +1,93 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.PodcastsChannel;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
import java.util.List;
public class PodcastFragment extends Fragment {
private View emptyTextView;
ListView channelItemsListView = null;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.podcasts, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
FragmentTitle.Companion.setTitle(this, R.string.podcasts_label);
emptyTextView = view.findViewById(R.id.select_podcasts_empty);
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list);
channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
if (pc == null) {
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
});
// TODO: Probably a swipeRefresh should be added here in the long run
load();
}
private void load()
{
BackgroundTask<List<PodcastsChannel>> task = new TabActivityBackgroundTask<List<PodcastsChannel>>(getActivity(), true)
{
@Override
protected List<PodcastsChannel> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getPodcastsChannels(false,getContext(), this);
/* TODO Why is here a cache cleaning? (original TODO text: c'est quoi ce nettoyage de cache ?)
if (!Util.isOffline(PodcastsActivity.this))
new CacheCleaner(PodcastsActivity.this, getDownloadService()).cleanPlaylists(playlists);
*/
}
@Override
protected void done(List<PodcastsChannel> result)
{
channelItemsListView.setAdapter(new PodcastsChannelsAdapter(getContext(), result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
}

View File

@ -0,0 +1,586 @@
package org.moire.ultrasonic.fragment;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
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.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
import org.moire.ultrasonic.subsonic.ShareHandler;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ArtistAdapter;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
public class SearchFragment extends Fragment {
private static int DEFAULT_ARTISTS;
private static int DEFAULT_ALBUMS;
private static int DEFAULT_SONGS;
private ListView list;
private View artistsHeading;
private View albumsHeading;
private View songsHeading;
private TextView searchButton;
private View moreArtistsButton;
private View moreAlbumsButton;
private View moreSongsButton;
private SearchResult searchResult;
private MergeAdapter mergeAdapter;
private ArtistAdapter artistAdapter;
private ListAdapter moreArtistsAdapter;
private EntryAdapter albumAdapter;
private ListAdapter moreAlbumsAdapter;
private ListAdapter moreSongsAdapter;
private EntryAdapter songAdapter;
private SwipeRefreshLayout searchRefresh;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private final Lazy<ShareHandler> shareHandler = inject(ShareHandler.class);
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.search, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
cancellationToken = new CancellationToken();
FragmentTitle.Companion.setTitle(this, R.string.search_title);
setHasOptionsMenu(true);
DEFAULT_ARTISTS = Util.getDefaultArtists(getContext());
DEFAULT_ALBUMS = Util.getDefaultAlbums(getContext());
DEFAULT_SONGS = Util.getDefaultSongs(getContext());
View buttons = LayoutInflater.from(getContext()).inflate(R.layout.search_buttons, null);
if (buttons != null)
{
artistsHeading = buttons.findViewById(R.id.search_artists);
albumsHeading = buttons.findViewById(R.id.search_albums);
songsHeading = buttons.findViewById(R.id.search_songs);
searchButton = buttons.findViewById(R.id.search_search);
moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
moreSongsButton = buttons.findViewById(R.id.search_more_songs);
}
list = view.findViewById(R.id.search_list);
searchRefresh = view.findViewById(R.id.search_entries_refresh);
searchRefresh.setEnabled(false); // TODO: Should this be enabled?
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == moreArtistsButton)
{
expandArtists();
}
else if (view == moreAlbumsButton)
{
expandAlbums();
}
else if (view == moreSongsButton)
{
expandSongs();
}
else
{
Object item = parent.getItemAtPosition(position);
if (item instanceof Artist)
{
onArtistSelected((Artist) item);
}
else if (item instanceof MusicDirectory.Entry)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
if (entry.isDirectory())
{
onAlbumSelected(entry, false);
}
else if (entry.isVideo())
{
onVideoSelected(entry);
}
else
{
onSongSelected(entry, false, true, true, false);
}
}
}
}
});
registerForContextMenu(list);
populateList();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
Activity activity = getActivity();
if (activity == null) return;
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
Bundle arguments = getArguments();
final boolean autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
inflater.inflate(R.menu.search, menu);
MenuItem searchItem = menu.findItem(R.id.search_item);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) { return true; }
@Override
public boolean onSuggestionClick(int position) {
Timber.d("onSuggestionClick: %d", position);
Cursor cursor= searchView.getSuggestionsAdapter().getCursor();
cursor.moveToPosition(position);
String suggestion = cursor.getString(2); // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name.
searchView.setQuery(suggestion,true);
return true;
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Timber.d("onQueryTextSubmit: %s", query);
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
search(query, autoPlay);
return true;
}
@Override
public boolean onQueryTextChange(String newText) { return true; }
});
searchView.setIconifiedByDefault(false);
searchItem.expandActionView();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
Object selectedItem = list.getItemAtPosition(info.position);
boolean isArtist = selectedItem instanceof Artist;
boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
if (!isArtist && !isAlbum)
{
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.select_song_context, menu);
}
else
{
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.select_album_context, menu);
}
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(getContext()));
}
if (ActiveServerProvider.Companion.isOffline(getContext()) || isArtist)
{
if (shareButton != null)
{
shareButton.setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return true;
}
Object selectedItem = list.getItemAtPosition(info.position);
Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
String entryId = null;
if (entry != null)
{
entryId = entry.getId();
}
String id = artist != null ? artist.getId() : entryId;
if (id == null)
{
return true;
}
List<MusicDirectory.Entry> songs = new ArrayList<>(1);
switch (menuItem.getItemId())
{
case R.id.album_menu_play_now:
downloadHandler.getValue().downloadRecursively(this, id, false, false, true, false, false, false, false, false);
break;
case R.id.album_menu_play_next:
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false);
break;
case R.id.album_menu_play_last:
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, false, false, false, false, false);
break;
case R.id.album_menu_pin:
downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false);
break;
case R.id.album_menu_unpin:
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, false, false, true, false);
break;
case R.id.album_menu_download:
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, true, false, false, false);
break;
case R.id.song_menu_play_now:
if (entry != null)
{
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, false, false, true, false, false, songs);
}
break;
case R.id.song_menu_play_next:
if (entry != null)
{
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, true, false, false, true, false, songs);
}
break;
case R.id.song_menu_play_last:
if (entry != null)
{
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, true, false, false, false, false, songs);
}
break;
case R.id.song_menu_pin:
if (entry != null)
{
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
downloadBackground(true, songs);
}
break;
case R.id.song_menu_download:
if (entry != null)
{
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
downloadBackground(false, songs);
}
break;
case R.id.song_menu_unpin:
if (entry != null)
{
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
mediaPlayerControllerLazy.getValue().unpin(songs);
}
break;
case R.id.menu_item_share:
if (entry != null)
{
songs = new ArrayList<>(1);
songs.add(entry);
// TODO: Add SwipeRefresh spinner
shareHandler.getValue().createShare(this, songs, null, cancellationToken);
}
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
Runnable onValid = new Runnable()
{
@Override
public void run()
{
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
mediaPlayerControllerLazy.getValue().downloadBackground(songs, save);
}
};
onValid.run();
}
private void search(final String query, final boolean autoplay)
{
final int maxArtists = Util.getMaxArtists(getContext());
final int maxAlbums = Util.getMaxAlbums(getContext());
final int maxSongs = Util.getMaxSongs(getContext());
BackgroundTask<SearchResult> task = new TabActivityBackgroundTask<SearchResult>(getActivity(), true, searchRefresh, cancellationToken)
{
@Override
protected SearchResult doInBackground() throws Throwable
{
SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs);
MusicService service = MusicServiceFactory.getMusicService(getContext());
return service.search(criteria, getContext(), this);
}
@Override
protected void done(SearchResult result)
{
searchResult = result;
populateList();
if (autoplay)
{
autoplay();
}
}
};
task.execute();
}
private void populateList()
{
mergeAdapter = new MergeAdapter();
// TODO: Remove this if the search widget can do the same
//mergeAdapter.addView(searchButton, true);
if (searchResult != null)
{
List<Artist> artists = searchResult.getArtists();
if (!artists.isEmpty())
{
mergeAdapter.addView(artistsHeading);
List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
artistAdapter = new ArtistAdapter(getContext(), displayedArtists);
mergeAdapter.addAdapter(artistAdapter);
if (artists.size() > DEFAULT_ARTISTS)
{
moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
}
}
List<MusicDirectory.Entry> albums = searchResult.getAlbums();
if (!albums.isEmpty())
{
mergeAdapter.addView(albumsHeading);
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
albumAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedAlbums, false);
mergeAdapter.addAdapter(albumAdapter);
if (albums.size() > DEFAULT_ALBUMS)
{
moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
}
}
List<MusicDirectory.Entry> songs = searchResult.getSongs();
if (!songs.isEmpty())
{
mergeAdapter.addView(songsHeading);
List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
songAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedSongs, false);
mergeAdapter.addAdapter(songAdapter);
if (songs.size() > DEFAULT_SONGS)
{
moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
}
}
boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
}
list.setAdapter(mergeAdapter);
}
private void expandArtists()
{
artistAdapter.clear();
for (Artist artist : searchResult.getArtists())
{
artistAdapter.add(artist);
}
artistAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreArtistsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandAlbums()
{
albumAdapter.clear();
for (MusicDirectory.Entry album : searchResult.getAlbums())
{
albumAdapter.add(album);
}
albumAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreAlbumsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandSongs()
{
songAdapter.clear();
for (MusicDirectory.Entry song : searchResult.getSongs())
{
songAdapter.add(song);
}
songAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreSongsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void onArtistSelected(Artist artist)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getId());
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
}
private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
}
private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext)
{
MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue();
if (mediaPlayerController != null)
{
if (!append && !playNext)
{
mediaPlayerController.clear();
}
mediaPlayerController.download(Collections.singletonList(song), save, false, playNext, false, false);
if (autoplay)
{
mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1);
}
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
}
}
private void onVideoSelected(MusicDirectory.Entry entry)
{
videoPlayer.getValue().playVideo(entry);
}
private void autoplay()
{
if (!searchResult.getSongs().isEmpty())
{
onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
}
else if (!searchResult.getAlbums().isEmpty())
{
onAlbumSelected(searchResult.getAlbums().get(0), true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,174 @@
package org.moire.ultrasonic.fragment;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.GenreAdapter;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class SelectGenreFragment extends Fragment {
private SwipeRefreshLayout refreshGenreListView;
private ListView genreListView;
private View emptyView;
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_genre, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshGenreListView = view.findViewById(R.id.select_genre_refresh);
genreListView = view.findViewById(R.id.select_genre_list);
refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
genreListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Genre genre = (Genre) parent.getItemAtPosition(position);
if (genre != null)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(getContext()));
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
}
});
emptyView = view.findViewById(R.id.select_genre_empty);
registerForContextMenu(genreListView);
FragmentTitle.Companion.setTitle(this, R.string.main_genres_title);
load();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void refresh()
{
// TODO: create better restart
getView().post(new Runnable() {
public void run() {
Timber.d("Refresh called...");
if (getArguments() == null) {
Bundle bundle = new Bundle();
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
setArguments(bundle);
} else {
getArguments().putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
}
onViewCreated(getView(), null);
}
});
/* finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
*/
}
private void load()
{
BackgroundTask<List<Genre>> task = new TabActivityBackgroundTask<List<Genre>>(getActivity(), true, refreshGenreListView, cancellationToken)
{
@Override
protected List<Genre> doInBackground() throws Throwable
{
boolean refresh = getArguments() != null && getArguments().getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
List<Genre> genres = new ArrayList<Genre>();
try
{
genres = musicService.getGenres(refresh, getContext(), this);
}
catch (Exception x)
{
Timber.e(x, "Failed to load genres");
}
return genres;
}
@Override
protected void done(List<Genre> result)
{
emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
if (result != null)
{
genreListView.setAdapter(new GenreAdapter(getContext(), result));
}
}
};
task.execute();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -6,10 +6,18 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.*;
import android.provider.SearchRecentSuggestions;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.preference.CheckBoxPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import timber.log.Timber;
import android.view.View;
@ -26,6 +34,7 @@ import org.moire.ultrasonic.log.FileLoggerTree;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.service.Consumer;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.*;
import java.io.File;
@ -38,9 +47,10 @@ import static org.moire.ultrasonic.activity.ServerSelectorActivity.SERVER_SELECT
/**
* Shows main app settings.
*/
public class SettingsFragment extends PreferenceFragment
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener {
private Preference addServerPreference;
private ListPreference theme;
private ListPreference videoPlayer;
private ListPreference maxBitrateWifi;
@ -77,53 +87,60 @@ public class SettingsFragment extends PreferenceFragment
private SharedPreferences settings;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings);
settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.settings, rootKey);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentTitle.Companion.setTitle(this, R.string.menu_settings);
theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME);
videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVERS_EDIT);
theme = findPreference(Constants.PREFERENCES_KEY_THEME);
videoPlayer = findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
maxBitrateWifi = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
maxBitrateMobile = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
cacheSize = findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
cacheLocation = findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
bufferLength = (ListPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
incrementTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
networkTimeout = (ListPreference) findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
maxAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_ALBUMS);
maxSongs = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_SONGS);
maxArtists = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_ARTISTS);
defaultArtists = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS);
defaultSongs = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SONGS);
defaultAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS);
chatRefreshInterval = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH_INTERVAL);
directoryCacheTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME);
mediaButtonsEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS);
lockScreenEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS);
sendBluetoothAlbumArt = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
sendBluetoothNotifications = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
viewRefresh = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
imageLoaderConcurrency = (ListPreference) findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
sharingDefaultDescription = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
sharingDefaultGreeting = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
sharingDefaultExpiration = (TimeSpanPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
serversCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_SERVERS_KEY);
preloadCount = findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
bufferLength = findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
incrementTime = findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
networkTimeout = findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
maxAlbums = findPreference(Constants.PREFERENCES_KEY_MAX_ALBUMS);
maxSongs = findPreference(Constants.PREFERENCES_KEY_MAX_SONGS);
maxArtists = findPreference(Constants.PREFERENCES_KEY_MAX_ARTISTS);
defaultArtists = findPreference(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS);
defaultSongs = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SONGS);
defaultAlbums = findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS);
chatRefreshInterval = findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH_INTERVAL);
directoryCacheTime = findPreference(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME);
mediaButtonsEnabled = findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS);
lockScreenEnabled = findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS);
sendBluetoothAlbumArt = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
sendBluetoothNotifications = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
viewRefresh = findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
imageLoaderConcurrency = findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
sharingDefaultDescription = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
sharingDefaultExpiration = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
serversCategory = findPreference(Constants.PREFERENCES_KEY_SERVERS_KEY);
resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE);
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE);
debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
showArtistPicture = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
setupServersCategory();
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
setupClearSearchPreference();
setupGaplessControlSettingsV14();
@ -133,7 +150,7 @@ public class SettingsFragment extends PreferenceFragment
// After API26 foreground services must be used for music playback, and they must have a notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PreferenceCategory notificationsCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS);
PreferenceCategory notificationsCategory = findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS);
notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION));
notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION));
}
@ -149,8 +166,6 @@ public class SettingsFragment extends PreferenceFragment
@Override
public void onResume() {
super.onResume();
setupServersCategory();
SharedPreferences preferences = Util.getPreferences(getActivity());
preferences.registerOnSharedPreferenceChangeListener(this);
}
@ -158,7 +173,6 @@ public class SettingsFragment extends PreferenceFragment
@Override
public void onPause() {
super.onPause();
SharedPreferences prefs = Util.getPreferences(getActivity());
prefs.unregisterOnSharedPreferenceChangeListener(this);
}
@ -184,6 +198,29 @@ public class SettingsFragment extends PreferenceFragment
}
}
@Override
public void onDisplayPreferenceDialog(Preference preference)
{
DialogFragment dialogFragment = null;
if (preference instanceof TimeSpanPreference)
{
dialogFragment = new TimeSpanPreferenceDialogFragmentCompat();
Bundle bundle = new Bundle(1);
bundle.putString("key", preference.getKey());
dialogFragment.setArguments(bundle);
}
if (dialogFragment != null)
{
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(this.getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG");
}
else
{
super.onDisplayPreferenceDialog(preference);
}
}
private void setupCacheLocationPreference() {
cacheLocation.setSummary(settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,
FileUtil.getDefaultMusicDirectory(getActivity()).getPath()));
@ -202,9 +239,9 @@ public class SettingsFragment extends PreferenceFragment
filePickerDialog.setOnFileSelectedListener(new OnFileSelectedListener() {
@Override
public void onFileSelected(File file, String path) {
SharedPreferences.Editor editor = cacheLocation.getEditor();
SharedPreferences.Editor editor = cacheLocation.getSharedPreferences().edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, path);
editor.commit();
editor.apply();
setCacheLocation(path);
}
@ -234,9 +271,9 @@ public class SettingsFragment extends PreferenceFragment
new Consumer<Integer>() {
@Override
public void accept(Integer choice) {
SharedPreferences.Editor editor = resumeOnBluetoothDevice.getEditor();
SharedPreferences.Editor editor = resumeOnBluetoothDevice.getSharedPreferences().edit();
editor.putInt(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE, choice);
editor.commit();
editor.apply();
resumeOnBluetoothDevice.setSummary(bluetoothDevicePreferenceToString(choice));
}
});
@ -253,9 +290,9 @@ public class SettingsFragment extends PreferenceFragment
new Consumer<Integer>() {
@Override
public void accept(Integer choice) {
SharedPreferences.Editor editor = pauseOnBluetoothDevice.getEditor();
SharedPreferences.Editor editor = pauseOnBluetoothDevice.getSharedPreferences().edit();
editor.putInt(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE, choice);
editor.commit();
editor.apply();
pauseOnBluetoothDevice.setSummary(bluetoothDevicePreferenceToString(choice));
}
});
@ -330,7 +367,7 @@ public class SettingsFragment extends PreferenceFragment
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
featureStorage.changeFeatureFlag(Feature.NEW_IMAGE_DOWNLOADER, (Boolean) o);
((SubsonicTabActivity) getActivity()).clearImageLoader();
imageLoader.getValue().clearImageLoader();
return true;
}
});
@ -355,9 +392,9 @@ public class SettingsFragment extends PreferenceFragment
private void setupGaplessControlSettingsV14() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
PreferenceCategory playbackControlSettings =
(PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS);
findPreference(Constants.PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS);
CheckBoxPreference gaplessPlaybackEnabled =
(CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK);
findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK);
if (gaplessPlaybackEnabled != null) {
gaplessPlaybackEnabled.setChecked(false);
@ -371,15 +408,10 @@ public class SettingsFragment extends PreferenceFragment
}
private void setupServersCategory() {
final Preference addServerPreference = new Preference(getActivity());
addServerPreference.setPersistent(false);
addServerPreference.setTitle(getResources().getString(R.string.settings_server_manage_servers));
addServerPreference.setEnabled(true);
// TODO new server management here
serversCategory.removeAll();
serversCategory.addPreference(addServerPreference);
addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
@ -389,7 +421,6 @@ public class SettingsFragment extends PreferenceFragment
return true;
}
});
}
private void update() {
@ -437,15 +468,15 @@ public class SettingsFragment extends PreferenceFragment
else showArtistPicture.setEnabled(false);
}
private static void setImageLoaderConcurrency(int concurrency) {
private void setImageLoaderConcurrency(int concurrency) {
SubsonicTabActivity instance = SubsonicTabActivity.getInstance();
if (instance != null) {
ImageLoader imageLoader = instance.getImageLoader();
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
if (imageLoader != null) {
imageLoader.stopImageLoader();
imageLoader.setConcurrency(concurrency);
if (imageLoaderInstance != null) {
imageLoaderInstance.stopImageLoader();
imageLoaderInstance.setConcurrency(concurrency);
}
}
}

View File

@ -0,0 +1,388 @@
package org.moire.ultrasonic.fragment;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.TimeSpan;
import org.moire.ultrasonic.util.TimeSpanPicker;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ShareAdapter;
import java.util.List;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
public class SharesFragment extends Fragment {
private SwipeRefreshLayout refreshSharesListView;
private ListView sharesListView;
private View emptyTextView;
private ShareAdapter shareAdapter;
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_share, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshSharesListView = view.findViewById(R.id.select_share_refresh);
sharesListView = view.findViewById(R.id.select_share_list);
refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = view.findViewById(R.id.select_share_empty);
sharesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Share share = (Share) parent.getItemAtPosition(position);
if (share == null)
{
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
});
registerForContextMenu(sharesListView);
FragmentTitle.Companion.setTitle(this, R.string.button_bar_shares);
load();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void refresh()
{
// TODO: create better restart
getView().post(new Runnable() {
public void run() {
Timber.d("Refresh called...");
if (getArguments() == null) {
Bundle bundle = new Bundle();
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
setArguments(bundle);
} else {
getArguments().putBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true);
}
onViewCreated(getView(), null);
}
});
/* finish();
Intent intent = new Intent(this, ShareActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
*/
}
private void load()
{
BackgroundTask<List<Share>> task = new TabActivityBackgroundTask<List<Share>>(getActivity(), true, refreshSharesListView, cancellationToken)
{
@Override
protected List<Share> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
boolean refresh = getArguments() != null && getArguments().getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, false);
return musicService.getShares(refresh, getContext(), this);
}
@Override
protected void done(List<Share> result)
{
sharesListView.setAdapter(shareAdapter = new ShareAdapter(getContext(), result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.select_share_context, menu);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Share share = (Share) sharesListView.getItemAtPosition(info.position);
if (share == null)
{
return false;
}
switch (menuItem.getItemId())
{
case R.id.share_menu_pin:
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), true, true, false, false, true, false, false);
break;
case R.id.share_menu_unpin:
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, true);
break;
case R.id.share_menu_download:
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, false);
break;
case R.id.share_menu_play_now:
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, false, false, false, false);
break;
case R.id.share_menu_play_shuffled:
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, true, false, false, false);
break;
case R.id.share_menu_delete:
deleteShare(share);
break;
case R.id.share_info:
displayShareInfo(share);
break;
case R.id.share_update_info:
updateShareInfo(share);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
private void deleteShare(final Share share)
{
new AlertDialog.Builder(getContext()).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), false, refreshSharesListView)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.deleteShare(share.getId(), getContext(), null);
return null;
}
@Override
protected void done(Void result)
{
shareAdapter.remove(share);
shareAdapter.notifyDataSetChanged();
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_share, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayShareInfo(final Share share)
{
final TextView textView = new TextView(getContext());
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + share.getUsername() +
"\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
"\nURL: " + share.getUrl() +
"\nEntry Count: " + share.getEntries().size() +
"\nVisit Count: " + share.getVisitCount() +
((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(getContext()).setTitle("Share Details").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updateShareInfo(final Share share)
{
View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
if (dialogView == null)
{
return;
}
final EditText shareDescription = dialogView.findViewById(R.id.share_description);
final TimeSpanPicker timeSpanPicker = dialogView.findViewById(R.id.date_picker);
shareDescription.setText(share.getDescription());
CheckBox hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog);
CheckBox saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults);
CheckBox noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox);
noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
timeSpanPicker.setEnabled(!b);
}
});
noExpirationCheckBox.setChecked(true);
timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
hideDialogCheckBox.setVisibility(View.GONE);
saveAsDefaultsCheckBox.setVisibility(View.GONE);
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), false, refreshSharesListView)
{
@Override
protected Void doInBackground() throws Throwable
{
long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
if (millis > 0)
{
millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
}
Editable shareDescriptionText = shareDescription.getText();
String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.updateShare(share.getId(), description, millis, getContext(), null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -76,6 +76,7 @@ public final class Constants
// Preferences keys.
public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
public static final String PREFERENCES_KEY_SERVERS_KEY = "serversKey";
public static final String PREFERENCES_KEY_SERVERS_EDIT = "editServers";
public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
public static final String PREFERENCES_KEY_THEME = "theme";
public static final String PREFERENCES_KEY_THEME_LIGHT = "light";

View File

@ -24,10 +24,13 @@ import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import kotlin.Lazy;
import timber.log.Timber;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import java.io.File;
import java.io.FileInputStream;
@ -43,6 +46,8 @@ import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;
import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Sindre Mehus
*/
@ -55,6 +60,8 @@ public class FileUtil
private static final List<String> PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u");
private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*");
private static Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
public static File getSongFile(Context context, MusicDirectory.Entry song)
{
File dir = getAlbumDirectory(context, song);
@ -154,7 +161,7 @@ public class FileUtil
if (subsonicTabActivity != null)
{
imageLoader = subsonicTabActivity.getImageLoader();
imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (imageLoader != null)
{
@ -224,7 +231,7 @@ public class FileUtil
if (subsonicTabActivity != null)
{
imageLoader = subsonicTabActivity.getImageLoader();
imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (imageLoader != null)
{

View File

@ -1,10 +1,13 @@
package org.moire.ultrasonic.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Build;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
/**
@ -13,29 +16,23 @@ import org.moire.ultrasonic.activity.SubsonicTabActivity;
*/
public abstract class LoadingTask<T> extends BackgroundTask<T>
{
private final SubsonicTabActivity tabActivity;
private final Activity tabActivity;
private final boolean cancellable;
private boolean cancelled;
private SwipeRefreshLayout swipe;
public LoadingTask(SubsonicTabActivity activity, final boolean cancellable)
public LoadingTask(Activity activity, final boolean cancellable, SwipeRefreshLayout swipe)
{
super(activity);
tabActivity = activity;
this.cancellable = cancellable;
this.swipe = swipe;
}
@Override
public void execute()
{
final ProgressDialog loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener()
{
@Override
public void onCancel(DialogInterface dialog)
{
cancelled = true;
}
});
swipe.setRefreshing(true);
new Thread()
{
@ -55,7 +52,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@Override
public void run()
{
loading.cancel();
swipe.setRefreshing(false);
done(result);
}
});
@ -72,7 +69,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@Override
public void run()
{
loading.cancel();
swipe.setRefreshing(false);
error(t);
}
});
@ -84,7 +81,9 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@SuppressLint("NewApi")
private boolean isCancelled()
{
return Build.VERSION.SDK_INT >= 17 ? tabActivity.isDestroyed() || cancelled : cancelled;
// TODO: Implement cancelled
//return Build.VERSION.SDK_INT >= 17 ? tabActivity.isDestroyed() || cancelled : cancelled;
return cancelled;
}
@Override
@ -95,7 +94,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@Override
public void run()
{
// TODO: This seems to be NOOP, can it be removed?
}
});
}

View File

@ -1,6 +1,8 @@
package org.moire.ultrasonic.util;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import android.app.Activity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
/**
* @author Sindre Mehus
@ -9,14 +11,25 @@ import org.moire.ultrasonic.activity.SubsonicTabActivity;
public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
private final SubsonicTabActivity tabActivity;
private final boolean changeProgress;
private final SwipeRefreshLayout swipe;
private CancellationToken cancel;
public TabActivityBackgroundTask(SubsonicTabActivity activity, boolean changeProgress)
// TODO: Try to remove this constructor
public TabActivityBackgroundTask(Activity activity, boolean changeProgress)
{
super(activity);
tabActivity = activity;
this.changeProgress = changeProgress;
this.swipe = null;
}
public TabActivityBackgroundTask(Activity activity, boolean changeProgress,
SwipeRefreshLayout swipe, CancellationToken cancel)
{
super(activity);
this.changeProgress = changeProgress;
this.swipe = swipe;
this.cancel = cancel;
}
@Override
@ -24,7 +37,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(true);
if (swipe != null) swipe.setRefreshing(true);
}
new Thread()
@ -35,7 +48,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
try
{
final T result = doInBackground();
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -47,7 +60,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(false);
if (swipe != null) swipe.setRefreshing(false);
}
done(result);
@ -56,7 +69,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
}
catch (final Throwable t)
{
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -67,7 +80,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(false);
if (swipe != null) swipe.setRefreshing(false);
}
error(t);
@ -78,20 +91,16 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
}.start();
}
private boolean isCancelled()
{
return tabActivity.getIsDestroyed();
}
@Override
public void updateProgress(final String message)
{
// TODO: Remove
getHandler().post(new Runnable()
{
@Override
public void run()
{
tabActivity.updateProgress(message);
//activity.updateProgress(message);
}
});
}

View File

@ -1,10 +1,11 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.DialogPreference;
import org.moire.ultrasonic.R;
import java.util.regex.Pattern;
@ -14,9 +15,7 @@ import java.util.regex.Pattern;
*/
public class TimeSpanPreference extends DialogPreference
{
private static final Pattern COMPILE = Pattern.compile(":");
Context context;
TimeSpanPicker picker;
public TimeSpanPreference(Context context, AttributeSet attrs)
{
@ -27,6 +26,7 @@ public class TimeSpanPreference extends DialogPreference
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
}
public String getText()
@ -40,59 +40,4 @@ public class TimeSpanPreference extends DialogPreference
return this.context.getResources().getString(R.string.time_span_disabled);
}
@Override
public View onCreateDialogView()
{
picker = new TimeSpanPicker(this.context);
picker.setTimeSpanDisableText(this.context.getResources().getString(R.string.no_expiration));
String persisted = getPersistedString("");
if (!"".equals(persisted))
{
String[] split = COMPILE.split(persisted);
if (split.length == 2)
{
String amount = split[0];
if ("0".equals(amount) || "".equals(amount))
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
picker.setTimeSpanAmount(amount);
picker.setTimeSpanType(split[1]);
}
}
else
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
return picker;
}
@Override
protected void onDialogClosed(boolean positiveResult)
{
super.onDialogClosed(positiveResult);
String persisted = "";
if (picker.getTimeSpanEnabled())
{
int tsAmount = picker.getTimeSpanAmount();
if (tsAmount > 0)
{
String tsType = picker.getTimeSpanType();
persisted = String.format("%d:%s", tsAmount, tsType);
}
}
persistString(persisted);
}
}

View File

@ -0,0 +1,88 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.DialogPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceDialogFragmentCompat;
import org.moire.ultrasonic.R;
import java.util.regex.Pattern;
/**
* Created by Joshua Bahnsen on 12/22/13.
*/
public class TimeSpanPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements DialogPreference.TargetFragment
{
private static final Pattern COMPILE = Pattern.compile(":");
Context context;
TimeSpanPicker picker;
@Override
protected View onCreateDialogView(Context context) {
picker = new TimeSpanPicker(context);
this.context = context;
picker.setTimeSpanDisableText(this.context.getResources().getString(R.string.no_expiration));
Preference preference = getPreference();
String persisted = preference.getSharedPreferences().getString(preference.getKey(), "");
if (!"".equals(persisted))
{
String[] split = COMPILE.split(persisted);
if (split.length == 2)
{
String amount = split[0];
if ("0".equals(amount) || "".equals(amount))
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
picker.setTimeSpanAmount(amount);
picker.setTimeSpanType(split[1]);
}
}
else
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
return picker;
}
@Override
public void onDialogClosed(boolean positiveResult)
{
String persisted = "";
if (picker.getTimeSpanEnabled())
{
int tsAmount = picker.getTimeSpanAmount();
if (tsAmount > 0)
{
String tsType = picker.getTimeSpanType();
persisted = String.format("%d:%s", tsAmount, tsType);
}
}
Preference preference = getPreference();
preference.getSharedPreferences().edit().putString(preference.getKey(), persisted).apply();
}
@Nullable
@Override
public Preference findPreference(@NonNull CharSequence key) {
return getPreference();
}
}

View File

@ -53,7 +53,6 @@ import androidx.annotation.ColorInt;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.SettingsActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -604,19 +603,6 @@ public class Util
showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
}
public static void showWelcomeDialog(final Context context, final MainActivity activity, int titleId, int messageId)
{
new AlertDialog.Builder(context).setIcon(android.R.drawable.ic_dialog_info).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
dialog.dismiss();
activity.startActivityForResultWithoutTransition(activity, SettingsActivity.class);
}
}).show();
}
private static void showDialog(Context context, int icon, int titleId, int messageId)
{
new AlertDialog.Builder(context).setIcon(icon).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()

View File

@ -20,6 +20,7 @@ package org.moire.ultrasonic.util;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
@ -38,27 +39,27 @@ public enum VideoPlayerType
MX("mx")
{
@Override
public void playVideo(final Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(final Context context, MusicDirectory.Entry entry) throws Exception
{
// Check if MX Player is installed.
boolean installedAd = Util.isPackageInstalled(activity, PACKAGE_NAME_MX_AD);
boolean installedPro = Util.isPackageInstalled(activity, PACKAGE_NAME_MX_PRO);
boolean installedAd = Util.isPackageInstalled(context, PACKAGE_NAME_MX_AD);
boolean installedPro = Util.isPackageInstalled(context, PACKAGE_NAME_MX_PRO);
if (!installedAd && !installedPro)
{
new AlertDialog.Builder(activity).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener()
new AlertDialog.Builder(context).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
try
{
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD))));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD))));
}
catch (android.content.ActivityNotFoundException x)
{
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD))));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD))));
}
dialog.dismiss();
@ -79,8 +80,8 @@ public enum VideoPlayerType
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(installedPro ? PACKAGE_NAME_MX_PRO : PACKAGE_NAME_MX_AD);
intent.putExtra("title", entry.getTitle());
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), false)), "video/*");
activity.startActivity(intent);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), false)), "video/*");
context.startActivity(intent);
}
}
},
@ -88,22 +89,22 @@ public enum VideoPlayerType
FLASH("flash")
{
@Override
public void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), true)));
activity.startActivity(intent);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), true)));
context.startActivity(intent);
}
},
DEFAULT("default")
{
@Override
public void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), false)), "video/*");
activity.startActivity(intent);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), false)), "video/*");
context.startActivity(intent);
}
};
@ -131,7 +132,7 @@ public enum VideoPlayerType
return null;
}
public abstract void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception;
public abstract void playVideo(Context context, MusicDirectory.Entry entry) throws Exception;
private static final String PACKAGE_NAME_MX_AD = "com.mxtech.videoplayer.ad";
private static final String PACKAGE_NAME_MX_PRO = "com.mxtech.videoplayer.pro";

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.LayoutInflater;
@ -9,11 +10,10 @@ import android.widget.ArrayAdapter;
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.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.util.Util;
import java.text.DateFormat;
import java.util.Date;
@ -26,18 +26,19 @@ import static org.koin.java.KoinJavaComponent.inject;
public class ChatAdapter extends ArrayAdapter<ChatMessage>
{
private final SubsonicTabActivity activity;
private final Context context;
private List<ChatMessage> messages;
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);
private Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
public ChatAdapter(SubsonicTabActivity activity, List<ChatMessage> messages)
public ChatAdapter(Context context, List<ChatMessage> messages)
{
super(activity, R.layout.chat_item, messages);
this.activity = activity;
super(context, R.layout.chat_item, messages);
this.context = context;
this.messages = messages;
}
@ -91,14 +92,14 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
holder.chatMessage = message;
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
ImageLoader imageLoader = activity.getImageLoader();
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
if (imageLoader != null)
if (imageLoaderInstance != null)
{
imageLoader.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
imageLoaderInstance.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
}
holder.username.setText(messageUser);
@ -110,7 +111,7 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
private View inflateView(int layout, ViewGroup parent)
{
return LayoutInflater.from(activity).inflate(layout, parent, false);
return LayoutInflater.from(context).inflate(layout, parent, false);
}
private static ViewHolder createViewHolder(int layout, View convertView)

View File

@ -18,6 +18,7 @@
*/
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
@ -25,7 +26,7 @@ import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.util.ImageLoader;
@ -36,15 +37,15 @@ import java.util.List;
*/
public class EntryAdapter extends ArrayAdapter<Entry>
{
private final SubsonicTabActivity activity;
private final Context context;
private final ImageLoader imageLoader;
private final boolean checkable;
public EntryAdapter(SubsonicTabActivity activity, ImageLoader imageLoader, List<Entry> entries, boolean checkable)
public EntryAdapter(Context context, ImageLoader imageLoader, List<Entry> entries, boolean checkable)
{
super(activity, android.R.layout.simple_list_item_1, entries);
super(context, android.R.layout.simple_list_item_1, entries);
this.activity = activity;
this.context = context;
this.imageLoader = imageLoader;
this.checkable = checkable;
}
@ -75,7 +76,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
}
else
{
view = new AlbumView(activity, imageLoader);
view = new AlbumView(context, imageLoader);
view.setLayout();
}
@ -104,7 +105,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
}
else
{
view = new SongView(activity);
view = new SongView(context);
view.setLayout(entry);
}

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.Playlist;
import java.io.Serializable;
@ -20,12 +20,12 @@ import java.util.List;
public class PlaylistAdapter extends ArrayAdapter<Playlist>
{
private final SubsonicTabActivity activity;
private final Context context;
public PlaylistAdapter(SubsonicTabActivity activity, List<Playlist> Playlists)
public PlaylistAdapter(Context context, List<Playlist> Playlists)
{
super(activity, R.layout.playlist_list_item, Playlists);
this.activity = activity;
super(context, R.layout.playlist_list_item, Playlists);
this.context = context;
}
@Override
@ -44,7 +44,7 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist>
}
else
{
view = new PlaylistView(activity);
view = new PlaylistView(context);
view.setLayout();
}

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.Share;
import java.io.Serializable;
@ -20,12 +20,12 @@ import java.util.List;
public class ShareAdapter extends ArrayAdapter<Share>
{
private final SubsonicTabActivity activity;
private final Context context;
public ShareAdapter(SubsonicTabActivity activity, List<Share> Shares)
public ShareAdapter(Context context, List<Share> Shares)
{
super(activity, R.layout.share_list_item, Shares);
this.activity = activity;
super(context, R.layout.share_list_item, Shares);
this.context = context;
}
@Override
@ -44,7 +44,7 @@ public class ShareAdapter extends ArrayAdapter<Share>
}
else
{
view = new ShareView(activity);
view = new ShareView(context);
view.setLayout();
}

View File

@ -0,0 +1,161 @@
package org.moire.ultrasonic.activity
import android.app.AlertDialog
import android.content.res.Resources
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationView
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* A simple activity demonstrating use of a NavHostFragment with a navigation drawer.
*/
class NavigationActivity : AppCompatActivity() {
private lateinit var appBarConfiguration : AppBarConfiguration
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private var infoDialogDisplayed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.navigation_activity)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val host: NavHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
val navController = host.navController
val drawerLayout : DrawerLayout? = findViewById(R.id.drawer_layout)
appBarConfiguration = AppBarConfiguration(
setOf(R.id.mainFragment, R.id.selectArtistFragment, R.id.searchFragment,
R.id.playlistsFragment, R.id.sharesFragment, R.id.bookmarksFragment,
R.id.chatFragment, R.id.podcastFragment, R.id.settingsFragment,
R.id.aboutFragment),
drawerLayout)
setupActionBar(navController, appBarConfiguration)
setupNavigationMenu(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
val dest: String = try {
resources.getResourceName(destination.id)
} catch (e: Resources.NotFoundException) {
Integer.toString(destination.id)
}
Timber.d("Navigated to $dest")
}
// Determine first run and migrate server settings to DB as early as possible
var showWelcomeScreen = Util.isFirstRun(this)
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
loadSettings()
showInfoDialog(showWelcomeScreen)
}
private fun setupNavigationMenu(navController: NavController) {
val sideNavView = findViewById<NavigationView>(R.id.nav_view)
sideNavView?.setupWithNavController(navController)
// The exit menu is handled here manually
val exitItem: MenuItem = sideNavView.menu.findItem(R.id.menu_exit)
exitItem.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.menu_exit) {
setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.stopJukeboxService()
imageLoaderProvider.getImageLoader().stopImageLoader()
finish()
exit()
}
true
}
}
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
setupActionBarWithNavController(navController, appBarConfig)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val retValue = super.onCreateOptionsMenu(menu)
val navigationView = findViewById<NavigationView>(R.id.nav_view)
if (navigationView == null) {
menuInflater.inflate(R.menu.navigation, menu)
return true
}
return retValue
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment))
|| super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
}
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
val preferences = Util.getPreferences(this)
if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
val editor = preferences.edit()
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(this).path)
editor.apply()
}
}
private fun exit() {
lifecycleSupport.onDestroy()
Util.unregisterMediaButtonEventReceiver(this, false)
finish()
}
private fun showInfoDialog(show: Boolean) {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
if (show) {
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text)
.setPositiveButton(R.string.common_ok) { dialog, i ->
dialog.dismiss()
findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment)
}.show()
}
}
}
}

View File

@ -9,6 +9,7 @@ import org.moire.ultrasonic.activity.ServerSettingsModel
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.AppDatabase
import org.moire.ultrasonic.data.MIGRATION_1_2
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Util
const val SP_NAME = "Default_SP"
@ -32,4 +33,6 @@ val appPermanentStorage = module {
viewModel { ServerSettingsModel(get(), get(), androidContext()) }
single { ActiveServerProvider(get(), androidContext()) }
single { ImageLoaderProvider(androidContext()) }
}

View File

@ -20,6 +20,10 @@ import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.Constants
@ -75,4 +79,9 @@ val musicServiceModule = module {
single { SubsonicImageLoader(androidContext(), get()) }
viewModel { ArtistListModel(get(), androidContext()) }
single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) }
single { VideoPlayer(androidContext()) }
single { ShareHandler(androidContext()) }
}

View File

@ -0,0 +1,6 @@
package org.moire.ultrasonic.fragment
import androidx.fragment.app.Fragment
class DownloadFragment: Fragment() {
}

View File

@ -0,0 +1,32 @@
package org.moire.ultrasonic.fragment
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
class FragmentTitle {
companion object {
fun setTitle(fragment: Fragment, title: CharSequence?) {
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(title)
}
fun setTitle(fragment: Fragment, id: Int) {
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id)
}
fun getTitle(fragment: Fragment): CharSequence? {
return (fragment.activity as AppCompatActivity).supportActionBar?.title
}
fun setSubtitle(fragment: Fragment, title: CharSequence?) {
(fragment.activity as AppCompatActivity).supportActionBar?.setSubtitle(title)
}
fun setSubtitle(fragment: Fragment, id: Int) {
(fragment.activity as AppCompatActivity).supportActionBar?.setSubtitle(id)
}
fun getSubtitle(fragment: Fragment): CharSequence? {
return (fragment.activity as AppCompatActivity).supportActionBar?.subtitle
}
}
}

View File

@ -1,49 +1,38 @@
/*
This file is part of Subsonic.
package org.moire.ultrasonic.fragment
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.ArtistListModel
import org.moire.ultrasonic.activity.ArtistRowAdapter
import org.moire.ultrasonic.activity.ServerSettingsModel
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
/**
* Displays the available Artists in a list
*/
class SelectArtistActivity : SubsonicTabActivity() {
class SelectArtistFragment : Fragment() {
private val activeServerProvider: ActiveServerProvider by inject()
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val artistListModel: ArtistListModel by viewModel()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val downloadHandler: DownloadHandler by inject()
private var refreshArtistListView: SwipeRefreshLayout? = null
private var artistListView: RecyclerView? = null
@ -54,35 +43,44 @@ class SelectArtistActivity : SubsonicTabActivity() {
/**
* Called when the activity is first created.
*/
public override fun onCreate(savedInstanceState: Bundle?) {
@Override
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
setContentView(R.layout.select_artist)
}
refreshArtistListView = findViewById(R.id.select_artist_refresh)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.select_artist, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
refreshArtistListView = view.findViewById(R.id.select_artist_refresh)
refreshArtistListView!!.setOnRefreshListener {
artistListModel.refresh(refreshArtistListView!!)
}
val shouldShowHeader = (!isOffline(this) && !Util.getShouldUseId3Tags(this))
val shouldShowHeader = (!ActiveServerProvider.isOffline(this.context) && !Util.getShouldUseId3Tags(this.context))
val title = arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
val title = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
if (title == null) {
setActionBarSubtitle(
if (isOffline(this)) R.string.music_library_label_offline
setTitle(
this,
if (ActiveServerProvider.isOffline(this.context)) R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
actionBarSubtitle = title
setTitle(this, title)
}
val browseMenuItem = findViewById<View>(R.id.menu_browse)
menuDrawer.setActiveView(browseMenuItem)
musicFolders = null
val refresh = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false)
val refresh = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
artistListModel.getMusicFolders()
.observe(
this,
viewLifecycleOwner,
Observer { changedFolders ->
if (changedFolders != null) {
musicFolders = changedFolders
@ -93,41 +91,26 @@ class SelectArtistActivity : SubsonicTabActivity() {
val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
artists.observe(
this, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
viewLifecycleOwner, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
)
viewManager = LinearLayoutManager(this)
viewManager = LinearLayoutManager(this.context)
viewAdapter = ArtistRowAdapter(
artists.value ?: listOf(),
getText(R.string.select_artist_all_folders).toString(),
shouldShowHeader,
{ artist -> onItemClick(artist) },
{ menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) },
{ view -> onFolderClick(view) },
imageLoader
{ onFolderClick(it) },
imageLoaderProvider.getImageLoader()
)
artistListView = findViewById<RecyclerView>(R.id.select_artist_list).apply {
artistListView = view.findViewById<RecyclerView>(R.id.select_artist_list).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
menuDrawer.toggleMenu()
return true
}
R.id.main_shuffle -> {
val intent = Intent(this, DownloadActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true)
startActivityForResultWithoutTransition(this, intent)
return true
}
}
return false
super.onViewCreated(view, savedInstanceState)
}
private fun getMusicFolderName(musicFolders: List<MusicFolder>): String {
@ -143,16 +126,16 @@ class SelectArtistActivity : SubsonicTabActivity() {
}
private fun onItemClick(artist: Artist) {
val intent = Intent(this, SelectAlbumActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true)
startActivityForResultWithoutTransition(this, intent)
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true)
findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle)
}
private fun onFolderClick(view: View) {
val popup = PopupMenu(this, view)
val popup = PopupMenu(this.context, view)
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
var menuItem = popup.menu.add(
@ -179,17 +162,17 @@ class SelectArtistActivity : SubsonicTabActivity() {
private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean {
when (menuItem.itemId) {
R.id.artist_menu_play_now ->
downloadRecursively(artist.id, false, false, true, false, false, false, false, true)
downloadHandler.downloadRecursively(this, artist.id, false, false, true, false, false, false, false, true)
R.id.artist_menu_play_next ->
downloadRecursively(artist.id, false, false, true, true, false, true, false, true)
downloadHandler.downloadRecursively(this, artist.id, false, false, true, true, false, true, false, true)
R.id.artist_menu_play_last ->
downloadRecursively(artist.id, false, true, false, false, false, false, false, true)
downloadHandler.downloadRecursively(this, artist.id, false, true, false, false, false, false, false, true)
R.id.artist_menu_pin ->
downloadRecursively(artist.id, true, true, false, false, false, false, false, true)
downloadHandler.downloadRecursively(this, artist.id, true, true, false, false, false, false, false, true)
R.id.artist_menu_unpin ->
downloadRecursively(artist.id, false, false, false, false, false, false, true, true)
downloadHandler.downloadRecursively(this, artist.id, false, false, false, false, false, false, true, true)
R.id.artist_menu_download ->
downloadRecursively(artist.id, false, false, false, false, true, false, false, true)
downloadHandler.downloadRecursively(this, artist.id, false, false, false, false, true, false, false, true)
}
return true
}
@ -199,7 +182,7 @@ class SelectArtistActivity : SubsonicTabActivity() {
val musicFolderId = selectedFolder?.id
val musicFolderName = selectedFolder?.name
?: getString(R.string.select_artist_all_folders)
if (!isOffline(this)) {
if (!ActiveServerProvider.isOffline(this.context)) {
val currentSetting = activeServerProvider.getActiveServer()
currentSetting.musicFolderId = musicFolderId
serverSettingsModel.updateItem(currentSetting)
@ -212,4 +195,4 @@ class SelectArtistActivity : SubsonicTabActivity() {
companion object {
private const val MENU_GROUP_MUSIC_FOLDER = 10
}
}
}

View File

@ -0,0 +1,210 @@
package org.moire.ultrasonic.subsonic
import android.app.Activity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.ModalBackgroundTask
import org.moire.ultrasonic.util.Util
import java.util.*
class DownloadHandler(
val mediaPlayerController: MediaPlayerController,
val networkAndStorageChecker: NetworkAndStorageChecker
) {
private val MAX_SONGS = 500
fun download(fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?>) {
val onValid = Runnable {
if (!append && !playNext) {
mediaPlayerController.clear()
}
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.download(songs, save, autoPlay, playNext, shuffle, false)
val playlistName: String? = fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME)
if (playlistName != null) {
mediaPlayerController.suggestedPlaylistName = playlistName
}
if (autoPlay) {
if (Util.getShouldTransitionOnPlaybackPreference(fragment.activity)) {
fragment.findNavController().popBackStack(R.id.downloadFragment, true)
fragment.findNavController().navigate(R.id.downloadFragment)
//startActivityForResultWithoutTransition(this@SubsonicTabActivity, DownloadActivity::class.java)
}
} else if (save) {
Util.toast(fragment.context, fragment.resources.getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size, songs.size))
} else if (playNext) {
Util.toast(fragment.context, fragment.resources.getQuantityString(R.plurals.select_album_n_songs_play_next, songs.size, songs.size))
} else if (append) {
Util.toast(fragment.context, fragment.resources.getQuantityString(R.plurals.select_album_n_songs_added, songs.size, songs.size))
}
}
onValid.run()
}
fun downloadPlaylist(fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean) {
downloadRecursively(fragment, id, name, false, false, save, append, autoplay, shuffle, background, playNext, unpin, false)
}
fun downloadShare(fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean) {
downloadRecursively(fragment, id, name, true, false, save, append, autoplay, shuffle, background, playNext, unpin, false)
}
fun downloadRecursively(
fragment: Fragment,
id: String?,
save: Boolean,
append: Boolean,
autoPlay: Boolean,
shuffle: Boolean,
background: Boolean,
playNext: Boolean,
unpin: Boolean,
isArtist: Boolean
) {
if (id.isNullOrEmpty()) return
downloadRecursively(
fragment,
id,
"",
isShare = false,
isDirectory = true,
save = save,
append = append,
autoPlay = autoPlay,
shuffle = shuffle,
background = background,
playNext = playNext,
unpin = unpin,
isArtist = isArtist)
}
fun downloadRecursively(
fragment: Fragment,
id: String,
name: String?,
isShare: Boolean,
isDirectory: Boolean,
save: Boolean,
append: Boolean,
autoPlay: Boolean,
shuffle: Boolean,
background: Boolean,
playNext: Boolean,
unpin: Boolean,
isArtist: Boolean
) {
val activity = fragment.activity as Activity
val task = object: ModalBackgroundTask<List<MusicDirectory.Entry>>(
activity,
false
) {
@Throws(Throwable::class)
override fun doInBackground(): List<MusicDirectory.Entry> {
// TODO: Handle swipe spinner here instead of the ProgressListener
val musicService = getMusicService(activity)
val songs: MutableList<MusicDirectory.Entry> = LinkedList()
val root: MusicDirectory
if (!isOffline(activity) && isArtist && Util.getShouldUseId3Tags(activity)) {
getSongsForArtist(id, songs)
} else {
if (isDirectory) {
root = if (!isOffline(activity) && Util.getShouldUseId3Tags(activity))
musicService.getAlbum(id, name, false, activity, null)
else
musicService.getMusicDirectory(id, name, false, activity, null)
} else if (isShare) {
root = MusicDirectory()
val shares = musicService.getShares(true, activity, null)
for (share in shares) {
if (share.id == id) {
for (entry in share.getEntries()) {
root.addChild(entry)
}
break
}
}
} else {
root = musicService.getPlaylist(id, name, activity, null)
}
getSongsRecursively(root, songs)
}
return songs
}
@Throws(Exception::class)
private fun getSongsRecursively(parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry>) {
if (songs.size > MAX_SONGS) {
return
}
for (song in parent.getChildren(false, true)) {
if (!song.isVideo) {
songs.add(song)
}
}
val musicService = getMusicService(activity)
for ((id1, _, _, title) in parent.getChildren(true, false)) {
var root: MusicDirectory
root = if (!isOffline(activity) && Util.getShouldUseId3Tags(activity)) musicService.getAlbum(id1, title, false, activity, null)
else musicService.getMusicDirectory(id1, title, false, activity, null)
getSongsRecursively(root, songs)
}
}
@Throws(Exception::class)
private fun getSongsForArtist(id: String, songs: MutableCollection<MusicDirectory.Entry>) {
if (songs.size > MAX_SONGS) {
return
}
val musicService = getMusicService(activity)
val artist = musicService.getArtist(id, "", false, activity, null)
for ((id1) in artist.getChildren()) {
val albumDirectory = musicService.getAlbum(id1, "", false, activity, null)
for (song in albumDirectory.getChildren()) {
if (!song.isVideo) {
songs.add(song)
}
}
}
}
override fun done(songs: List<MusicDirectory.Entry>) {
if (Util.getShouldSortByDisc(activity)) {
Collections.sort(songs, EntryByDiscAndTrackComparator())
}
if (songs.isNotEmpty()) {
if (!append && !playNext && !unpin && !background) {
mediaPlayerController.clear()
}
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
if (!background) {
if (unpin) {
mediaPlayerController.unpin(songs)
} else {
mediaPlayerController.download(songs, save, autoPlay, playNext, shuffle, false)
if (!append && Util.getShouldTransitionOnPlaybackPreference(activity)) {
fragment.findNavController().popBackStack(R.id.downloadFragment, true)
fragment.findNavController().navigate(R.id.downloadFragment)
//startActivityForResultWithoutTransition(activity, DownloadActivity::class.java)
}
}
} else {
if (unpin) {
mediaPlayerController.unpin(songs)
} else {
mediaPlayerController.downloadBackground(songs, save)
}
}
}
}
}
task.execute()
}
}

View File

@ -0,0 +1,45 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import org.koin.java.KoinJavaComponent.get
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.LegacyImageLoader
import org.moire.ultrasonic.util.Util
class ImageLoaderProvider (val context: Context) {
private var imageLoader: ImageLoader? = null
@Synchronized
fun clearImageLoader() {
if (imageLoader != null &&
imageLoader!!.isRunning) {
imageLoader!!.clear()
}
imageLoader = null
}
@Synchronized
fun getImageLoader(): ImageLoader {
if (imageLoader == null || !imageLoader!!.isRunning) {
val legacyImageLoader = LegacyImageLoader(
context,
Util.getImageLoaderConcurrency(context)
)
val isNewImageLoaderEnabled = get(FeatureStorage::class.java)
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
if (isNewImageLoaderEnabled) {
imageLoader = SubsonicImageLoaderProxy(
legacyImageLoader,
get(SubsonicImageLoader::class.java)
)
} else {
imageLoader = legacyImageLoader
}
imageLoader!!.startImageLoader()
}
return imageLoader!!
}
}

View File

@ -0,0 +1,16 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.util.Util
class NetworkAndStorageChecker(val context: Context) {
fun warnIfNetworkOrStorageUnavailable() {
if (!Util.isExternalStoragePresent()) {
Util.toast(context, R.string.select_album_no_sdcard)
} else if (!isOffline(context) && !Util.isNetworkConnected(context)) {
Util.toast(context, R.string.select_album_no_network)
}
}
}

View File

@ -0,0 +1,139 @@
package org.moire.ultrasonic.subsonic
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.widget.CheckBox
import android.widget.EditText
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ShareDetails
import org.moire.ultrasonic.util.TabActivityBackgroundTask
import org.moire.ultrasonic.util.TimeSpan
import org.moire.ultrasonic.util.TimeSpanPicker
import org.moire.ultrasonic.util.Util
import java.util.*
import java.util.regex.Pattern
class ShareHandler(val context: Context) {
private var shareDescription: EditText? = null
private var timeSpanPicker: TimeSpanPicker? = null
private var hideDialogCheckBox: CheckBox? = null
private var noExpirationCheckBox: CheckBox? = null
private var saveAsDefaultsCheckBox: CheckBox? = null
private val pattern = Pattern.compile(":")
fun createShare(fragment: Fragment, entries: List<MusicDirectory.Entry?>?, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
val askForDetails = Util.getShouldAskForShareDetails(context)
val shareDetails = ShareDetails()
shareDetails.Entries = entries
if (askForDetails) {
showDialog(fragment, shareDetails, swipe, cancellationToken)
} else {
shareDetails.Description = Util.getDefaultShareDescription(context)
shareDetails.Expiration = TimeSpan.getCurrentTime().add(Util.getDefaultShareExpirationInMillis(context)).totalMilliseconds
share(fragment, shareDetails, swipe, cancellationToken)
}
}
fun share(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
val task: BackgroundTask<Share> = object : TabActivityBackgroundTask<Share>(fragment.requireActivity(), true, swipe, cancellationToken) {
@Throws(Throwable::class)
override fun doInBackground(): Share {
val ids: MutableList<String?> = ArrayList()
if (shareDetails.Entries.isEmpty()) {
ids.add(fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID))
} else {
for ((id) in shareDetails.Entries) {
ids.add(id)
}
}
val musicService = getMusicService(context)
var timeInMillis: Long = 0
if (shareDetails.Expiration != 0L) {
timeInMillis = shareDetails.Expiration
}
val shares = musicService.createShare(ids, shareDetails.Description, timeInMillis, context, this)
return shares[0]
}
override fun done(result: Share) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, String.format("%s\n\n%s", Util.getShareGreeting(context), result.url))
fragment.activity?.startActivity(Intent.createChooser(intent, context.getResources().getString(R.string.share_via)))
}
}
task.execute()
}
private fun showDialog(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
if (layout != null) {
shareDescription = layout.findViewById<View>(R.id.share_description) as EditText
hideDialogCheckBox = layout.findViewById<View>(R.id.hide_dialog) as CheckBox
noExpirationCheckBox = layout.findViewById<View>(R.id.timeSpanDisableCheckBox) as CheckBox
saveAsDefaultsCheckBox = layout.findViewById<View>(R.id.save_as_defaults) as CheckBox
timeSpanPicker = layout.findViewById<View>(R.id.date_picker) as TimeSpanPicker
}
val builder = AlertDialog.Builder(fragment.context)
builder.setTitle(R.string.share_set_share_options)
builder.setPositiveButton(R.string.common_save) { dialog, clickId ->
if (!noExpirationCheckBox!!.isChecked) {
val timeSpan: TimeSpan = timeSpanPicker!!.timeSpan
val now = TimeSpan.getCurrentTime()
shareDetails.Expiration = now.add(timeSpan).totalMilliseconds
}
shareDetails.Description = shareDescription!!.text.toString()
if (hideDialogCheckBox!!.isChecked) {
Util.setShouldAskForShareDetails(context, false)
}
if (saveAsDefaultsCheckBox!!.isChecked) {
val timeSpanType: String = timeSpanPicker!!.timeSpanType
val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount
Util.setDefaultShareExpiration(context, if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0) String.format("%d:%s", timeSpanAmount, timeSpanType) else "")
Util.setDefaultShareDescription(context, shareDetails.Description)
}
share(fragment, shareDetails, swipe, cancellationToken)
}
builder.setNegativeButton(R.string.common_cancel) { dialog, clickId ->
dialog.cancel()
}
builder.setView(layout)
builder.setCancelable(true)
timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration))
noExpirationCheckBox!!.setOnCheckedChangeListener { _, b -> timeSpanPicker!!.isEnabled = !b }
val defaultDescription = Util.getDefaultShareDescription(context)
val timeSpan = Util.getDefaultShareExpiration(context)
val split = pattern.split(timeSpan)
if (split.size == 2) {
val timeSpanAmount = split[0].toInt()
val timeSpanType = split[1]
if (timeSpanAmount > 0) {
noExpirationCheckBox!!.isChecked = false
timeSpanPicker!!.isEnabled = true
timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString())
timeSpanPicker!!.setTimeSpanType(timeSpanType)
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
shareDescription!!.setText(defaultDescription)
builder.create()
builder.show()
}
}

View File

@ -0,0 +1,21 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Util
class VideoPlayer(val context: Context) {
fun playVideo(entry: MusicDirectory.Entry?) {
if (!Util.isNetworkConnected(context)) {
Util.toast(context, R.string.select_album_no_network)
return
}
val player = Util.getVideoPlayerType(context)
try {
player.playVideo(context, entry)
} catch (e: Exception) {
Util.toast(context, e.message, false)
}
}
}

View File

@ -0,0 +1,33 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2021 (C) Jozsef Varga
*/
package org.moire.ultrasonic.util
/**
* This class contains a very simple implementation of a CancellationToken
*/
class CancellationToken {
var isCancellationRequested: Boolean = false
/**
* Requests that this token be cancelled
*/
fun cancel() {
isCancellationRequested = true;
}
}

View File

@ -29,12 +29,18 @@
</LinearLayout>
<WebView
a:id="@+id/help_contents"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/help_refresh"
a:layout_width="fill_parent"
a:layout_height="match_parent"
a:layout_below="@id/help_buttons"
a:fadingEdge="vertical"
a:fadingEdgeLength="12dip" />
a:layout_below="@id/help_buttons">
<WebView
a:id="@+id/help_contents"
a:layout_width="fill_parent"
a:layout_height="match_parent"
a:fadingEdge="vertical"
a:fadingEdgeLength="12dip" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/drawer_layout"
a:layout_width="match_parent"
a:layout_height="match_parent"
tools:context="org.moire.ultrasonic.activity.NavigationActivity">
<LinearLayout
a:layout_width="match_parent"
a:layout_height="match_parent"
a:orientation="vertical">
<androidx.appcompat.widget.Toolbar
a:id="@+id/toolbar"
a:layout_width="match_parent"
a:layout_height="wrap_content" />
<fragment
a:id="@+id/nav_host_fragment"
a:name="androidx.navigation.fragment.NavHostFragment"
a:layout_width="match_parent"
a:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_graph" />
</LinearLayout>
<com.google.android.material.navigation.NavigationView
a:id="@+id/nav_view"
a:layout_width="wrap_content"
a:layout_height="match_parent"
a:layout_gravity="start"
a:fitsSystemWindows="true"
a:theme="@style/ThemeOverlay.AppCompat.navTheme"
app:headerLayout="@layout/navigation_header"
app:menu="@menu/navigation"/>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/view_container"
a:layout_width="match_parent"
a:layout_height="96dp"
a:gravity="bottom"
a:orientation="vertical"
a:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
a:id="@+id/img_header_bg"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:scaleType="fitXY"
a:src="@drawable/ic_launcher_background" />
<LinearLayout
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_centerVertical="true"
a:orientation="horizontal"
a:padding="16dp">
<ImageView
a:id="@+id/img_profile"
a:layout_width="52dp"
a:layout_height="52dp"
a:src="@drawable/ic_stat_ultrasonic" />
<TextView
a:id="@+id/name"
a:layout_width="wrap_content"
a:layout_height="match_parent"
a:gravity="center_vertical"
a:paddingStart="16dp"
a:paddingLeft="16dp"
a:text="@string/common.appname"
a:textAppearance="@style/TextAppearance.AppCompat.Title" />
</LinearLayout>
</RelativeLayout>

View File

@ -4,7 +4,11 @@
a:layout_width="fill_parent"
a:layout_height="fill_parent">
<include layout="@layout/tab_progress"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/search_entries_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<ListView
a:id="@+id/search_list"
@ -12,6 +16,8 @@
a:layout_height="0dip"
a:layout_weight="1.0"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/now_playing" />
</LinearLayout>

View File

@ -9,8 +9,6 @@
a:layout_height="1dp"
a:background="@color/dividerColor" />
<include layout="@layout/tab_progress" />
<TextView
a:id="@+id/select_album_empty"
a:layout_width="fill_parent"

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:a="http://schemas.android.com/apk/res/android">
<group
a:checkableBehavior="none"
a:enabled="true"
a:visible="true">
<item
a:id="@+id/mainFragment"
a:checkable="true"
a:icon="?attr/home"
a:title="@string/button_bar.home" />
<item
a:id="@+id/selectArtistFragment"
a:checkable="true"
a:icon="?attr/browse"
a:title="@string/button_bar.browse" />
<item
a:id="@+id/searchFragment"
a:checkable="true"
a:icon="?attr/search"
a:title="@string/button_bar.search" />
<item
a:id="@+id/playlistsFragment"
a:checkable="true"
a:icon="?attr/playlists"
a:title="@string/button_bar.playlists" />
<item
a:id="@+id/sharesFragment"
a:checkable="true"
a:icon="?attr/share"
a:title="@string/button_bar.shares" />
<item
a:id="@+id/bookmarksFragment"
a:checkable="true"
a:icon="?attr/bookmark"
a:title="@string/button_bar.bookmarks" />
<item
a:id="@+id/chatFragment"
a:checkable="true"
a:icon="?attr/chat"
a:title="@string/button_bar.chat" />
<item
a:id="@+id/menu_now_playing"
a:checkable="true"
a:icon="?attr/media_play"
a:title="@string/button_bar.now_playing" />
<item
a:id="@+id/podcastFragment"
a:checkable="true"
a:icon="?attr/podcasts"
a:title="@string/button_bar.podcasts" />
</group>
<item a:title="@string/menu.common">
<menu>
<item
a:id="@+id/settingsFragment"
a:checkable="true"
a:icon="?attr/settings"
a:title="@string/menu.settings" />
<item
a:id="@+id/aboutFragment"
a:checkable="true"
a:icon="?attr/about"
a:title="@string/menu.about" />
<item
a:id="@+id/menu_exit"
a:checkable="true"
a:icon="?attr/exit"
a:title="@string/menu.exit" />
</menu>
</item>
</menu>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:a="http://schemas.android.com/apk/res/android">
<item a:id="@+id/search_item"
a:title="@string/search.label"
a:icon="?attr/searchIcon"
app:showAsAction="always"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph"
app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="org.moire.ultrasonic.fragment.MainFragment"
android:label="@string/common.appname" >
<action
android:id="@+id/mainToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
<action
android:id="@+id/mainToSelectGenre"
app:destination="@id/selectGenreFragment" />
</fragment>
<fragment
android:id="@+id/selectArtistFragment"
android:name="org.moire.ultrasonic.fragment.SelectArtistFragment"
android:label="@string/music_library.label" >
<action
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/downloadFragment"
android:name="org.moire.ultrasonic.fragment.DownloadFragment" />
<fragment
android:id="@+id/selectAlbumFragment"
android:name="org.moire.ultrasonic.fragment.SelectAlbumFragment"
android:label="SelectAlbumFragment" >
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="org.moire.ultrasonic.fragment.SearchFragment"
android:label="SearchFragment" >
<action
android:id="@+id/searchToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/playlistsFragment"
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment"
android:label="PlaylistsFragment" >
<action
android:id="@+id/playlistsToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/sharesFragment"
android:name="org.moire.ultrasonic.fragment.SharesFragment"
android:label="SharesFragment" >
<action
android:id="@+id/sharesToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/bookmarksFragment"
android:name="org.moire.ultrasonic.fragment.BookmarksFragment"
android:label="BookmarksFragment" />
<fragment
android:id="@+id/chatFragment"
android:name="org.moire.ultrasonic.fragment.ChatFragment"
android:label="ChatFragment" />
<fragment
android:id="@+id/podcastFragment"
android:name="org.moire.ultrasonic.fragment.PodcastFragment"
android:label="PodcastFragment" >
<action
android:id="@+id/podcastToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="org.moire.ultrasonic.fragment.SettingsFragment"
android:label="SettingsFragment" />
<fragment
android:id="@+id/aboutFragment"
android:name="org.moire.ultrasonic.fragment.AboutFragment"
android:label="AboutFragment" />
<fragment
android:id="@+id/selectGenreFragment"
android:name="org.moire.ultrasonic.fragment.SelectGenreFragment"
android:label="SelectGenreFragment" />
</navigation>

View File

@ -9,6 +9,11 @@
<item name="mdAllowIndicatorAnimation">true</item>
</style>
<style name="NoActionBar" parent="@style/Theme.AppCompat">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="MenuDrawer"/>
<style name="MenuDrawer.Widget"/>
@ -40,6 +45,11 @@
<item name="cornerSize">8dp</item>
</style>
<style name="ThemeOverlay.AppCompat.navTheme">
<item name="colorPrimary">@color/cyan</item>
<item name="colorControlHighlight">?attr/color_selected</item>
</style>
<attr name="star_hollow" format="reference"/>
<attr name="star_full" format="reference"/>
<attr name="about" format="reference"/>

View File

@ -1,327 +1,404 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:a="http://schemas.android.com/apk/res/android"
a:title="@string/common.appname">
xmlns:app="http://schemas.android.com/apk/res-auto" a:title="@string/common.appname">
<PreferenceCategory
a:key="serversKey"
a:title="@string/settings.servers_title">
a:title="@string/settings.servers_title"
app:iconSpaceReserved="false">
<Preference
a:key="editServers"
a:title="@string/settings.server_manage_servers"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.appearance_title">
<PreferenceCategory
a:title="@string/settings.appearance_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="@string/preferences_key_theme_dark"
a:entries="@array/themeNames"
a:entryValues="@array/themeValues"
a:key="theme"
a:title="@string/settings.theme_title"/>
a:title="@string/settings.theme_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="serverScaling"
a:summary="@string/settings.server_scaling_summary"
a:title="@string/settings.server_scaling_title"/>
a:title="@string/settings.server_scaling_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="displayBitrateWithArtist"
a:summary="@string/settings.display_bitrate_summary"
a:title="@string/settings.display_bitrate"/>
a:title="@string/settings.display_bitrate"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="useFolderForAlbumArtist"
a:summary="@string/settings.use_folder_for_album_artist_summary"
a:title="@string/settings.use_folder_for_album_artist"/>
a:title="@string/settings.use_folder_for_album_artist"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="showAllSongsByArtist"
a:summary="@string/settings.show_all_songs_by_artist_summary"
a:title="@string/settings.show_all_songs_by_artist"/>
a:title="@string/settings.show_all_songs_by_artist"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="showTrackNumber"
a:summary="@string/settings.show_track_number_summary"
a:title="@string/settings.show_track_number"/>
a:title="@string/settings.show_track_number"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="discAndTrackSort"
a:summary="@string/settings.disc_sort_summary"
a:title="@string/settings.disc_sort"/>
a:title="@string/settings.disc_sort"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="1000"
a:entries="@array/viewRefreshNames"
a:entryValues="@array/viewRefreshValues"
a:key="viewRefresh"
a:title="@string/settings.view_refresh"/>
a:title="@string/settings.view_refresh"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5"
a:entries="@array/imageConcurrencyNames"
a:entryValues="@array/imageConcurrencyValues"
a:key="imageLoaderConcurrency"
a:title="@string/settings.image_loader_concurrency"/>
a:title="@string/settings.image_loader_concurrency"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
a:title="@string/settings.playback_control_title"
a:key="playbackControlSettings">
a:key="playbackControlSettings"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:defaultValue="false"
a:key="useId3Tags"
a:summary="@string/settings.use_id3_summary"
a:title="@string/settings.use_id3"/>
a:title="@string/settings.use_id3"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="showArtistPicture"
a:summary="@string/settings.show_artist_picture_summary"
a:title="@string/settings.show_artist_picture"/>
a:title="@string/settings.show_artist_picture"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="mediaButtons"
a:summary="@string/settings.media_button_summary"
a:title="@string/settings.media_button_title"/>
a:title="@string/settings.media_button_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="transitionToDownloadOnPlay"
a:summary="@string/settings.download_transition_summary"
a:title="@string/settings.download_transition"/>
a:title="@string/settings.download_transition"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="gaplessPlayback"
a:summary="@string/settings.gapless_playback_summary"
a:title="@string/settings.gapless_playback"/>
a:title="@string/settings.gapless_playback"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="clearPlaylist"
a:summary="@string/settings.clear_playlist_summary"
a:title="@string/settings.clear_playlist"/>
a:title="@string/settings.clear_playlist"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="clearBookmark"
a:summary="@string/settings.clear_bookmark_summary"
a:title="@string/settings.clear_bookmark"/>
a:title="@string/settings.clear_bookmark"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="scanMedia"
a:summary="@string/settings.scan_media_summary"
a:title="@string/settings.scan_media"/>
a:title="@string/settings.scan_media"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5000"
a:entries="@array/bufferLengthNames"
a:entryValues="@array/incrementTimeValues"
a:key="incrementTime"
a:title="@string/settings.increment_time"/>
a:title="@string/settings.increment_time"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="@string/settings.playback.resume_play_on_headphones_plug"
a:title="@string/settings.playback.resume_play_on_headphones_plug.title"
a:summary="@string/settings.playback.resume_play_on_headphones_plug.summary" />
a:summary="@string/settings.playback.resume_play_on_headphones_plug.summary"
app:iconSpaceReserved="false"/>
<Preference
a:key="resumeOnBluetoothDevice"
a:title="@string/settings.playback.resume_on_bluetooth_device"/>
a:title="@string/settings.playback.resume_on_bluetooth_device"
app:iconSpaceReserved="false"/>
<Preference
a:key="pauseOnBluetoothDevice"
a:title="@string/settings.playback.pause_on_bluetooth_device"/>
a:title="@string/settings.playback.pause_on_bluetooth_device"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="singleButtonPlayPause"
a:summary="@string/settings.playback.single_button_bluetooth_device_summary"
a:title="@string/settings.playback.single_button_bluetooth_device"/>
a:title="@string/settings.playback.single_button_bluetooth_device"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
a:title="@string/settings.notifications_title"
a:key="notificationsCategory">
a:key="notificationsCategory"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:defaultValue="true"
a:key="showNowPlaying"
a:summary="@string/settings.show_now_playing_summary"
a:title="@string/settings.show_now_playing"/>
a:title="@string/settings.show_now_playing"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="showNotification"
a:summary="@string/settings.show_notification_summary"
a:title="@string/settings.show_notification"/>
a:title="@string/settings.show_notification"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="alwaysShowNotification"
a:summary="@string/settings.show_notification_always_summary"
a:title="@string/settings.show_notification_always"/>
a:title="@string/settings.show_notification_always"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="showLockScreen"
a:summary="@string/settings.show_lockscreen_controls_summary"
a:title="@string/settings.show_lockscreen_controls"/>
a:title="@string/settings.show_lockscreen_controls"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="sendBluetoothNotifications"
a:summary="@string/settings.send_bluetooth_notification_summary"
a:title="@string/settings.send_bluetooth_notification"/>
a:title="@string/settings.send_bluetooth_notification"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="sendBluetoothAlbumArt"
a:summary="@string/settings.send_bluetooth_album_art_summary"
a:title="@string/settings.send_bluetooth_album_art"/>
a:title="@string/settings.send_bluetooth_album_art"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.video_title">
<PreferenceCategory
a:title="@string/settings.video_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="default"
a:entries="@array/videoPlayerNames"
a:entryValues="@array/videoPlayerValues"
a:key="videoPlayer"
a:title="@string/settings.video_player"/>
a:title="@string/settings.video_player"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.sharing_title">
<PreferenceCategory
a:title="@string/settings.sharing_title"
app:iconSpaceReserved="false">
<EditTextPreference
a:key="sharingDefaultDescription"
a:title="@string/settings.share_description_default"/>
a:title="@string/settings.share_description_default"
app:iconSpaceReserved="false"/>
<EditTextPreference
a:key="sharingDefaultGreeting"
a:title="@string/settings.share_greeting_default"/>
a:title="@string/settings.share_greeting_default"
app:iconSpaceReserved="false"/>
<org.moire.ultrasonic.util.TimeSpanPreference
a:defaultValue="0"
a:key="sharingDefaultExpiration"
a:title="@string/settings.share_expiration_default"/>
a:title="@string/settings.share_expiration_default"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="sharingAlwaysAskForDetails"
a:summary="@string/settings.sharing_always_ask_for_details_summary"
a:title="@string/settings.sharing_always_ask_for_details"/>
a:title="@string/settings.sharing_always_ask_for_details"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.network_title">
<PreferenceCategory
a:title="@string/settings.network_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="0"
a:entries="@array/maxBitrateNames"
a:entryValues="@array/maxBitrateValues"
a:key="maxBitrateWifi"
a:title="@string/settings.max_bitrate_wifi"/>
a:title="@string/settings.max_bitrate_wifi"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="0"
a:entries="@array/maxBitrateNames"
a:entryValues="@array/maxBitrateValues"
a:key="maxBitrateMobile"
a:title="@string/settings.max_bitrate_mobile"/>
a:title="@string/settings.max_bitrate_mobile"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="wifiRequiredForDownload"
a:summary="@string/settings.wifi_required_summary"
a:title="@string/settings.wifi_required_title"/>
a:title="@string/settings.wifi_required_title"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5"
a:entries="@array/bufferLengthNames"
a:entryValues="@array/bufferLengthValues"
a:key="bufferLength"
a:title="@string/settings.buffer_length"/>
a:title="@string/settings.buffer_length"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="15000"
a:entries="@array/networkTimeoutNames"
a:entryValues="@array/networkTimeoutValues"
a:key="networkTimeout"
a:title="@string/settings.network_timeout"/>
a:title="@string/settings.network_timeout"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5000"
a:entries="@array/chatRefreshNames"
a:entryValues="@array/chatRefreshValues"
a:key="chatRefreshInterval"
a:title="@string/settings.chat_refresh"/>
a:title="@string/settings.chat_refresh"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.cache_title">
<PreferenceCategory
a:title="@string/settings.cache_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="500"
a:entries="@array/cacheSizeNames"
a:entryValues="@array/cacheSizeValues"
a:key="cacheSize"
a:title="@string/settings.cache_size"/>
a:title="@string/settings.cache_size"
app:iconSpaceReserved="false"/>
<Preference
a:key="cacheLocation"
a:title="@string/settings.cache_location"/>
a:title="@string/settings.cache_location"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="3"
a:entries="@array/preloadCountNames"
a:entryValues="@array/preloadCountValues"
a:key="preloadCount"
a:title="@string/settings.preload"/>
a:title="@string/settings.preload"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="300"
a:entries="@array/directoryCacheTimeNames"
a:entryValues="@array/directoryCacheTimeValues"
a:key="directoryCacheTime"
a:title="@string/settings.directory_cache_time"/>
a:title="@string/settings.directory_cache_time"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.search_title">
<PreferenceCategory
a:title="@string/settings.search_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="3"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="defaultArtists"
a:title="@string/settings.default_artists"/>
a:title="@string/settings.default_artists"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="10"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="maxArtists"
a:title="@string/settings.max_artists"/>
a:title="@string/settings.max_artists"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="defaultAlbums"
a:title="@string/settings.default_albums"/>
a:title="@string/settings.default_albums"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="20"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="maxAlbums"
a:title="@string/settings.max_albums"/>
a:title="@string/settings.max_albums"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="10"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="defaultSongs"
a:title="@string/settings.default_songs"/>
a:title="@string/settings.default_songs"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="25"
a:entries="@array/searchNames"
a:entryValues="@array/searchValues"
a:key="maxSongs"
a:title="@string/settings.max_songs"/>
a:title="@string/settings.max_songs"
app:iconSpaceReserved="false"/>
<Preference
a:key="clearSearchHistory"
a:persistent="false"
a:title="@string/settings.clear_search_history"/>
a:title="@string/settings.clear_search_history"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.other_title">
<PreferenceCategory
a:title="@string/settings.other_title"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:defaultValue="false"
a:key="scrobble"
a:summary="@string/settings.scrobble_summary"
a:title="@string/settings.scrobble_title"/>
a:title="@string/settings.scrobble_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="hideMedia"
a:summary="@string/settings.hide_media_summary"
a:title="@string/settings.hide_media_title"/>
a:title="@string/settings.hide_media_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="screenLitOnDownload"
a:summary="@string/settings.screen_lit_summary"
a:title="@string/settings.screen_lit_title"/>
a:title="@string/settings.screen_lit_title"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/feature_flags_category_title">
<PreferenceCategory
a:title="@string/feature_flags_category_title"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:key="ff_new_image_loader"
a:persistent="false"
a:title="@string/feature_flags_image_loader_title"
a:summary="@string/feature_flags_image_loader_description"
/>
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:key="use_five_star_rating"
a:persistent="false"
a:title="@string/feature_flags_five_star_rating_title"
a:summary="@string/feature_flags_five_star_rating_description"
/>
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.debug.title">
<PreferenceCategory
a:title="@string/settings.debug.title"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:defaultValue="false"
a:key="debugLogToFile"
a:title="@string/settings.debug.log_to_file"
a:summary=""
/>
app:iconSpaceReserved="false"/>
</PreferenceCategory>
</PreferenceScreen>