Migrated SelectArtistActivity to Kotlin

Updated SelectArtistActivity to use RecyclerView
Changed old progress display to use SwipeRefreshLayout's spinner
Added alphabetic side index
Enabled RecyclerView's FastScroll
This commit is contained in:
Nite 2020-11-16 19:14:44 +01:00
parent 6aa29d54fe
commit 34a6413f10
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
14 changed files with 643 additions and 440 deletions

View File

@ -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 <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.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> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> 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<MusicFolder> 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<Indexes> task = new TabActivityBackgroundTask<Indexes>(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<Artist> artists = new ArrayList<Artist>(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<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
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<List<MusicFolder>> = MutableLiveData()
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe)
return artists
}
fun getMusicFolders(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<MusicFolder>> {
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<Artist> =
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)
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<Artist>,
private var folderName: String,
private var shouldShowHeader: Boolean,
val onArtistClick: (Artist) -> Unit,
val onContextMenuClick: (MenuItem, Artist) -> Boolean,
val onFolderClick: (view: View) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun setData(data: List<Artist>) {
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
}
}

View File

@ -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 <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.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<MusicFolder>? = 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<View>(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<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
}
private fun getMusicFolderName(musicFolders: List<MusicFolder>): 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
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<padding
android:top="10dp"
android:left="10dp"
android:right="10dp"
android:bottom="10dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/line"/>
<item
android:drawable="@drawable/line"/>
</selector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="44dp"
android:topRightRadius="44dp"
android:bottomLeftRadius="44dp" />
<padding
android:paddingLeft="22dp"
android:paddingRight="22dp" />
<solid android:color="@color/selected_color_dark" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/thumb"/>
<item
android:drawable="@drawable/thumb"/>
</selector>

View File

@ -1,11 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@android:id/text1"
a:drawablePadding="6dip"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_vertical"
a:paddingLeft="3dip"
a:paddingRight="3dip"
a:minHeight="50dip"/>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/row_artist_layout"
a:layout_height="wrap_content"
a:layout_width="match_parent"
a:background="?android:attr/selectableItemBackground"
a:clickable="true"
a:focusable="true">
<TextView
a:id="@+id/row_section"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:gravity="center_horizontal|center_vertical"
a:minWidth="72dip"
a:minHeight="50dip"
a:paddingStart="16dip"
a:paddingLeft="16dip"
a:paddingEnd="32dip"
a:paddingRight="32dip"
a:text="A"
a:textAppearance="?android:attr/textAppearanceLarge"
a:textColor="@color/cyan" />
<TextView
a:id="@+id/row_artist_name"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_toEndOf="@+id/row_section"
a:layout_toRightOf="@+id/row_section"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:minHeight="50dip"
a:paddingLeft="3dip"
a:paddingRight="3dip"
a:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>

View File

@ -2,21 +2,29 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical" >
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical">
<include layout="@layout/tab_progress" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/select_artist_refresh"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1.0">
a:id="@+id/select_artist_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<ListView
android:id="@+id/select_artist_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.recyclerview.widget.RecyclerView
a:id="@+id/select_artist_list"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -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">
<ImageView
a:layout_width="wrap_content"