diff --git a/dependencies.gradle b/dependencies.gradle index be338ccc..cffa0219 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -38,6 +38,7 @@ ext.versions = [ robolectric : "4.4", dexter : "6.1.2", timber : "4.7.1", + fastScroll : "2.0.1", ] ext.gradlePlugins = [ @@ -77,6 +78,7 @@ ext.other = [ picasso : "com.squareup.picasso:picasso:$versions.picasso", dexter : "com.karumi:dexter:$versions.dexter", timber : "com.jakewharton.timber:timber:$versions.timber", + fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll", ] ext.testing = [ diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 479eb980..415ea07a 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -77,6 +77,7 @@ dependencies { implementation other.koinAndroid implementation other.koinViewModel implementation other.okhttpLogging + implementation other.fastScroll kapt androidSupport.room 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/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 7b1fe14a..c6e97ba0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -63,6 +63,7 @@ public class SettingsFragment extends PreferenceFragment private CheckBoxPreference lockScreenEnabled; private CheckBoxPreference sendBluetoothNotifications; private CheckBoxPreference sendBluetoothAlbumArt; + private CheckBoxPreference showArtistPicture; private ListPreference viewRefresh; private ListPreference imageLoaderConcurrency; private EditTextPreference sharingDefaultDescription; @@ -121,6 +122,7 @@ public class SettingsFragment extends PreferenceFragment resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE); pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE); debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE); + showArtistPicture = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE); sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity())); setupClearSearchPreference(); @@ -176,6 +178,9 @@ public class SettingsFragment extends PreferenceFragment setImageLoaderConcurrency(Integer.parseInt(sharedPreferences.getString(key, "5"))); } else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) { setDebugLogToFile(sharedPreferences.getBoolean(key, false)); + } else if (Constants.PREFERENCES_KEY_ID3_TAGS.equals(key)) { + if (sharedPreferences.getBoolean(key, false)) showArtistPicture.setEnabled(true); + else showArtistPicture.setEnabled(false); } } @@ -427,6 +432,9 @@ public class SettingsFragment extends PreferenceFragment } else { debugLogToFile.setSummary(""); } + + if (Util.getShouldUseId3Tags(getActivity())) showArtistPicture.setEnabled(true); + else showArtistPicture.setEnabled(false); } private static void setImageLoaderConcurrency(int concurrency) { 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/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 1f2b1b08..8e5b0925 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -114,6 +114,7 @@ public final class Constants public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay"; public static final String PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"; public static final String PREFERENCES_KEY_ID3_TAGS = "useId3Tags"; + public static final String PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture"; public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss"; public static final String PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"; public static final String PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java index 74202148..d45c67b2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java @@ -13,23 +13,16 @@ public interface ImageLoader { void stopImageLoader(); - void loadAvatarImage( - View view, - String username, - boolean large, - int size, - boolean crossFade, - boolean highQuality - ); + void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade, + boolean highQuality); - void loadImage( - View view, - MusicDirectory.Entry entry, - boolean large, - int size, - boolean crossFade, - boolean highQuality - ); + void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, + boolean crossFade, boolean highQuality); + + void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, + boolean crossFade, boolean highQuality, int defaultResourceId); + + void cancel(String coverArt); Bitmap getImageBitmap(String username, int size); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java index 3684c6b8..e6370f39 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java @@ -38,6 +38,8 @@ import org.moire.ultrasonic.service.MusicServiceFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.SortedSet; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -165,25 +167,24 @@ public class LegacyImageLoader implements Runnable, ImageLoader { } @Override - public void loadImage( - View view, - MusicDirectory.Entry entry, - boolean large, - int size, - boolean crossFade, - boolean highQuality - ) { + public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, + boolean crossFade, boolean highQuality) { + loadImage(view, entry, large, size, crossFade, highQuality, -1); + } + + public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, + boolean crossFade, boolean highQuality, int defaultResourceId) { view.invalidate(); if (entry == null) { - setUnknownImage(view, large); + setUnknownImage(view, large, defaultResourceId); return; } String coverArt = entry.getCoverArt(); if (TextUtils.isEmpty(coverArt)) { - setUnknownImage(view, large); + setUnknownImage(view, large, defaultResourceId); return; } @@ -198,11 +199,21 @@ public class LegacyImageLoader implements Runnable, ImageLoader { return; } - setUnknownImage(view, large); + setUnknownImage(view, large, defaultResourceId); queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); } + public void cancel(String coverArt) { + for (Object taskObject : queue.toArray()) { + Task task = (Task)taskObject; + if ((task.entry.getCoverArt() != null) && (coverArt.compareTo(task.entry.getCoverArt()) == 0)) { + queue.remove(taskObject); + break; + } + } + } + private static String getKey(String coverArtId, int size) { return String.format("%s:%d", coverArtId, size); } @@ -330,13 +341,18 @@ public class LegacyImageLoader implements Runnable, ImageLoader { } private void setUnknownImage(View view, boolean large) { + setUnknownImage(view, large, -1); + } + + private void setUnknownImage(View view, boolean large, int resId) { + if (resId == -1) resId = R.drawable.unknown_album; if (large) { setImageBitmap(view, null, largeUnknownImage, false); } else { if (view instanceof TextView) { - ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); + ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(resId, 0, 0, 0); } else if (view instanceof ImageView) { - ((ImageView) view).setImageResource(R.drawable.unknown_album); + ((ImageView) view).setImageResource(resId); } } } @@ -381,14 +397,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader { private final boolean crossFade; private final boolean highQuality; - Task( - View view, - MusicDirectory.Entry entry, - int size, - boolean saveToFile, - boolean crossFade, - boolean highQuality - ) { + Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) { this.view = view; this.entry = entry; this.username = null; @@ -399,14 +408,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader { handler = new Handler(); } - Task( - View view, - String username, - int size, - boolean saveToFile, - boolean crossFade, - boolean highQuality - ) { + Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) { this.view = view; this.entry = null; this.username = username; @@ -421,9 +423,9 @@ public class LegacyImageLoader implements Runnable, ImageLoader { try { MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); final boolean isAvatar = this.username != null && this.entry == null; - final Bitmap bitmap = this.entry != null - ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) - : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); + final Bitmap bitmap = this.entry != null ? + musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : + musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); if (bitmap == null) { Timber.d("Found empty album art."); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 7a8c4281..75c96683 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -54,6 +54,7 @@ import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.SettingsActivity; +import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; @@ -1181,6 +1182,15 @@ public class Util return preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false); } + public static boolean getShouldShowArtistPicture(Context context) + { + SharedPreferences preferences = getPreferences(context); + boolean isOffline = ActiveServerProvider.Companion.isOffline(context); + boolean isId3Enabled = preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false); + boolean shouldShowArtistPicture = preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false); + return (!isOffline) && isId3Enabled && shouldShowArtistPicture; + } + public static int getChatRefreshInterval(Context context) { SharedPreferences preferences = getPreferences(context); 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..a12e6a43 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt @@ -0,0 +1,109 @@ +/* + 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 + +/** + * Provides ViewModel which contains the list of available Artists + */ +class ArtistListModel( + private val activeServerProvider: ActiveServerProvider, + private val context: Context +) : ViewModel() { + private val musicFolders: MutableLiveData> = MutableLiveData() + private val artists: MutableLiveData> = MutableLiveData() + + /** + * Retrieves the available Artists in a LiveData + */ + fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { + backgroundLoadFromServer(refresh, swipe) + return artists + } + + /** + * Retrieves the available Music Folders in a LiveData + */ + fun getMusicFolders(): LiveData> { + return musicFolders + } + + /** + * Refreshes the cached Artists from the server + */ + fun refresh(swipe: SwipeRefreshLayout) { + backgroundLoadFromServer(true, swipe) + } + + private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) { + viewModelScope.launch { + swipe.isRefreshing = true + 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..28105012 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt @@ -0,0 +1,194 @@ +/* + 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.ImageView +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter +import java.text.Collator +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.util.ImageLoader +import org.moire.ultrasonic.util.Util + +/** + * Creates a Row in a RecyclerView which contains the details of an 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, + private val imageLoader: ImageLoader +) : RecyclerView.Adapter(), SectionedAdapter { + + /** + * Sets the data to be displayed in the RecyclerView + */ + fun setData(data: List) { + artistList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name }) + notifyDataSetChanged() + } + + /** + * Sets the name of the folder to be displayed n the Header (first) row + */ + fun setFolderName(name: String) { + folderName = name + notifyDataSetChanged() + } + + /** + * Holds the view properties of an Artist row + */ + 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) + var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart) + var coverArtId: String? = null + } + + /** + * Holds the view properties of the Header row + */ + 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 onViewRecycled(holder: RecyclerView.ViewHolder) { + if ((holder is ArtistViewHolder) && (holder.coverArtId != null)) { + imageLoader.cancel(holder.coverArtId) + } + super.onViewRecycled(holder) + } + + 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) } + holder.coverArtId = artistList[listPosition].coverArt + + if (Util.getShouldShowArtistPicture(holder.coverArt.context)) { + holder.coverArt.visibility = View.VISIBLE + imageLoader.loadImage( + holder.coverArt, + MusicDirectory.Entry().apply { coverArt = holder.coverArtId }, + false, 0, false, true, R.drawable.ic_contact_picture + ) + } else { + holder.coverArt.visibility = View.GONE + } + } 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 + } + + override fun getSectionName(position: Int): String { + var listPosition = if (shouldShowHeader) position - 1 else position + + // Show the first artist's initial in the popup when the list is + // scrolled up to the "Select Folder" row + if (listPosition < 0) listPosition = 0 + + return getSectionFromName(artistList[listPosition].name ?: " ") + } + + 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..87a4c4c8 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt @@ -0,0 +1,215 @@ +/* + 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 + +/** + * Displays the available Artists in a list + */ +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() + .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) }, + imageLoader + ) + + 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..42ac9b6b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -4,9 +4,11 @@ package org.moire.ultrasonic.di import kotlin.math.abs import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext +import org.koin.android.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.moire.ultrasonic.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 +73,6 @@ val musicServiceModule = module { } single { SubsonicImageLoader(androidContext(), get()) } + + viewModel { ArtistListModel(get(), androidContext()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt index d7381164..cec72778 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -7,6 +7,7 @@ import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist fun APIArtist.toDomainEntity(): Artist = Artist( id = this@toDomainEntity.id, + coverArt = this@toDomainEntity.coverArt, name = this@toDomainEntity.name ) 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..39a72dcf --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt @@ -0,0 +1,85 @@ +/* + 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 + +/** + * Contains helper functions to handle the exceptions + * thrown during the communication with a Subsonic server + */ +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/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 90f33a17..6d0d2b99 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -26,18 +26,32 @@ class SubsonicImageLoaderProxy( size: Int, crossFade: Boolean, highQuality: Boolean + ) { + return loadImage(view, entry, large, size, crossFade, highQuality, -1) + } + + override fun loadImage( + view: View?, + entry: MusicDirectory.Entry?, + large: Boolean, + size: Int, + crossFade: Boolean, + highQuality: Boolean, + defaultResourceId: Int ) { val id = entry?.coverArt + val unknownImageId = + if (defaultResourceId == -1) R.drawable.unknown_album + else defaultResourceId if (id != null && view != null && view is ImageView ) { val request = ImageRequest.CoverArt( - id, - view, - placeHolderDrawableRes = R.drawable.unknown_album, - errorDrawableRes = R.drawable.unknown_album + id, view, + placeHolderDrawableRes = unknownImageId, + errorDrawableRes = unknownImageId ) subsonicImageLoader.load(request) } @@ -56,8 +70,7 @@ class SubsonicImageLoaderProxy( view is ImageView ) { val request = ImageRequest.Avatar( - username, - view, + username, view, placeHolderDrawableRes = R.drawable.ic_contact_picture, errorDrawableRes = R.drawable.ic_contact_picture ) 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..078a12c1 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/thumb.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ 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..26a4d7a1 100644 --- a/ultrasonic/src/main/res/layout/artist_list_item.xml +++ b/ultrasonic/src/main/res/layout/artist_list_item.xml @@ -1,11 +1,56 @@ - \ 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..455f8531 100644 --- a/ultrasonic/src/main/res/layout/select_artist.xml +++ b/ultrasonic/src/main/res/layout/select_artist.xml @@ -2,21 +2,33 @@ + 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"> Feltételezi, hogy a legfelső szintű mappa az előadó neve. Böngészés ID3 Tag használatával ID3 Tag módszer használata a fájlredszer alapú mód helyett. + Előadó képének megjelenítése + Az előadó listában megjeleníti a képeket, amennyiben elérhetőek Videó Videólejátszó Nézet frissítési gyakorisága diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 53b796ef..019cc0cd 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -316,6 +316,8 @@ Assume top-level folder is the name of the album artist Browse Using ID3 Tags Use ID3 tag methods instead of file system based methods + Show artist picture in artist list + Displays the artist picture in the artist list if available Video Video player View Refresh diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index 9334649d..65012f6d 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -35,6 +35,11 @@ center_vertical + + diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 0101ea77..2f451ad2 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -64,6 +64,11 @@ a:key="useId3Tags" a:summary="@string/settings.use_id3_summary" a:title="@string/settings.use_id3"/> +