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