diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java
deleted file mode 100644
index 1f48b79e..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java
+++ /dev/null
@@ -1,374 +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 .
-
- 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.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.AdapterView;
-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.data.ServerSetting;
-import org.moire.ultrasonic.domain.Artist;
-import org.moire.ultrasonic.domain.Indexes;
-import org.moire.ultrasonic.domain.MusicFolder;
-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.ArtistAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import kotlin.Lazy;
-
-import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel;
-import static org.koin.java.KoinJavaComponent.inject;
-
-public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
-{
- private Lazy activeServerProvider = inject(ActiveServerProvider.class);
- private Lazy serverSettingsModel = viewModel(this, ServerSettingsModel.class);
-
- private static final int MENU_GROUP_MUSIC_FOLDER = 10;
-
- private SwipeRefreshLayout refreshArtistListView;
- private ListView artistListView;
- private View folderButton;
- private TextView folderName;
- private List musicFolders;
-
- /**
- * Called when the activity is first created.
- */
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.select_artist);
-
- refreshArtistListView = findViewById(R.id.select_artist_refresh);
- artistListView = findViewById(R.id.select_artist_list);
-
- refreshArtistListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
- {
- @Override
- public void onRefresh()
- {
- new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- });
-
- artistListView.setOnItemClickListener(this);
-
- folderButton = LayoutInflater.from(this).inflate(R.layout.select_artist_header, artistListView, false);
-
- if (folderButton != null)
- {
- folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2);
- }
-
- if (!ActiveServerProvider.Companion.isOffline(this) && !Util.getShouldUseId3Tags(this))
- {
- artistListView.addHeaderView(folderButton);
- }
-
- registerForContextMenu(artistListView);
-
- String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE);
- if (title == null)
- {
- setActionBarSubtitle(ActiveServerProvider.Companion.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
- }
- else
- {
- setActionBarSubtitle(title);
- }
-
- View browseMenuItem = findViewById(R.id.menu_browse);
- menuDrawer.setActiveView(browseMenuItem);
-
- musicFolders = null;
- load();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu)
- {
- super.onCreateOptionsMenu(menu);
- return true;
- }
-
- private void refresh()
- {
- finish();
- Intent intent = getIntent();
- String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE);
- intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
- intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
- startActivityForResultWithoutTransition(this, intent);
- }
-
- private void selectFolder()
- {
- folderButton.showContextMenu();
- }
-
- private void load()
- {
- BackgroundTask task = new TabActivityBackgroundTask(this, true)
- {
- @Override
- protected Indexes doInBackground() throws Throwable
- {
- boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
- MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
-
- boolean isOffline = ActiveServerProvider.Companion.isOffline(SelectArtistActivity.this);
- boolean useId3Tags = Util.getShouldUseId3Tags(SelectArtistActivity.this);
-
- if (!isOffline && !useId3Tags)
- {
- musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
- }
-
- String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
-
- return !isOffline && useId3Tags ? musicService.getArtists(refresh, SelectArtistActivity.this, this) : musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
- }
-
- @Override
- protected void done(Indexes result)
- {
- if (result != null)
- {
- List artists = new ArrayList(result.getShortcuts().size() + result.getArtists().size());
- artists.addAll(result.getShortcuts());
- artists.addAll(result.getArtists());
- artistListView.setAdapter(new ArtistAdapter(SelectArtistActivity.this, artists));
- }
-
- // Display selected music folder
- if (musicFolders != null)
- {
- String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
- if (musicFolderId == null || musicFolderId.equals(""))
- {
- if (folderName != null)
- {
- folderName.setText(R.string.select_artist_all_folders);
- }
- }
- else
- {
- for (MusicFolder musicFolder : musicFolders)
- {
- if (musicFolder.getId().equals(musicFolderId))
- {
- if (folderName != null)
- {
- folderName.setText(musicFolder.getName());
- }
-
- break;
- }
- }
- }
- }
- }
- };
- task.execute();
- }
-
- @Override
- public void onItemClick(AdapterView> parent, View view, int position, long id)
- {
- if (view == folderButton)
- {
- selectFolder();
- }
- else
- {
- Artist artist = (Artist) parent.getItemAtPosition(position);
-
- if (artist != null)
- {
- Intent intent = new Intent(this, SelectAlbumActivity.class);
- intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
- intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
- intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.getId());
- intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true);
- startActivityForResultWithoutTransition(this, intent);
- }
- }
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
- {
- super.onCreateContextMenu(menu, view, menuInfo);
-
- AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
-
- if (artistListView.getItemAtPosition(info.position) instanceof Artist)
- {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.select_artist_context, menu);
- }
- else if (info.position == 0)
- {
- String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
- MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
-
- if (musicFolderId == null || musicFolderId.isEmpty())
- {
- menuItem.setChecked(true);
- }
-
- if (musicFolders != null)
- {
- for (int i = 0; i < musicFolders.size(); i++)
- {
- MusicFolder musicFolder = musicFolders.get(i);
- menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
-
- if (musicFolder.getId().equals(musicFolderId))
- {
- menuItem.setChecked(true);
- }
- }
- }
-
- menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
- }
-
- MenuItem downloadMenuItem = menu.findItem(R.id.artist_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 true;
- }
-
- Artist artist = (Artist) artistListView.getItemAtPosition(info.position);
-
- if (artist != null)
- {
- switch (menuItem.getItemId())
- {
- case R.id.artist_menu_play_now:
- downloadRecursively(artist.getId(), false, false, true, false, false, false, false, true);
- break;
- case R.id.artist_menu_play_next:
- downloadRecursively(artist.getId(), false, false, true, true, false, true, false, true);
- break;
- case R.id.artist_menu_play_last:
- downloadRecursively(artist.getId(), false, true, false, false, false, false, false, true);
- break;
- case R.id.artist_menu_pin:
- downloadRecursively(artist.getId(), true, true, false, false, false, false, false, true);
- break;
- case R.id.artist_menu_unpin:
- downloadRecursively(artist.getId(), false, false, false, false, false, false, true, true);
- break;
- case R.id.artist_menu_download:
- downloadRecursively(artist.getId(), false, false, false, false, true, false, false, true);
- break;
- default:
- return super.onContextItemSelected(menuItem);
- }
- }
- else if (info.position == 0)
- {
- MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
- String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
- String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders) : selectedFolder.getName();
-
- if (!ActiveServerProvider.Companion.isOffline(this)) {
- ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
- currentSetting.setMusicFolderId(musicFolderId);
- serverSettingsModel.getValue().updateItem(currentSetting);
- }
-
- folderName.setText(musicFolderName);
- refresh();
- }
-
- return true;
- }
-
- @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
- {
- @Override
- protected void onPostExecute(String[] result)
- {
- super.onPostExecute(result);
- }
-
- @Override
- protected String[] doInBackground(Void... params)
- {
- refresh();
- return null;
- }
- }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
index 498b9742..3bd56c61 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
@@ -20,18 +20,7 @@ package org.moire.ultrasonic.util;
import android.app.Activity;
import android.os.Handler;
-import timber.log.Timber;
-import com.fasterxml.jackson.core.JsonParseException;
-import org.moire.ultrasonic.R;
-import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
-import org.moire.ultrasonic.service.SubsonicRESTException;
-import org.moire.ultrasonic.subsonic.RestErrorMapper;
-
-import javax.net.ssl.SSLException;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.security.cert.CertPathValidatorException;
-import java.security.cert.CertificateException;
+import org.moire.ultrasonic.service.CommunicationErrorHandler;
/**
* @author Sindre Mehus
@@ -65,41 +54,13 @@ public abstract class BackgroundTask implements ProgressListener
protected void error(Throwable error)
{
- Timber.w(error);
- new ErrorDialog(activity, getErrorMessage(error), false);
+ CommunicationErrorHandler.Companion.handleError(error, activity);
}
- protected String getErrorMessage(Throwable error) {
- if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
- return activity.getResources().getString(R.string.background_task_no_network);
- } else if (error instanceof FileNotFoundException) {
- return activity.getResources().getString(R.string.background_task_not_found);
- } else if (error instanceof JsonParseException) {
- return activity.getResources().getString(R.string.background_task_parse_error);
- } else if (error instanceof SSLException) {
- if (error.getCause() instanceof CertificateException &&
- error.getCause().getCause() instanceof CertPathValidatorException) {
- return activity.getResources()
- .getString(R.string.background_task_ssl_cert_error,
- error.getCause().getCause().getMessage());
- } else {
- return activity.getResources().getString(R.string.background_task_ssl_error);
- }
- } else if (error instanceof ApiNotSupportedException) {
- return activity.getResources().getString(R.string.background_task_unsupported_api,
- ((ApiNotSupportedException) error).getServerApiVersion());
- } else if (error instanceof IOException) {
- return activity.getResources().getString(R.string.background_task_network_error);
- } else if (error instanceof SubsonicRESTException) {
- return RestErrorMapper.getLocalizedErrorMessage((SubsonicRESTException) error, activity);
- }
-
- String message = error.getMessage();
- if (message != null) {
- return message;
- }
- return error.getClass().getSimpleName();
- }
+ protected String getErrorMessage(Throwable error)
+ {
+ return CommunicationErrorHandler.Companion.getErrorMessage(error, activity);
+ }
@Override
public abstract void updateProgress(final String message);
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt
new file mode 100644
index 00000000..a8b7afdb
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt
@@ -0,0 +1,98 @@
+/*
+ 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 .
+
+ Copyright 2020 (C) Jozsef Varga
+ */
+package org.moire.ultrasonic.activity
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.moire.ultrasonic.data.ActiveServerProvider
+import org.moire.ultrasonic.domain.Artist
+import org.moire.ultrasonic.domain.MusicFolder
+import org.moire.ultrasonic.service.CommunicationErrorHandler
+import org.moire.ultrasonic.service.MusicServiceFactory
+import org.moire.ultrasonic.util.Util
+
+class ArtistListModel(
+ private val activeServerProvider: ActiveServerProvider,
+ private val context: Context
+) : ViewModel() {
+ private val musicFolders: MutableLiveData> = MutableLiveData()
+ private val artists: MutableLiveData> = MutableLiveData()
+
+ fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> {
+ backgroundLoadFromServer(refresh, swipe)
+ return artists
+ }
+
+ fun getMusicFolders(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> {
+ backgroundLoadFromServer(refresh, swipe)
+ return musicFolders
+ }
+
+ fun refresh(swipe: SwipeRefreshLayout) {
+ backgroundLoadFromServer(true, swipe)
+ }
+
+ private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
+ swipe.isRefreshing = true
+ viewModelScope.launch {
+ loadFromServer(refresh, swipe)
+ swipe.isRefreshing = false
+ }
+ }
+
+ private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) =
+ withContext(Dispatchers.IO) {
+ val musicService = MusicServiceFactory.getMusicService(context)
+ val isOffline = ActiveServerProvider.isOffline(context)
+ val useId3Tags = Util.getShouldUseId3Tags(context)
+
+ try {
+ if (!isOffline && !useId3Tags) {
+ musicFolders.postValue(
+ musicService.getMusicFolders(refresh, context, null)
+ )
+ }
+
+ val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
+
+ val result = if (!isOffline && useId3Tags)
+ musicService.getArtists(refresh, context, null)
+ else musicService.getIndexes(musicFolderId, refresh, context, null)
+
+ val retrievedArtists: MutableList =
+ ArrayList(result.shortcuts.size + result.artists.size)
+ retrievedArtists.addAll(result.shortcuts)
+ retrievedArtists.addAll(result.artists)
+ artists.postValue(retrievedArtists)
+ } catch (exception: Exception) {
+ Handler(Looper.getMainLooper()).post {
+ CommunicationErrorHandler.handleError(exception, swipe.context)
+ }
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt
new file mode 100644
index 00000000..c187f2ab
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt
@@ -0,0 +1,141 @@
+/*
+ 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 .
+
+ Copyright 2020 (C) Jozsef Varga
+ */
+package org.moire.ultrasonic.activity
+
+import android.view.LayoutInflater
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.PopupMenu
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
+import org.moire.ultrasonic.domain.Artist
+
+class ArtistRowAdapter(
+ private var artistList: List,
+ private var folderName: String,
+ private var shouldShowHeader: Boolean,
+ val onArtistClick: (Artist) -> Unit,
+ val onContextMenuClick: (MenuItem, Artist) -> Boolean,
+ val onFolderClick: (view: View) -> Unit
+) : RecyclerView.Adapter() {
+
+ fun setData(data: List) {
+ artistList = data.sortedBy { t -> t.name }
+ notifyDataSetChanged()
+ }
+
+ fun setFolderName(name: String) {
+ folderName = name
+ notifyDataSetChanged()
+ }
+
+ class ArtistViewHolder(
+ itemView: View
+ ) : RecyclerView.ViewHolder(itemView) {
+ var section: TextView = itemView.findViewById(R.id.row_section)
+ var textView: TextView = itemView.findViewById(R.id.row_artist_name)
+ var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
+ }
+
+ class HeaderViewHolder(
+ itemView: View
+ ) : RecyclerView.ViewHolder(itemView) {
+ var folderName: TextView = itemView.findViewById(R.id.select_artist_folder_2)
+ var layout: LinearLayout = itemView.findViewById(R.id.select_artist_folder)
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): RecyclerView.ViewHolder {
+ if (viewType == TYPE_ITEM) {
+ val row = LayoutInflater.from(parent.context)
+ .inflate(R.layout.artist_list_item, parent, false)
+ return ArtistViewHolder(row)
+ }
+ val header = LayoutInflater.from(parent.context)
+ .inflate(R.layout.select_artist_header, parent, false)
+ return HeaderViewHolder(header)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder is ArtistViewHolder) {
+ val listPosition = if (shouldShowHeader) position - 1 else position
+ holder.textView.text = artistList[listPosition].name
+ holder.section.text = getSectionForArtist(listPosition)
+ holder.layout.setOnClickListener { onArtistClick(artistList[listPosition]) }
+ holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
+ } else if (holder is HeaderViewHolder) {
+ holder.folderName.text = folderName
+ holder.layout.setOnClickListener { onFolderClick(holder.layout) }
+ }
+ }
+
+ override fun getItemCount() = if (shouldShowHeader) artistList.size + 1 else artistList.size
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0 && shouldShowHeader) TYPE_HEADER else TYPE_ITEM
+ }
+
+ private fun getSectionForArtist(artistPosition: Int): String {
+ if (artistPosition == 0)
+ return getSectionFromName(artistList[artistPosition].name ?: " ")
+
+ val previousArtistSection = getSectionFromName(
+ artistList[artistPosition - 1].name ?: " "
+ )
+ val currentArtistSection = getSectionFromName(
+ artistList[artistPosition].name ?: " "
+ )
+
+ return if (previousArtistSection == currentArtistSection) "" else currentArtistSection
+ }
+
+ private fun getSectionFromName(name: String): String {
+ var section = name.first().toUpperCase()
+ if (!section.isLetter()) section = '#'
+ return section.toString()
+ }
+
+ private fun createPopupMenu(view: View, position: Int): Boolean {
+ val popup = PopupMenu(view.context, view)
+ val inflater: MenuInflater = popup.menuInflater
+ inflater.inflate(R.menu.select_artist_context, popup.menu)
+
+ val downloadMenuItem = popup.menu.findItem(R.id.artist_menu_download)
+ downloadMenuItem?.isVisible = !isOffline(view.context)
+
+ popup.setOnMenuItemClickListener { menuItem ->
+ onContextMenuClick(menuItem, artistList[position])
+ }
+ popup.show()
+ return true
+ }
+
+ companion object {
+ private const val TYPE_HEADER = 0
+ private const val TYPE_ITEM = 1
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt
new file mode 100644
index 00000000..f822ea3d
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt
@@ -0,0 +1,211 @@
+/*
+ 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 .
+
+ Copyright 2020 (C) Jozsef Varga
+ */
+package org.moire.ultrasonic.activity
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.PopupMenu
+import androidx.lifecycle.Observer
+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.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.util.Constants
+import org.moire.ultrasonic.util.Util
+
+class SelectArtistActivity : SubsonicTabActivity() {
+ private val activeServerProvider: ActiveServerProvider by inject()
+ private val serverSettingsModel: ServerSettingsModel by viewModel()
+ private val artistListModel: ArtistListModel by viewModel()
+
+ private var refreshArtistListView: SwipeRefreshLayout? = null
+ private var artistListView: RecyclerView? = null
+ private var musicFolders: List? = null
+ private lateinit var viewManager: RecyclerView.LayoutManager
+ private lateinit var viewAdapter: ArtistRowAdapter
+
+ /**
+ * Called when the activity is first created.
+ */
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.select_artist)
+
+ refreshArtistListView = findViewById(R.id.select_artist_refresh)
+ refreshArtistListView!!.setOnRefreshListener {
+ artistListModel.refresh(refreshArtistListView!!)
+ }
+
+ val shouldShowHeader = (!isOffline(this) && !Util.getShouldUseId3Tags(this))
+
+ val title = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
+ if (title == null) {
+ setActionBarSubtitle(
+ if (isOffline(this)) R.string.music_library_label_offline
+ else R.string.music_library_label
+ )
+ } else {
+ actionBarSubtitle = title
+ }
+ val browseMenuItem = findViewById(R.id.menu_browse)
+ menuDrawer.setActiveView(browseMenuItem)
+ musicFolders = null
+
+ val refresh = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false)
+
+ artistListModel.getMusicFolders(refresh, refreshArtistListView!!)
+ .observe(
+ this,
+ Observer { changedFolders ->
+ if (changedFolders != null) {
+ musicFolders = changedFolders
+ viewAdapter.setFolderName(getMusicFolderName(changedFolders))
+ }
+ }
+ )
+
+ val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
+ artists.observe(
+ this, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
+ )
+
+ viewManager = LinearLayoutManager(this)
+ 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) }
+ )
+
+ artistListView = findViewById(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
+ }
+
+ private fun getMusicFolderName(musicFolders: List): String {
+ val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
+ if (musicFolderId != null && musicFolderId != "") {
+ for ((id, name) in musicFolders) {
+ if (id == musicFolderId) {
+ return name
+ }
+ }
+ }
+ return getText(R.string.select_artist_all_folders).toString()
+ }
+
+ 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)
+ }
+
+ private fun onFolderClick(view: View) {
+ val popup = PopupMenu(this, view)
+
+ val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
+ var menuItem = popup.menu.add(
+ MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
+ )
+ if (musicFolderId == null || musicFolderId.isEmpty()) {
+ menuItem.isChecked = true
+ }
+ if (musicFolders != null) {
+ for (i in musicFolders!!.indices) {
+ val (id, name) = musicFolders!![i]
+ menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
+ if (id == musicFolderId) {
+ menuItem.isChecked = true
+ }
+ }
+ }
+ popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
+
+ popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
+ popup.show()
+ }
+
+ 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)
+ R.id.artist_menu_play_next ->
+ downloadRecursively(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)
+ R.id.artist_menu_pin ->
+ downloadRecursively(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)
+ R.id.artist_menu_download ->
+ downloadRecursively(artist.id, false, false, false, false, true, false, false, true)
+ }
+ return true
+ }
+
+ private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
+ val selectedFolder = if (menuItem.itemId == -1) null else musicFolders!![menuItem.itemId]
+ val musicFolderId = selectedFolder?.id
+ val musicFolderName = selectedFolder?.name
+ ?: getString(R.string.select_artist_all_folders)
+ if (!isOffline(this)) {
+ val currentSetting = activeServerProvider.getActiveServer()
+ currentSetting.musicFolderId = musicFolderId
+ serverSettingsModel.updateItem(currentSetting)
+ }
+ viewAdapter.setFolderName(musicFolderName)
+ artistListModel.refresh(refreshArtistListView!!)
+ return true
+ }
+
+ companion object {
+ private const val MENU_GROUP_MUSIC_FOLDER = 10
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
index fba4b146..ae6b75c6 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
@@ -7,6 +7,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig
+import org.moire.ultrasonic.activity.ArtistListModel
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
@@ -71,4 +72,5 @@ val musicServiceModule = module {
}
single { SubsonicImageLoader(androidContext(), get()) }
+ single { ArtistListModel(get(), get()) }
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
new file mode 100644
index 00000000..23436a7c
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
@@ -0,0 +1,81 @@
+/*
+ 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 .
+
+ Copyright 2020 (C) Jozsef Varga
+ */
+package org.moire.ultrasonic.service
+
+import android.app.AlertDialog
+import android.content.Context
+import com.fasterxml.jackson.core.JsonParseException
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.security.cert.CertPathValidatorException
+import java.security.cert.CertificateException
+import javax.net.ssl.SSLException
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
+import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
+import org.moire.ultrasonic.util.Util
+import timber.log.Timber
+
+class CommunicationErrorHandler {
+ companion object {
+ fun handleError(error: Throwable?, context: Context) {
+ Timber.w(error)
+
+ AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.error_label)
+ .setMessage(getErrorMessage(error!!, context))
+ .setCancelable(true)
+ .setPositiveButton(R.string.common_ok) { _, _ -> }
+ .create().show()
+ }
+
+ fun getErrorMessage(error: Throwable, context: Context): String {
+ if (error is IOException && !Util.isNetworkConnected(context)) {
+ return context.resources.getString(R.string.background_task_no_network)
+ } else if (error is FileNotFoundException) {
+ return context.resources.getString(R.string.background_task_not_found)
+ } else if (error is JsonParseException) {
+ return context.resources.getString(R.string.background_task_parse_error)
+ } else if (error is SSLException) {
+ return if (
+ error.cause is CertificateException &&
+ error.cause?.cause is CertPathValidatorException
+ ) {
+ context.resources
+ .getString(
+ R.string.background_task_ssl_cert_error, error.cause?.cause?.message
+ )
+ } else {
+ context.resources.getString(R.string.background_task_ssl_error)
+ }
+ } else if (error is ApiNotSupportedException) {
+ return context.resources.getString(
+ R.string.background_task_unsupported_api, error.serverApiVersion
+ )
+ } else if (error is IOException) {
+ return context.resources.getString(R.string.background_task_network_error)
+ } else if (error is SubsonicRESTException) {
+ return error.getLocalizedErrorMessage(context)
+ }
+ val message = error.message
+ return message ?: error.javaClass.simpleName
+ }
+ }
+}
diff --git a/ultrasonic/src/main/res/drawable/line.xml b/ultrasonic/src/main/res/drawable/line.xml
new file mode 100644
index 00000000..e3f2eaac
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/line.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/line_drawable.xml b/ultrasonic/src/main/res/drawable/line_drawable.xml
new file mode 100644
index 00000000..4f79f470
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/line_drawable.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/thumb.xml b/ultrasonic/src/main/res/drawable/thumb.xml
new file mode 100644
index 00000000..1a5a8aad
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/thumb.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/thumb_drawable.xml b/ultrasonic/src/main/res/drawable/thumb_drawable.xml
new file mode 100644
index 00000000..a93b8b83
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/thumb_drawable.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/artist_list_item.xml b/ultrasonic/src/main/res/layout/artist_list_item.xml
index 2891750e..d3892691 100644
--- a/ultrasonic/src/main/res/layout/artist_list_item.xml
+++ b/ultrasonic/src/main/res/layout/artist_list_item.xml
@@ -1,11 +1,37 @@
-
\ No newline at end of file
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/select_artist.xml b/ultrasonic/src/main/res/layout/select_artist.xml
index 43a1a8ae..78448455 100644
--- a/ultrasonic/src/main/res/layout/select_artist.xml
+++ b/ultrasonic/src/main/res/layout/select_artist.xml
@@ -2,21 +2,29 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ a:orientation="vertical">
+ a:id="@+id/select_artist_refresh"
+ a:layout_width="fill_parent"
+ a:layout_height="0dip"
+ a:layout_weight="1.0">
-
+
diff --git a/ultrasonic/src/main/res/layout/select_artist_header.xml b/ultrasonic/src/main/res/layout/select_artist_header.xml
index a42df88b..7a56076e 100644
--- a/ultrasonic/src/main/res/layout/select_artist_header.xml
+++ b/ultrasonic/src/main/res/layout/select_artist_header.xml
@@ -7,7 +7,10 @@
a:orientation="horizontal"
a:paddingBottom="2dip"
a:paddingLeft="6dp"
- a:paddingTop="2dip" >
+ a:paddingTop="2dip"
+ a:background="?android:attr/selectableItemBackground"
+ a:clickable="true"
+ a:focusable="true">