diff options
Diffstat (limited to 'app/src/main/java/github/daneren2005')
114 files changed, 5491 insertions, 2844 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java index a58f169b..a1c5ceef 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java @@ -32,13 +32,15 @@ import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.AppCompatDelegate; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.KeyEvent; @@ -67,7 +69,9 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.AdminFragment; import github.daneren2005.dsub.fragments.SubsonicFragment; +import github.daneren2005.dsub.fragments.UserFragment; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.HeadphoneListenerService; import github.daneren2005.dsub.service.MusicService; @@ -76,10 +80,13 @@ import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.ThemeUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.UpdateView; import github.daneren2005.dsub.util.UserUtil; +import static android.Manifest.*; + public class SubsonicActivity extends AppCompatActivity implements OnItemSelectedListener { private static final String TAG = SubsonicActivity.class.getSimpleName(); private static ImageLoader IMAGE_LOADER; @@ -88,6 +95,8 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte protected static boolean actionbarColored; private static final int MENU_GROUP_SERVER = 10; private static final int MENU_ITEM_SERVER_BASE = 100; + public static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; + public static final int PERMISSIONS_REQUEST_LOCATION = 2; private final List<Runnable> afterServiceAvailable = new ArrayList<>(); private boolean drawerIdle = true; @@ -116,6 +125,10 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte boolean drawerOpen = false; SharedPreferences.OnSharedPreferenceChangeListener preferencesListener; + static { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); + } + @Override protected void onCreate(Bundle bundle) { UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE); @@ -150,6 +163,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte case Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED: setDrawerItemVisible(R.id.drawer_bookmarks, false); break; + case Constants.PREFERENCES_KEY_INTERNET_RADIO_ENABLED: + setDrawerItemVisible(R.id.drawer_internet_radio_stations, false); + break; case Constants.PREFERENCES_KEY_SHARED_ENABLED: setDrawerItemVisible(R.id.drawer_shares, false); break; @@ -164,6 +180,25 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte }; Util.getPreferences(this).registerOnSharedPreferenceChangeListener(preferencesListener); } + + if (ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{ permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + switch (requestCode) { + case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + } else { + Util.toast(this, R.string.permission_external_storage_failed); + finish(); + } + } + } } @Override @@ -189,40 +224,48 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void createCustomActionBarView() { - View customActionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null); - actionBarSpinner = (Spinner)customActionbar.findViewById(R.id.spinner); - if(Util.getThemeRes(this) == R.style.Theme_DSub_Light_No_Actionbar) { - actionBarSpinner.setBackgroundResource(R.drawable.abc_spinner_mtrl_am_alpha); + actionBarSpinner = (Spinner) getLayoutInflater().inflate(R.layout.actionbar_spinner, null); + if((this instanceof SubsonicFragmentActivity || this instanceof SettingsActivity) && (Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true) || ThemeUtil.getThemeRes(this) != R.style.Theme_DSub_Light_No_Color)) { + actionBarSpinner.setBackgroundDrawable(DrawableTint.getTintedDrawableFromColor(this, R.drawable.abc_spinner_mtrl_am_alpha, android.R.color.white)); } spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); actionBarSpinner.setOnItemSelectedListener(this); actionBarSpinner.setAdapter(spinnerAdapter); - getSupportActionBar().setCustomView(customActionbar); + getSupportActionBar().setCustomView(actionBarSpinner); } @Override - protected void onResume() { - super.onResume(); + protected void onStart() { + super.onStart(); Util.registerMediaButtonEventReceiver(this); // Make sure to update theme SharedPreferences prefs = Util.getPreferences(this); - if (theme != null && !theme.equals(Util.getTheme(this)) || fullScreen != prefs.getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false) || actionbarColored != prefs.getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + if (theme != null && !theme.equals(ThemeUtil.getTheme(this)) || fullScreen != prefs.getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false) || actionbarColored != prefs.getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { restart(); overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - DrawableTint.wipeTintCache(); + DrawableTint.clearCache(); + return; } - populateTabs(); getImageLoader().onUIVisible(); UpdateView.addActiveActivity(); } @Override - protected void onPause() { - super.onPause(); + protected void onResume() { + super.onResume(); + + // If this is in onStart is causes crashes when rotating screen in offline mode + // Actual root cause of error is `drawerItemSelected(newFragment);` in the offline mode branch of code + populateTabs(); + } + + @Override + protected void onStop() { + super.onStop(); UpdateView.removeActiveActivity(); } @@ -281,6 +324,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte case R.id.drawer_bookmarks: drawerItemSelected("Bookmark"); return true; + case R.id.drawer_internet_radio_stations: + drawerItemSelected("Internet Radio"); + return true; case R.id.drawer_shares: drawerItemSelected("Share"); return true; @@ -538,7 +584,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { int top = spinnerAdapter.getCount() - 1; if(position < top) { - for(int i = top; i > position; i--) { + for(int i = top; i > position && i >= 0; i--) { removeCurrent(); } } @@ -556,6 +602,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte SharedPreferences prefs = Util.getPreferences(this); boolean podcastsEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true); boolean bookmarksEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true) && !Util.isOffline(this) && ServerInfo.canBookmark(this); + boolean internetRadioEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_INTERNET_RADIO_ENABLED, true) && !Util.isOffline(this) && ServerInfo.canInternetRadio(this); boolean sharedEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SHARED_ENABLED, true) && !Util.isOffline(this); boolean chatEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_CHAT_ENABLED, true) && !Util.isOffline(this); boolean adminEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_ADMIN_ENABLED, true) && !Util.isOffline(this); @@ -585,6 +632,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte if(!bookmarksEnabled) { setDrawerItemVisible(R.id.drawer_bookmarks, false); } + if(!internetRadioEnabled) { + setDrawerItemVisible(R.id.drawer_internet_radio_stations, false); + } if(!sharedEnabled) { setDrawerItemVisible(R.id.drawer_shares, false); } @@ -752,6 +802,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte recreateSpinner(); } public void removeCurrent() { + // Don't try to remove current if there is no backstack to remove from + if(backStack.isEmpty()) { + return; + } + if(currentFragment != null) { currentFragment.setPrimaryFragment(false); } @@ -819,7 +874,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte removeCurrent(); } - currentFragment.invalidate(); + if(currentFragment instanceof UserFragment || currentFragment instanceof AdminFragment) { + restart(false); + } else { + currentFragment.invalidate(); + } populateTabs(); } @@ -853,6 +912,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte spinnerAdapter.notifyDataSetChanged(); actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1); if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setDisplayShowCustomEnabled(true); } @@ -862,6 +922,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } else if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(true); getSupportActionBar().setTitle(currentFragment.getTitle()); getSupportActionBar().setDisplayShowCustomEnabled(false); drawerToggle.setDrawerIndicatorEnabled(true); @@ -869,22 +930,31 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void restart() { - Intent intent = new Intent(this, ((Object) this).getClass()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + restart(true); + } + protected void restart(boolean resumePosition) { + Intent intent = new Intent(this, this.getClass()); intent.putExtras(getIntent()); - intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + if(resumePosition) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } else { + String fragmentType = Util.openToTab(this); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + intent.putExtra(Constants.FRAGMENT_POSITION, getDrawerItemId(fragmentType)); + } + finish(); Util.startActivityWithoutTransition(this, intent); } private void applyTheme() { - theme = Util.getTheme(this); + theme = ThemeUtil.getTheme(this); if(theme != null && theme.indexOf("fullscreen") != -1) { theme = theme.substring(0, theme.indexOf("_fullscreen")); - Util.setTheme(this, theme); + ThemeUtil.setTheme(this, theme); } - Util.applyTheme(this, theme); + ThemeUtil.applyTheme(this, theme); actionbarColored = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true); } private void applyFullscreen() { @@ -1044,6 +1114,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte UserUtil.seedCurrentUser(this); this.updateDrawerHeader(); + drawer.closeDrawers(); } private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) { @@ -1125,6 +1196,10 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } public int getDrawerItemId(String fragmentType) { + if(fragmentType == null) { + return R.id.drawer_home; + } + switch(fragmentType) { case "Home": return R.id.drawer_home; @@ -1136,6 +1211,8 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte return R.id.drawer_podcasts; case "Bookmark": return R.id.drawer_bookmarks; + case "Internet Radio": + return R.id.drawer_internet_radio_stations; case "Share": return R.id.drawer_shares; case "Chat": diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 87a67e80..803e6f72 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -28,8 +28,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.os.Bundle; -import android.os.Handler; import android.preference.PreferenceManager; +import android.provider.MediaStore; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; @@ -37,8 +37,6 @@ import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -48,9 +46,6 @@ import com.sothree.slidinguppanel.SlidingUpPanelLayout; import java.io.File; import java.util.Date; import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; @@ -66,6 +61,7 @@ import github.daneren2005.dsub.fragments.SearchFragment; import github.daneren2005.dsub.fragments.SelectArtistFragment; import github.daneren2005.dsub.fragments.SelectBookmarkFragment; import github.daneren2005.dsub.fragments.SelectDirectoryFragment; +import github.daneren2005.dsub.fragments.SelectInternetRadioStationFragment; import github.daneren2005.dsub.fragments.SelectPlaylistFragment; import github.daneren2005.dsub.fragments.SelectPodcastsFragment; import github.daneren2005.dsub.fragments.SelectShareFragment; @@ -76,6 +72,7 @@ import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.updates.Updater; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.UserUtil; @@ -93,6 +90,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private SlidingUpPanelLayout slideUpPanel; private SlidingUpPanelLayout.PanelSlideListener panelSlideListener; + private boolean isPanelClosing = false; private NowPlayingFragment nowPlayingFragment; private SubsonicFragment secondaryFragment; private Toolbar mainToolbar; @@ -106,6 +104,10 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private long lastBackPressTime = 0; private DownloadFile currentPlaying; private PlayerState currentState; + private ImageButton previousButton; + private ImageButton nextButton; + private ImageButton rewindButton; + private ImageButton fastforwardButton; @Override public void onCreate(Bundle savedInstanceState) { @@ -132,6 +134,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo stopService(new Intent(this, DownloadService.class)); finish(); getImageLoader().clearCache(); + DrawableTint.clearCache(); } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) { getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Download"); lastSelectedPosition = R.id.drawer_downloading; @@ -153,8 +156,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo if(item != null) { item.setChecked(true); } + } else { + lastSelectedPosition = getDrawerItemId(fragmentType); } + currentFragment = getNewFragment(fragmentType); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ID)) { + Bundle currentArguments = currentFragment.getArguments(); + if(currentArguments == null) { + currentArguments = new Bundle(); + } + currentArguments.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + currentFragment.setArguments(currentArguments); + } currentFragment.setPrimaryFragment(true); getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); @@ -187,24 +201,34 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void onPanelCollapsed(View panel) { - bottomBar.setVisibility(View.VISIBLE); - nowPlayingToolbar.setVisibility(View.GONE); - nowPlayingFragment.setPrimaryFragment(false); - setSupportActionBar(mainToolbar); - recreateSpinner(); + isPanelClosing = false; + if(bottomBar.getVisibility() == View.GONE) { + bottomBar.setVisibility(View.VISIBLE); + nowPlayingToolbar.setVisibility(View.GONE); + nowPlayingFragment.setPrimaryFragment(false); + setSupportActionBar(mainToolbar); + recreateSpinner(); + } } @Override public void onPanelExpanded(View panel) { + isPanelClosing = false; currentFragment.stopActionMode(); // Disable custom view before switching getSupportActionBar().setDisplayShowCustomEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(true); bottomBar.setVisibility(View.GONE); nowPlayingToolbar.setVisibility(View.VISIBLE); setSupportActionBar(nowPlayingToolbar); - nowPlayingFragment.setPrimaryFragment(true); + + if(secondaryFragment == null) { + nowPlayingFragment.setPrimaryFragment(true); + } else { + secondaryFragment.setPrimaryFragment(true); + } drawerToggle.setDrawerIndicatorEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -230,6 +254,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo openNowPlaying(); } }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); } bottomBar = findViewById(R.id.bottom_bar); @@ -248,7 +274,25 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo trans.commit(); } - ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous); + rewindButton = (ImageButton) findViewById(R.id.download_rewind); + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().rewind(); + return null; + } + }.execute(); + } + }); + + previousButton = (ImageButton) findViewById(R.id.download_previous); previousButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -276,6 +320,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo PlayerState state = getDownloadService().getPlayerState(); if(state == PlayerState.STARTED) { getDownloadService().pause(); + } else if(state == PlayerState.IDLE) { + getDownloadService().play(); } else { getDownloadService().start(); } @@ -286,7 +332,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } }); - ImageButton nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton = (ImageButton) findViewById(R.id.download_next); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -303,6 +349,24 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo }.execute(); } }); + + fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward); + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().fastForward(); + return null; + } + }.execute(); + } + }); } @Override @@ -330,17 +394,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo super.onNewIntent(intent); if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + if(currentFragment instanceof SearchFragment) { String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); - boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false); + String artist = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST); + String album = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM); + String title = intent.getStringExtra(MediaStore.EXTRA_MEDIA_TITLE); if (query != null) { - ((SearchFragment)currentFragment).search(query, autoplay); - } else { - if (requestsearch) { - onSearchRequested(); - } + ((SearchFragment)currentFragment).search(query, autoplay, artist, album, title); } getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY); } else { @@ -349,7 +415,14 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo SearchFragment fragment = new SearchFragment(); replaceFragment(fragment, fragment.getSupportTag()); } + } else if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, false)) { + if(slideUpPanel.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) { + openNowPlaying(); + } } else { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } setIntent(intent); } if(drawer != null) { @@ -402,6 +475,9 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putString(Constants.MAIN_NOW_PLAYING, nowPlayingFragment.getTag()); + if(secondaryFragment != null) { + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING_SECONDARY, secondaryFragment.getTag()); + } savedInstanceState.putInt(Constants.MAIN_SLIDE_PANEL_STATE, slideUpPanel.getPanelState().hashCode()); } @Override @@ -411,6 +487,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo String id = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING); FragmentManager fm = getSupportFragmentManager(); nowPlayingFragment = (NowPlayingFragment) fm.findFragmentByTag(id); + + String secondaryId = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING_SECONDARY); + if(secondaryId != null) { + secondaryFragment = (SubsonicFragment) fm.findFragmentByTag(secondaryId); + + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.hide(nowPlayingFragment); + trans.commit(); + } + if(drawerToggle != null && backStack.size() > 0) { drawerToggle.setDrawerIndicatorEnabled(false); } @@ -472,7 +561,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { - if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) { secondaryFragment = fragment; nowPlayingFragment.setPrimaryFragment(false); secondaryFragment.setPrimaryFragment(true); @@ -562,6 +651,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void closeNowPlaying() { slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; } private SubsonicFragment getNewFragment(String fragmentType) { @@ -575,6 +665,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo return new SelectPodcastsFragment(); } else if("Bookmark".equals(fragmentType)) { return new SelectBookmarkFragment(); + } else if("Internet Radio".equals(fragmentType)) { + return new SelectInternetRadioStationFragment(); } else if("Share".equals(fragmentType)) { return new SelectShareFragment(); } else if("Admin".equals(fragmentType)) { @@ -637,7 +729,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server"); editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org"); - editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "guest"); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "guest2"); editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); editor.commit(); @@ -828,14 +920,20 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } @Override - public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) { this.currentPlaying = currentPlaying; MusicDirectory.Entry song = null; if (currentPlaying != null) { song = currentPlaying.getSong(); trackView.setText(song.getTitle()); - artistView.setText(song.getArtist()); + + if(song.getArtist() != null) { + artistView.setVisibility(View.VISIBLE); + artistView.setText(song.getArtist()); + } else { + artistView.setVisibility(View.GONE); + } } else { trackView.setText(R.string.main_title); artistView.setText(R.string.main_artist); @@ -851,13 +949,40 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } getImageLoader().loadImage(coverArtView, song, false, height, false); } + + updateMediaButtons(shouldFastForward); + } + + private void updateMediaButtons(boolean shouldFastForward) { + DownloadService downloadService = getDownloadService(); + if(downloadService.isCurrentPlayingSingle()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } else { + if (shouldFastForward) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + } } @Override - public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) { + public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) { if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { - onSongChanged(currentPlaying, currentPlayingIndex); - onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward); + } else { + updateMediaButtons(shouldFastForward); } } @@ -875,7 +1000,21 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } @Override - public void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange) { + public void onMetadataUpdate(MusicDirectory.Entry song, int fieldChange) { + if(song != null && coverArtView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + // We need to update it immediately since it won't update if updater is not running for it + if(nowPlayingFragment != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) { + nowPlayingFragment.onMetadataUpdate(song, fieldChange); + } + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java index c0effe27..641b118f 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java @@ -55,6 +55,22 @@ public class VoiceQueryReceiverActivity extends Activity { if(!GMS_SEARCH_ACTION.equals(getIntent().getAction())) { intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); } + + String artist = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST); + if(artist != null) { + intent.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist); + } + + String album = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM); + if(album != null) { + intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album); + } + + String title = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_TITLE); + if(title != null) { + intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, title); + } + intent.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS)); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java index 5feaa482..5ed79e82 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java @@ -23,27 +23,32 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import java.io.Serializable; import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.ArtistView; import github.daneren2005.dsub.view.FastScroller; +import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; -public class ArtistAdapter extends SectionAdapter<Artist> implements FastScroller.BubbleTextGetter { +public class ArtistAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_SONG = 3; public static int VIEW_TYPE_ARTIST = 4; private List<MusicFolder> musicFolders; private OnMusicFolderChanged onMusicFolderChanged; - public ArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener listener) { + public ArtistAdapter(Context context, List<Serializable> artists, OnItemClickedListener listener) { this(context, artists, null, listener, null); } - public ArtistAdapter(Context context, List<Artist> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { + public ArtistAdapter(Context context, List<Serializable> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { super(context, artists); this.musicFolders = musicFolders; this.onItemClickedListener = onItemClickedListener; @@ -92,7 +97,7 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle return new UpdateView.UpdateViewHolder(header, false); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { TextView folderName = (TextView) holder.getView().findViewById(R.id.select_artist_folder_2); String musicFolderId = Util.getSelectedMusicFolderId(context); @@ -110,17 +115,35 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new ArtistView(context)); + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); } @Override - public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { - holder.getUpdateView().setObject(item); + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + Entry entry = (Entry) item; + songView.setObject(entry, checkable && !entry.isVideo()); + } } @Override - public int getItemViewType(Artist item) { - return VIEW_TYPE_ARTIST; + public int getItemViewType(Serializable item) { + if(item instanceof Artist) { + return VIEW_TYPE_ARTIST; + } else { + return VIEW_TYPE_SONG; + } } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java index d4613994..5b3dc289 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java @@ -16,13 +16,18 @@ package github.daneren2005.dsub.adapter; import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import java.util.List; +import github.daneren2005.dsub.R; import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.FastScroller; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -33,6 +38,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public DownloadFileAdapter(Context context, List<DownloadFile> entries, OnItemClickedListener onItemClickedListener) { super(context, entries); this.onItemClickedListener = onItemClickedListener; + this.checkable = true; } @Override @@ -43,7 +49,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements @Override public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) { SongView songView = (SongView) holder.getUpdateView(); - songView.setObject(item.getSong(), false); + songView.setObject(item.getSong(), Util.isBatchMode(context)); songView.setDownloadFile(item); } @@ -56,4 +62,21 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public String getTextToShowInBubble(int position) { return null; } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_nowplaying, menu); + } + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).getSong().isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java index 38931482..e75a5104 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java @@ -16,13 +16,12 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; @@ -45,8 +44,8 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { private ImageLoader imageLoader; private boolean largeAlbums; private boolean showArtist = false; + private boolean showAlbum = false; private boolean removeFromPlaylist = false; - private boolean removeStarred = true; private View header; public EntryGridAdapter(Context context, List<Entry> entries, ImageLoader imageLoader, boolean largeCell) { @@ -89,6 +88,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { albumView.setObject(entry, imageLoader); } else if(viewType == VIEW_TYPE_SONG) { SongView songView = (SongView) view; + songView.setShowAlbum(showAlbum); songView.setObject(entry, checkable && !entry.isVideo()); } } @@ -96,7 +96,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { } @@ -125,6 +125,10 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { this.showArtist = showArtist; } + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } + public void removeAt(int index) { sections.get(0).remove(index); if(header != null) { @@ -136,9 +140,6 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public void setRemoveFromPlaylist(boolean removeFromPlaylist) { this.removeFromPlaylist = removeFromPlaylist; } - public void setRemoveStarred(boolean removeStarred) { - this.removeStarred = removeStarred; - } @Override public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { @@ -151,8 +152,13 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { if(!removeFromPlaylist) { menu.removeItem(R.id.menu_remove_playlist); } - if(removeStarred) { - menu.removeItem(R.id.menu_unstar); + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } } } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java index 2c4f75dc..6c1c14da 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java @@ -26,6 +26,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.MainFragment; import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ImageLoader; @@ -88,6 +89,10 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { this.type = type; this.extra = extra; this.size = size; + + if(super.getItemCount() < size) { + allLoaded = true; + } } public void loadMore() { @@ -110,7 +115,7 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { appendCachedData(newData); loading = false; - if(newData.isEmpty()) { + if(newData.size() < size) { allLoaded = true; notifyDataSetChanged(); } @@ -126,6 +131,8 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { result = service.getAlbumList(type, extra, size, offset, false, context, null); } else if("genres".equals(type) || "genres-songs".equals(type)) { result = service.getSongsByGenre(extra, size, offset, context, null); + }else if(type.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(type, size, offset, context, null); } else { result = service.getAlbumList(type, size, offset, false, context, null); } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java new file mode 100644 index 00000000..6ebb34e3 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java @@ -0,0 +1,150 @@ +/* + 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 2015 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.view.BasicHeaderView; +import github.daneren2005.dsub.view.UpdateView; + +public abstract class ExpandableSectionAdapter<T> extends SectionAdapter<T> { + private static final String TAG = ExpandableSectionAdapter.class.getSimpleName(); + private static final int DEFAULT_VISIBLE = 4; + private static final int EXPAND_TOGGLE = R.attr.select_server; + private static final int COLLAPSE_TOGGLE = R.attr.select_tabs; + + protected List<Integer> sectionsDefaultVisible; + protected List<List<T>> sectionsExtras; + protected int expandToggleRes; + protected int collapseToggleRes; + + protected ExpandableSectionAdapter() {} + public ExpandableSectionAdapter(Context context, List<T> section) { + List<List<T>> sections = new ArrayList<>(); + sections.add(section); + + init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null)); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections) { + init(context, headers, sections, null); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections, List<Integer> sectionsDefaultVisible) { + init(context, headers, sections, sectionsDefaultVisible); + } + protected void init(Context context, List<String> headers, List<List<T>> fullSections, List<Integer> sectionsDefaultVisible) { + this.context = context; + this.headers = headers; + this.sectionsDefaultVisible = sectionsDefaultVisible; + if(sectionsDefaultVisible == null) { + sectionsDefaultVisible = new ArrayList<>(fullSections.size()); + for(int i = 0; i < fullSections.size(); i++) { + sectionsDefaultVisible.add(DEFAULT_VISIBLE); + } + } + + this.sections = new ArrayList<>(); + this.sectionsExtras = new ArrayList<>(); + int i = 0; + for(List<T> fullSection: fullSections) { + List<T> visibleSection = new ArrayList<>(); + + Integer defaultVisible = sectionsDefaultVisible.get(i); + if(defaultVisible == null || defaultVisible >= fullSection.size()) { + visibleSection.addAll(fullSection); + this.sectionsExtras.add(null); + } else { + visibleSection.addAll(fullSection.subList(0, defaultVisible)); + this.sectionsExtras.add(fullSection.subList(defaultVisible, fullSection.size())); + } + this.sections.add(visibleSection); + + i++; + } + + expandToggleRes = DrawableTint.getDrawableRes(context, EXPAND_TOGGLE); + collapseToggleRes = DrawableTint.getDrawableRes(context, COLLAPSE_TOGGLE); + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.expandable_header)); + } + + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, final int sectionIndex) { + UpdateView view = holder.getUpdateView(); + ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); + + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + if(sectionExtras != null && !sectionExtras.isEmpty()) { + toggleSelectionView.setVisibility(View.VISIBLE); + toggleSelectionView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + // Update icon + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = COLLAPSE_TOGGLE; + + // Update how many are displayed + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + visibleSelection.addAll(sectionExtras); + notifyItemRangeInserted(lastIndex, sectionExtras.size()); + } else { + selectToggleAttr = EXPAND_TOGGLE; + + // Update how many are displayed + visibleSelection.removeAll(sectionExtras); + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + notifyItemRangeRemoved(lastIndex, sectionExtras.size()); + } + + ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } + }); + + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = EXPAND_TOGGLE; + } else { + selectToggleAttr = COLLAPSE_TOGGLE; + } + + toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } else { + toggleSelectionView.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(header); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java new file mode 100644 index 00000000..9d47d70c --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java @@ -0,0 +1,56 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.daneren2005.dsub.domain.InternetRadioStation; +import github.daneren2005.dsub.view.FastScroller; +import github.daneren2005.dsub.view.InternetRadioStationView; +import github.daneren2005.dsub.view.UpdateView; + +public class InternetRadioStationAdapter extends SectionAdapter<InternetRadioStation> implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_INTERNET_RADIO_STATION = 1; + + public InternetRadioStationAdapter(Context context, List<InternetRadioStation> stations, OnItemClickedListener listener) { + super(context, stations); + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new InternetRadioStationView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, InternetRadioStation station, int viewType) { + holder.getUpdateView().setObject(station); + holder.setItem(station); + } + + @Override + public int getItemViewType(InternetRadioStation station) { + return VIEW_TYPE_INTERNET_RADIO_STATION; + } + + @Override + public String getTextToShowInBubble(int position) { + InternetRadioStation item = getItemForPosition(position); + return item.getTitle(); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java index 8f1f1c38..dd70aa99 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java @@ -77,7 +77,7 @@ public class MainAdapter extends SectionAdapter<Integer> { return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header)); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox); @@ -100,6 +100,9 @@ public class MainAdapter extends SectionAdapter<Integer> { } else if("videos".equals(header)) { display = context.getResources().getString(R.string.main_videos); checkBox.setVisibility(View.GONE); + } else if("songs".equals(header)) { + display = context.getResources().getString(R.string.search_songs); + checkBox.setVisibility(View.GONE); } else { display = header; checkBox.setVisibility(View.GONE); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java index a95abeda..f843a722 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java @@ -34,21 +34,17 @@ import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; import java.io.Serializable; +import java.util.Arrays; import java.util.List; -public class PodcastChannelAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { +public class PodcastChannelAdapter extends ExpandableSectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { public static final int VIEW_TYPE_PODCAST_LEGACY = 1; public static final int VIEW_TYPE_PODCAST_LINE = 2; public static final int VIEW_TYPE_PODCAST_CELL = 3; public static final int VIEW_TYPE_PODCAST_EPISODE = 4; - public static final String EPISODE_HEADER = "episodes"; - public static final String CHANNEL_HEADER = "channels"; - private ImageLoader imageLoader; private boolean largeCell; - private int selectToggleAttr = R.attr.select_server; - private List<Serializable> extraEpisodes; public PodcastChannelAdapter(Context context, List<Serializable> podcasts, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { super(context, podcasts); @@ -56,9 +52,8 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen this.onItemClickedListener = listener; this.largeCell = largeCell; } - public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, List<Serializable> extraEpisodes, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { - super(context, headers, sections); - this.extraEpisodes = extraEpisodes; + public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { + super(context, headers, sections, Arrays.asList(3, null)); this.imageLoader = imageLoader; this.onItemClickedListener = listener; this.largeCell = largeCell; @@ -126,58 +121,6 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen } menu.removeItem(R.id.menu_remove_playlist); - menu.removeItem(R.id.menu_unstar); - } - - @Override - public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { - return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.newest_episode_header)); - } - - @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { - UpdateView view = holder.getUpdateView(); - ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); - - String display; - if(EPISODE_HEADER.equals(header)) { - display = context.getResources().getString(R.string.main_albums_newest); - - if(extraEpisodes != null && !extraEpisodes.isEmpty()) { - toggleSelectionView.setVisibility(View.VISIBLE); - toggleSelectionView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Update icon - if (selectToggleAttr == R.attr.select_server) { - selectToggleAttr = R.attr.select_tabs; - - // Update how many are displayed - sections.get(0).addAll(extraEpisodes); - notifyItemRangeInserted(4, extraEpisodes.size()); - } else { - selectToggleAttr = R.attr.select_server; - - // Update how many are displayed - sections.get(0).removeAll(extraEpisodes); - notifyItemRangeRemoved(4, extraEpisodes.size()); - } - - ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - - } - }); - toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - } else { - toggleSelectionView.setVisibility(View.GONE); - } - } else { - display = context.getResources().getString(R.string.select_podcasts_channels); - toggleSelectionView.setVisibility(View.GONE); - } - - if(view != null) { - view.setObject(display); - } + menu.removeItem(R.id.menu_star); } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java index 66f2db21..69e5d56d 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java @@ -19,7 +19,9 @@ import android.content.Context; import android.content.res.Resources; import android.view.Menu; import android.view.MenuInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import java.io.Serializable; import java.util.ArrayList; @@ -28,10 +30,12 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.AlbumView; import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -40,32 +44,39 @@ import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_C import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE; import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_SONG; -public class SearchAdapter extends SectionAdapter<Serializable> { - private SearchResult searchResult; +public class SearchAdapter extends ExpandableSectionAdapter<Serializable> { private ImageLoader imageLoader; private boolean largeAlbums; + private static final int MAX_ARTISTS = 10; + private static final int MAX_ALBUMS = 4; + private static final int MAX_SONGS = 10; + public SearchAdapter(Context context, SearchResult searchResult, ImageLoader imageLoader, boolean largeAlbums, OnItemClickedListener listener) { - this.context = context; - this.searchResult = searchResult; this.imageLoader = imageLoader; this.largeAlbums = largeAlbums; - this.sections = new ArrayList<>(); - this.headers = new ArrayList<>(); + List<List<Serializable>> sections = new ArrayList<>(); + List<String> headers = new ArrayList<>(); + List<Integer> defaultVisible = new ArrayList<>(); Resources res = context.getResources(); if(!searchResult.getArtists().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); - this.headers.add(res.getString(R.string.search_artists)); + sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); + headers.add(res.getString(R.string.search_artists)); + defaultVisible.add(MAX_ARTISTS); } if(!searchResult.getAlbums().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); - this.headers.add(res.getString(R.string.search_albums)); + sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); + headers.add(res.getString(R.string.search_albums)); + defaultVisible.add(MAX_ALBUMS); } if(!searchResult.getSongs().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); - this.headers.add(res.getString(R.string.search_songs)); + sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); + headers.add(res.getString(R.string.search_songs)); + defaultVisible.add(MAX_SONGS); } + init(context, headers, sections, defaultVisible); + this.onItemClickedListener = listener; checkable = true; } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java index d5f9a6ea..33bbb384 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java @@ -194,7 +194,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH for(List<T> section: sections) { boolean validHeader = headers.get(subHeader) != null; if(position == subPosition && validHeader) { - onBindHeaderHolder(holder, headers.get(subHeader)); + onBindHeaderHolder(holder, headers.get(subHeader), subHeader); return; } @@ -289,7 +289,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(new BasicHeaderView(context)); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); if(view != null) { view.setObject(header); @@ -414,14 +414,15 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { currentActionMode = mode; - onCreateActionModeMenu(menu, mode.getMenuInflater()); - MenuUtil.hideMenuItems(context, menu, updateView); T item = holder.getItem(); selected.add(item); selectedViews.add(updateView); setChecked(updateView, true); + onCreateActionModeMenu(menu, mode.getMenuInflater()); + MenuUtil.hideMenuItems(context, menu, updateView); + mode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { TypedValue typedValue = new TypedValue(); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java index 1cb9c34e..4e75a2f7 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java @@ -16,18 +16,20 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.User; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.UserUtil; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.RecyclingImageView; import github.daneren2005.dsub.view.SettingView; import github.daneren2005.dsub.view.UpdateView; @@ -37,56 +39,81 @@ import static github.daneren2005.dsub.domain.User.Setting; public class SettingsAdapter extends SectionAdapter<Setting> { private static final String TAG = SettingsAdapter.class.getSimpleName(); public final int VIEW_TYPE_SETTING = 1; + public final int VIEW_TYPE_SETTING_HEADER = 2; private final User user; private final boolean editable; private final ImageLoader imageLoader; - public SettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { - super(context, user.getSettings(), imageLoader != null); + public SettingsAdapter(Context context, User user, List<String> headers, List<List<User.Setting>> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { + super(context, headers, settingSections, imageLoader != null); this.user = user; this.imageLoader = imageLoader; this.editable = editable; this.onItemClickedListener = onItemClickedListener; - List<Setting> settings = sections.get(0); - for(Setting setting: settings) { - if(setting.getValue()) { - addSelected(setting); + for(List<Setting> settings: sections) { + for (Setting setting : settings) { + if (setting.getValue()) { + addSelected(setting); + } } } } + @Override + public int getItemViewType(int position) { + int viewType = super.getItemViewType(position); + if(viewType == SectionAdapter.VIEW_TYPE_HEADER) { + if(position == 0 && imageLoader != null) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_SETTING_HEADER; + } + } else { + return viewType; + } + } + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { View header = LayoutInflater.from(context).inflate(R.layout.user_header, parent, false); return new UpdateView.UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) { View header = holder.getView(); RecyclingImageView coverArtView = (RecyclingImageView) header.findViewById(R.id.user_avatar); - imageLoader.loadAvatar(context, coverArtView, user.getUsername()); - coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { - @Override - public void onInvalidated(RecyclingImageView imageView) { - imageLoader.loadAvatar(context, imageView, user.getUsername()); + if(coverArtView != null) { + imageLoader.loadAvatar(context, coverArtView, user.getUsername()); + coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { + @Override + public void onInvalidated(RecyclingImageView imageView) { + imageLoader.loadAvatar(context, imageView, user.getUsername()); + } + }); + + TextView usernameView = (TextView) header.findViewById(R.id.user_username); + usernameView.setText(user.getUsername()); + + final TextView emailView = (TextView) header.findViewById(R.id.user_email); + if (user.getEmail() != null) { + emailView.setText(user.getEmail()); + } else { + emailView.setVisibility(View.GONE); } - }); - - TextView usernameView = (TextView) header.findViewById(R.id.user_username); - usernameView.setText(user.getUsername()); - - final TextView emailView = (TextView) header.findViewById(R.id.user_email); - if(user.getEmail() != null) { - emailView.setText(user.getEmail()); } else { - emailView.setVisibility(View.GONE); + TextView nameView = (TextView) header.findViewById(R.id.item_name); + nameView.setText(description); } } @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new SettingView(context)); + if(viewType == VIEW_TYPE_SETTING_HEADER) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context)); + } else { + return new UpdateView.UpdateViewHolder(new SettingView(context)); + } } @Override @@ -105,4 +132,21 @@ public class SettingsAdapter extends SectionAdapter<Setting> { updateView.setChecked(checked); } } + + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener<Setting> onItemClickedListener) { + return getSettingsAdapter(context, user, imageLoader, UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), onItemClickedListener); + } + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener<Setting> onItemClickedListener) { + List<String> headers = new ArrayList<>(); + List<List<User.Setting>> settingsSections = new ArrayList<>(); + headers.add(context.getResources().getString(R.string.admin_permissions)); + settingsSections.add(user.getSettings()); + + if(user.getMusicFolderSettings() != null) { + headers.add(context.getResources().getString(R.string.admin_musicFolders)); + settingsSections.add(user.getMusicFolderSettings()); + } + + return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener); + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java new file mode 100644 index 00000000..2234d4cd --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java @@ -0,0 +1,53 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.UpdateView; + +public class SimilarArtistAdapter extends SectionAdapter<Artist> { + public static int VIEW_TYPE_ARTIST = 4; + + public SimilarArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener onItemClickedListener) { + super(context, artists); + this.onItemClickedListener = onItemClickedListener; + } + public SimilarArtistAdapter(Context context, List<String> headers, List<List<Artist>> sections, OnItemClickedListener onItemClickedListener) { + super(context, headers, sections); + this.onItemClickedListener = onItemClickedListener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new ArtistView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(Artist item) { + return VIEW_TYPE_ARTIST; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java index 56e8f92e..ff4d86ce 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java @@ -21,15 +21,19 @@ package github.daneren2005.dsub.domain; import android.util.Log; import java.io.Serializable; +import java.text.Collator; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Locale; /** * @author Sindre Mehus */ public class Artist implements Serializable { private static final String TAG = Artist.class.getSimpleName(); + public static final String ROOT_ID = "-1"; + public static final String MISSING_ID = "-2"; private String id; private String name; @@ -38,6 +42,14 @@ public class Artist implements Serializable { private Integer rating; private int closeness; + public Artist() { + + } + public Artist(String id, String name) { + this.id = id; + this.name = name; + } + public String getId() { return id; } @@ -109,30 +121,18 @@ public class Artist implements Serializable { public static class ArtistComparator implements Comparator<Artist> { private String[] ignoredArticles; + private Collator collator; public ArtistComparator(String[] ignoredArticles) { this.ignoredArticles = ignoredArticles; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Artist lhsArtist, Artist rhsArtist) { - if("root".equals(lhsArtist.getId())) { - return 1; - } else if("root".equals(rhsArtist.getId())) { - return -1; - } - String lhs = lhsArtist.getName().toLowerCase(); String rhs = rhsArtist.getName().toLowerCase(); - char lhs1 = lhs.charAt(0); - char rhs1 = rhs.charAt(0); - - if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { - return 1; - } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { - return -1; - } - for (String article : ignoredArticles) { int index = lhs.indexOf(article.toLowerCase() + " "); if (index == 0) { @@ -144,7 +144,7 @@ public class Artist implements Serializable { } } - return lhs.compareTo(rhs); + return collator.compare(lhs, rhs); } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java index e15ccf9f..05e686ca 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java @@ -52,13 +52,6 @@ public class Indexes implements Serializable { this.shortcuts = shortcuts; this.artists = artists; this.entries = entries; - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } } public long getLastModified() { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java b/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java new file mode 100644 index 00000000..47d79b99 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java @@ -0,0 +1,43 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.domain; + +public class InternetRadioStation extends MusicDirectory.Entry { + private String streamUrl; + private String homePageUrl; + + public InternetRadioStation() {} + + public String getStreamUrl() { + return streamUrl; + } + + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; + } + + public String getHomePageUrl() { + return homePageUrl; + } + + public void setHomePageUrl(String homePageUrl) { + this.homePageUrl = homePageUrl; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java index 3c022cea..5f7b2412 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java @@ -24,6 +24,8 @@ import android.content.SharedPreferences; import android.media.MediaMetadataRetriever; import android.os.Build; import android.util.Log; + +import java.text.Collator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -31,8 +33,11 @@ import java.io.File; import java.io.Serializable; import java.util.Collections; import java.util.Comparator; +import java.util.Locale; +import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.Util; /** @@ -135,13 +140,14 @@ public class MusicDirectory implements Serializable { EntryComparator.sort(children, byYear); } - public synchronized void updateMetadata(MusicDirectory refreshedDirectory) { + public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) { + boolean metadataUpdated = false; Iterator<Entry> it = children.iterator(); while(it.hasNext()) { Entry entry = it.next(); int index = refreshedDirectory.children.indexOf(entry); if(index != -1) { - Entry refreshed = refreshedDirectory.children.get(index); + final Entry refreshed = refreshedDirectory.children.get(index); entry.setTitle(refreshed.getTitle()); entry.setAlbum(refreshed.getAlbum()); @@ -155,8 +161,36 @@ public class MusicDirectory implements Serializable { entry.setStarred(refreshed.isStarred()); entry.setRating(refreshed.getRating()); entry.setType(refreshed.getType()); + if(!Util.equals(entry.getCoverArt(), refreshed.getCoverArt())) { + metadataUpdated = true; + entry.setCoverArt(refreshed.getCoverArt()); + } + + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setTitle(refreshed.getTitle()); + found.setAlbum(refreshed.getAlbum()); + found.setArtist(refreshed.getArtist()); + found.setTrack(refreshed.getTrack()); + found.setYear(refreshed.getYear()); + found.setGenre(refreshed.getGenre()); + found.setTranscodedContentType(refreshed.getTranscodedContentType()); + found.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + found.setDiscNumber(refreshed.getDiscNumber()); + found.setStarred(refreshed.isStarred()); + found.setRating(refreshed.getRating()); + found.setType(refreshed.getType()); + if(!Util.equals(found.getCoverArt(), refreshed.getCoverArt())) { + found.setCoverArt(refreshed.getCoverArt()); + metadataUpdate = DownloadService.METADATA_UPDATED_COVER_ART; + } + } + }.execute(); } } + + return metadataUpdated; } public synchronized boolean updateEntriesList(Context context, int instance, MusicDirectory refreshedDirectory) { boolean changed = false; @@ -202,6 +236,7 @@ public class MusicDirectory implements Serializable { private String album; private String artist; private Integer track; + private Integer customOrder; private Integer year; private String genre; private String contentType; @@ -275,6 +310,10 @@ public class MusicDirectory implements Serializable { public void rebaseTitleOffPath() { try { String filename = getPath(); + if(filename == null) { + return; + } + int index = filename.lastIndexOf('/'); if (index != -1) { filename = filename.substring(index + 1); @@ -386,6 +425,13 @@ public class MusicDirectory implements Serializable { this.track = track; } + public Integer getCustomOrder() { + return customOrder; + } + public void setCustomOrder(Integer customOrder) { + this.customOrder = customOrder; + } + public Integer getYear() { return year; } @@ -586,9 +632,12 @@ public class MusicDirectory implements Serializable { public static class EntryComparator implements Comparator<Entry> { private boolean byYear; + private Collator collator; public EntryComparator(boolean byYear) { this.byYear = byYear; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Entry lhs, Entry rhs) { @@ -608,8 +657,8 @@ public class MusicDirectory implements Serializable { return 1; } } - - return lhs.getAlbumDisplay().compareToIgnoreCase(rhs.getAlbumDisplay()); + + return collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay()); } Integer lhsDisc = lhs.getDiscNumber(); @@ -633,7 +682,7 @@ public class MusicDirectory implements Serializable { return 1; } - return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + return collator.compare(lhs.getTitle(), rhs.getTitle()); } public static void sort(List<Entry> entries) { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java index 99e86e23..37f76249 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java @@ -18,7 +18,12 @@ */ package github.daneren2005.dsub.domain; +import android.util.Log; + import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; /** * Represents a top level directory in which music or other media is stored. @@ -27,23 +32,49 @@ import java.io.Serializable; * @version $Id$ */ public class MusicFolder implements Serializable { - - private String id; - private String name; + private static final String TAG = MusicFolder.class.getSimpleName(); + private String id; + private String name; + private boolean enabled; public MusicFolder() { } - public MusicFolder(String id, String name) { - this.id = id; - this.name = name; - } + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean getEnabled() { + return enabled; + } - public String getId() { - return id; - } + public static class MusicFolderComparator implements Comparator<MusicFolder> { + public int compare(MusicFolder lhsMusicFolder, MusicFolder rhsMusicFolder) { + if(lhsMusicFolder == rhsMusicFolder || lhsMusicFolder.getName().equals(rhsMusicFolder.getName())) { + return 0; + } else { + return lhsMusicFolder.getName().compareToIgnoreCase(rhsMusicFolder.getName()); + } + } + } - public String getName() { - return name; - } + public static void sort(List<MusicFolder> musicFolders) { + try { + Collections.sort(musicFolders, new MusicFolderComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort music folders", e); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java index 20d46aa0..ed2400ef 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java @@ -18,6 +18,8 @@ */ package github.daneren2005.dsub.domain; +import java.util.regex.Pattern; + /** * The criteria for a music search. * @@ -25,31 +27,67 @@ package github.daneren2005.dsub.domain; */ public class SearchCritera { - private final String query; - private final int artistCount; - private final int albumCount; - private final int songCount; - - public SearchCritera(String query, int artistCount, int albumCount, int songCount) { - this.query = query; - this.artistCount = artistCount; - this.albumCount = albumCount; - this.songCount = songCount; - } - - public String getQuery() { - return query; - } - - public int getArtistCount() { - return artistCount; - } - - public int getAlbumCount() { - return albumCount; - } - - public int getSongCount() { - return songCount; - } -}
\ No newline at end of file + private final String query; + private final int artistCount; + private final int albumCount; + private final int songCount; + private Pattern pattern; + + public SearchCritera(String query, int artistCount, int albumCount, int songCount) { + this.query = query; + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + } + + public String getQuery() { + return query; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } + + /** + * Returns and caches a pattern instance that can be used to check if a + * string matches the query. + */ + public Pattern getPattern() { + + // If the pattern wasn't already cached, create a new regular expression + // from the search string : + // * Surround the search string with ".*" (match anything) + // * Replace spaces and wildcard '*' characters with ".*" + // * All other characters are properly quoted + if (this.pattern == null) { + String regex = ".*"; + String currentPart = ""; + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + if (c == '*' || c == ' ') { + regex += Pattern.quote(currentPart); + regex += ".*"; + currentPart = ""; + } else { + currentPart += c; + } + } + if (currentPart.length() > 0) { + regex += Pattern.quote(currentPart); + } + + regex += ".*"; + this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + return this.pattern; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java index 73037c4a..5852210e 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java @@ -24,6 +24,7 @@ import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.Util; @@ -35,6 +36,7 @@ import github.daneren2005.dsub.util.Util; public class ServerInfo implements Serializable { public static final int TYPE_SUBSONIC = 1; public static final int TYPE_MADSONIC = 2; + public static final int TYPE_AMPACHE = 3; private static final Map<Integer, ServerInfo> SERVERS = new ConcurrentHashMap<Integer, ServerInfo>(); private boolean isLicenseValid; @@ -189,13 +191,20 @@ public class ServerInfo implements Serializable { public static boolean isMadsonic6(Context context, int instance) { return getServerType(context, instance) == TYPE_MADSONIC && checkServerVersion(context, "2.0", instance); } + + public static boolean isAmpache(Context context) { + return isAmpache(context, Util.getActiveServer(context)); + } + public static boolean isAmpache(Context context, int instance) { + return getServerType(context, instance) == TYPE_AMPACHE; + } private static String getCacheName(Context context, int instance) { return "server-" + Util.getRestUrl(context, null, instance, false).hashCode() + ".ser"; } public static boolean hasArtistInfo(Context context) { - if(isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { + if(!isMadsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { return true; } else if(isMadsonic(context)) { return checkServerVersion(context, "2.0"); @@ -207,6 +216,9 @@ public class ServerInfo implements Serializable { public static boolean canBookmark(Context context) { return checkServerVersion(context, "1.9"); } + public static boolean canInternetRadio(Context context) { + return checkServerVersion(context, "1.9"); + } public static boolean canSavePlayQueue(Context context) { return ServerInfo.checkServerVersion(context, "1.12") && (!ServerInfo.isMadsonic(context) || checkServerVersion(context, "2.0")); @@ -223,8 +235,15 @@ public class ServerInfo implements Serializable { return canUseToken(context, Util.getActiveServer(context)); } public static boolean canUseToken(Context context, int instance) { - return false; /*isStockSubsonic(context, instance) && checkServerVersion(context, "1.13", instance) || - isMadsonic(context, instance) && checkServerVersion(context, "2.0", instance);*/ + if(isStockSubsonic(context, instance) && checkServerVersion(context, "1.14", instance)) { + if(Util.getBlockTokenUse(context, instance)) { + return false; + } else { + return true; + } + } else { + return false; + } } public static boolean hasSimilarArtists(Context context) { return !ServerInfo.isMadsonic(context) || ServerInfo.checkServerVersion(context, "2.0"); @@ -232,4 +251,9 @@ public class ServerInfo implements Serializable { public static boolean hasNewestPodcastEpisodes(Context context) { return ServerInfo.checkServerVersion(context, "1.13"); } + + public static boolean canRescanServer(Context context) { + return ServerInfo.isMadsonic(context) || + (ServerInfo.isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.15")); + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/User.java b/app/src/main/java/github/daneren2005/dsub/domain/User.java index 797a1271..5307828a 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/User.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/User.java @@ -15,6 +15,8 @@ package github.daneren2005.dsub.domain; +import android.util.Pair; + import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -31,8 +33,9 @@ public class User implements Serializable { public static final String STREAM = "streamRole"; public static final String JUKEBOX = "jukeboxRole"; public static final String SHARE = "shareRole"; + public static final String VIDEO_CONVERSION = "videoConversionRole"; public static final String LASTFM = "lastFMRole"; - public static final List<String> ROLES = new ArrayList<String>(); + public static final List<String> ROLES = new ArrayList<>(); static { ROLES.add(ADMIN); @@ -45,6 +48,7 @@ public class User implements Serializable { ROLES.add(PODCAST); ROLES.add(JUKEBOX); ROLES.add(SHARE); + ROLES.add(VIDEO_CONVERSION); } private String username; @@ -52,6 +56,7 @@ public class User implements Serializable { private String email; private List<Setting> settings = new ArrayList<Setting>(); + private List<Setting> musicFolders; public User() { @@ -92,9 +97,27 @@ public class User implements Serializable { settings.add(new Setting(name, value)); } + public void addMusicFolder(MusicFolder musicFolder) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolder.getId(), musicFolder.getName(), false)); + } + public void addMusicFolder(MusicFolderSetting musicFolderSetting, boolean defaultValue) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolderSetting.getName(), musicFolderSetting.getLabel(), defaultValue)); + } + public List<Setting> getMusicFolderSettings() { + return musicFolders; + } + public static class Setting implements Serializable { - String name; - Boolean value; + private String name; + private Boolean value; public Setting() { @@ -114,4 +137,20 @@ public class User implements Serializable { this.value = value; } } + + public static class MusicFolderSetting extends Setting { + private String label; + + public MusicFolderSetting() { + + } + public MusicFolderSetting(String name, String label, Boolean value) { + super(name, value); + this.label = label; + } + + public String getLabel() { + return label; + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Version.java b/app/src/main/java/github/daneren2005/dsub/domain/Version.java index 97246ecf..9df0dbb4 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Version.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Version.java @@ -27,41 +27,41 @@ import java.io.Serializable; * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ */ public class Version implements Comparable<Version>, Serializable { - private int major; - private int minor; - private int beta; - private int bugfix; + private int major; + private int minor; + private int beta; + private int bugfix; public Version() { // For Kryo } - /** - * Creates a new version instance by parsing the given string. - * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". - */ - public Version(String version) { - String[] s = version.split("\\."); - major = Integer.valueOf(s[0]); - minor = Integer.valueOf(s[1]); - - if (s.length > 2) { - if (s[2].contains("beta")) { - beta = Integer.valueOf(s[2].replace("beta", "")); - } else { - bugfix = Integer.valueOf(s[2]); - } - } - } - - public int getMajor() { - return major; - } - - public int getMinor() { - return minor; - } - + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + public String getVersion() { switch(major) { case 1: @@ -90,96 +90,98 @@ public class Version implements Comparable<Version>, Serializable { return "4.9"; case 11: return "5.1"; - case 12: - return "5.2"; - case 13: - return "5.3"; + case 12: + return "5.2"; + case 13: + return "5.3"; + case 14: + return "6.0"; } } return ""; } - /** - * Return whether this object is equal to another. - * @param o Object to compare to. - * @return Whether this object is equals to another. - */ - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final Version version = (Version) o; - - if (beta != version.beta) return false; - if (bugfix != version.bugfix) return false; - if (major != version.major) return false; - return minor == version.minor; - } - - /** - * Returns a hash code for this object. - * @return A hash code for this object. - */ - public int hashCode() { - int result; - result = major; - result = 29 * result + minor; - result = 29 * result + beta; - result = 29 * result + bugfix; - return result; - } - - /** - * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". - * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". - */ - public String toString() { - StringBuffer buf = new StringBuffer(); - buf.append(major).append('.').append(minor); - if (beta != 0) { - buf.append(".beta").append(beta); - } else if (bugfix != 0) { - buf.append('.').append(bugfix); - } - - return buf.toString(); - } - - /** - * Compares this object with the specified object for order. - * @param version The object to compare to. - * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or - * greater than the specified object. - */ - @Override - public int compareTo(Version version) { - if (major < version.major) { - return -1; - } else if (major > version.major) { - return 1; - } - - if (minor < version.minor) { - return -1; - } else if (minor > version.minor) { - return 1; - } - - if (bugfix < version.bugfix) { - return -1; - } else if (bugfix > version.bugfix) { - return 1; - } - - int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; - int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; - - if (thisBeta < otherBeta) { - return -1; - } else if (thisBeta > otherBeta) { - return 1; - } - - return 0; - } + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + @Override + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java index 630acf2c..552712f7 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java @@ -47,7 +47,7 @@ public class AdminFragment extends SelectRecyclerFragment<User> { switch (item.getItemId()) { case R.id.menu_add_user: - UserUtil.addNewUser(context, this); + UserUtil.addNewUser(context, this, (objects.size() > 0) ? objects.get(0) : null); break; } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java index febf22de..3208ffb7 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java @@ -135,8 +135,8 @@ public class ChatFragment extends SubsonicFragment { } @Override - public void onResume() { - super.onResume(); + public void onStart() { + super.onStart(); final Handler handler = new Handler(); Runnable runnable = new Runnable() { @@ -164,8 +164,8 @@ public class ChatFragment extends SubsonicFragment { } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); if(executorService != null) { executorService.shutdown(); executorService = null; diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java index 7594a99e..9e8f8c5b 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java @@ -65,8 +65,8 @@ public class DownloadFragment extends SelectRecyclerFragment<DownloadFile> imple } @Override - public void onResume() { - super.onResume(); + public void onStart() { + super.onStart(); final Handler handler = new Handler(); Runnable runnable = new Runnable() { @@ -86,8 +86,8 @@ public class DownloadFragment extends SelectRecyclerFragment<DownloadFile> imple } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); executorService.shutdown(); } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java index 9ee98cb4..948c59cf 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java @@ -110,8 +110,8 @@ public class EqualizerFragment extends SubsonicFragment { } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); try { equalizerController.saveSettings(); @@ -125,8 +125,8 @@ public class EqualizerFragment extends SubsonicFragment { } @Override - public void onResume() { - super.onResume(); + public void onStart() { + super.onStart(); equalizerController = DownloadService.getInstance().getEqualizerController(); equalizer = equalizerController.getEqualizer(); bass = equalizerController.getBassBoost(); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java index 5daf3d7a..82e50b76 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java @@ -1,11 +1,12 @@ package github.daneren2005.dsub.fragments; -import android.content.res.Resources; -import android.os.Environment; import android.content.Intent; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.StatFs; import android.util.Log; @@ -18,6 +19,7 @@ import github.daneren2005.dsub.adapter.MainAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.EnvironmentVariables; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.ProgressListener; @@ -28,13 +30,28 @@ import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.view.ChangeLog; import github.daneren2005.dsub.view.UpdateView; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.net.ssl.HttpsURLConnection; + public class MainFragment extends SelectRecyclerFragment<Integer> { private static final String TAG = MainFragment.class.getSimpleName(); + public static final String SONGS_LIST_PREFIX = "songs-"; + public static final String SONGS_NEWEST = SONGS_LIST_PREFIX + "newest"; + public static final String SONGS_TOP_PLAYED = SONGS_LIST_PREFIX + "topPlayed"; + public static final String SONGS_RECENT = SONGS_LIST_PREFIX + "recent"; + public static final String SONGS_FREQUENT = SONGS_LIST_PREFIX + "frequent"; public MainFragment() { super(); @@ -47,10 +64,11 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.main, menu); + onFinishSetupOptionsMenu(menu); try { - if (!ServerInfo.isMadsonic(context) || !UserUtil.isCurrentAdmin()) { - menu.setGroupVisible(R.id.madsonic, false); + if (!ServerInfo.canRescanServer(context) || !UserUtil.isCurrentAdmin()) { + menu.setGroupVisible(R.id.rescan_server, false); } } catch(Exception e) { Log.w(TAG, "Error on setting madsonic invisible", e); @@ -113,6 +131,22 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { sections.add(albums); headers.add("albums"); + if(ServerInfo.isMadsonic6(context)) { + List<Integer> songs = new ArrayList<>(); + + songs.add(R.string.main_songs_newest); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_top_played); + } + songs.add(R.string.main_songs_recent); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_frequent); + } + + sections.add(songs); + headers.add("songs"); + } + if(ServerInfo.checkServerVersion(context, "1.8")) { List<Integer> videos = Arrays.asList(R.string.main_videos); sections.add(videos); @@ -237,10 +271,10 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { private void getLogs() { try { - final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - new LoadingTask<File>(context) { + final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + new LoadingTask<String>(context) { @Override - protected File doInBackground() throws Throwable { + protected String doInBackground() throws Throwable { updateProgress("Gathering Logs"); File logcat = new File(Environment.getExternalStorageDirectory(), "dsub-logcat.txt"); Util.delete(logcat); @@ -258,30 +292,94 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()])); logcatProc.waitFor(); - } catch(Exception e) { - Util.toast(context, "Failed to gather logs"); } finally { if(logcatProc != null) { logcatProc.destroy(); } } - return logcat; + URL url = new URL("https://pastebin.com/api/api_post.php"); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + StringBuffer responseBuffer = new StringBuffer(); + try { + urlConnection.setReadTimeout(10000); + urlConnection.setConnectTimeout(15000); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + + OutputStream os = urlConnection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, Constants.UTF_8)); + writer.write("api_dev_key=" + URLEncoder.encode(EnvironmentVariables.PASTEBIN_DEV_KEY, Constants.UTF_8) + "&api_option=paste&api_paste_private=1&api_paste_code="); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logcat))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + + File stacktrace = new File(Environment.getExternalStorageDirectory(), "dsub-stacktrace.txt"); + if(stacktrace.exists() && stacktrace.isFile()) { + writer.write("\n\nMost Recent Stacktrace:\n\n"); + + reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(stacktrace))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + } + + writer.flush(); + writer.close(); + os.close(); + + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseBuffer.append(inputLine); + } + in.close(); + } finally { + urlConnection.disconnect(); + } + + String response = responseBuffer.toString(); + if(response.indexOf("http") == 0) { + return response.replace("http:", "https:"); + } else { + throw new Exception("Pastebin Error: " + response); + } + } + + @Override + protected void error(Throwable error) { + Log.e(TAG, "Failed to gather logs", error); + Util.toast(context, "Failed to gather logs"); } @Override - protected void done(File logcat) { + protected void done(String logcat) { String footer = "Android SDK: " + Build.VERSION.SDK; footer += "\nDevice Model: " + Build.MODEL; footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT; footer += "\nROM: " + Build.DISPLAY; + footer += "\nLogs: " + logcat; + footer += "\nBuild Number: " + packageInfo.versionCode; Intent email = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "dsub.android@gmail.com", null)); - email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs"); + email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + packageInfo.versionName + " Error Logs"); email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer); - Uri attachment = Uri.fromFile(logcat); - email.putExtra(Intent.EXTRA_STREAM, attachment); startActivity(email); } }.execute(); @@ -310,6 +408,14 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { showAlbumList("alphabeticalByName"); } else if(item == R.string.main_videos) { showVideos(); + } else if (item == R.string.main_songs_newest) { + showAlbumList(SONGS_NEWEST); + } else if (item == R.string.main_songs_top_played) { + showAlbumList(SONGS_TOP_PLAYED); + } else if (item == R.string.main_songs_recent) { + showAlbumList(SONGS_RECENT); + } else if (item == R.string.main_songs_frequent) { + showAlbumList(SONGS_FREQUENT); } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java index c557a174..10623b4e 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -51,15 +51,18 @@ import android.view.animation.AnimationUtils; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.PopupMenu; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ViewFlipper; +import com.shehabic.droppy.DroppyClickCallbackInterface; +import com.shehabic.droppy.DroppyMenuPopup; +import com.shehabic.droppy.animations.DroppyFadeInAnimation; import github.daneren2005.dsub.R; import github.daneren2005.dsub.activity.SubsonicFragmentActivity; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.audiofx.EqualizerController; import github.daneren2005.dsub.domain.Bookmark; -import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.domain.ServerInfo; @@ -73,6 +76,7 @@ import github.daneren2005.dsub.service.ServerTooOldException; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.adapter.DownloadFileAdapter; +import github.daneren2005.dsub.view.compat.CustomMediaRouteDialogFactory; import github.daneren2005.dsub.view.FadeOutAnimation; import github.daneren2005.dsub.view.FastScroller; import github.daneren2005.dsub.view.UpdateView; @@ -88,7 +92,6 @@ import java.util.concurrent.ScheduledFuture; public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener<DownloadFile>, OnSongChangedListener { private static final String TAG = NowPlayingFragment.class.getSimpleName(); private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; - private static final int INCREMENT_TIME = 5000; private static final int ACTION_PREVIOUS = 1; private static final int ACTION_NEXT = 2; @@ -106,6 +109,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private SeekBar progressBar; private AutoRepeatButton previousButton; private AutoRepeatButton nextButton; + private AutoRepeatButton rewindButton; + private AutoRepeatButton fastforwardButton; private View pauseButton; private View stopButton; private View startButton; @@ -115,6 +120,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private ImageButton bookmarkButton; private ImageButton rateBadButton; private ImageButton rateGoodButton; + private ImageButton playbackSpeedButton; private ScheduledExecutorService executorService; private DownloadFile currentPlaying; @@ -129,6 +135,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private int lastY = 0; private int currentPlayingSize = 0; private MenuItem timerMenu; + private DroppySpeedControl speed; /** * Called when the activity is first created. @@ -172,6 +179,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar); previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous); nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next); + rewindButton = (AutoRepeatButton) rootView.findViewById(R.id.download_rewind); + fastforwardButton = (AutoRepeatButton) rootView.findViewById(R.id.download_fastforward); pauseButton =rootView.findViewById(R.id.download_pause); stopButton =rootView.findViewById(R.id.download_stop); startButton =rootView.findViewById(R.id.download_start); @@ -179,6 +188,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark); rateBadButton = (ImageButton) rootView.findViewById(R.id.download_rating_bad); rateGoodButton = (ImageButton) rootView.findViewById(R.id.download_rating_good); + playbackSpeedButton = (ImageButton) rootView.findViewById(R.id.download_playback_speed); toggleListButton =rootView.findViewById(R.id.download_toggle_list); playlistView = (RecyclerView)rootView.findViewById(R.id.download_list); @@ -194,6 +204,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View v) { getDownloadService().toggleStarred(); + setControlsVisible(true); } }); } else { @@ -212,6 +223,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis bookmarkButton.setOnTouchListener(touchListener); rateBadButton.setOnTouchListener(touchListener); rateGoodButton.setOnTouchListener(touchListener); + playbackSpeedButton.setOnTouchListener(touchListener); emptyTextView.setOnTouchListener(touchListener); albumArtImageView.setOnTouchListener(new View.OnTouchListener() { @Override @@ -239,7 +251,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); previousButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(-INCREMENT_TIME); + changeProgress(true); } }); @@ -259,10 +271,35 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); nextButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(INCREMENT_TIME); + changeProgress(false); + } + }); + + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(true); + } + }); + rewindButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(false); + } + }); + fastforwardButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); } }); + pauseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -330,6 +367,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View view) { createBookmark(); + setControlsVisible(true); } }); @@ -341,6 +379,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(1); + setControlsVisible(true); } }); rateGoodButton.setOnClickListener(new View.OnClickListener() { @@ -351,9 +390,16 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(5); + setControlsVisible(true); } }); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setPlaybackSpeed(); + } else { + playbackSpeedButton.setVisibility(View.GONE); + } + toggleListButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -405,11 +451,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } }); - if(Build.MODEL.equals("Nexus 4") || Build.MODEL.equals("GT-I9100")) { - View slider = rootView.findViewById(R.id.download_slider); - slider.setPadding(0, 0, 0, 0); - } - return rootView; } @@ -438,7 +479,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable(); - if(equalizerAvailable && !downloadService.isRemoteEnabled()) { + boolean isRemoteEnabled = downloadService != null && downloadService.isRemoteEnabled(); + if(equalizerAvailable && !isRemoteEnabled) { SharedPreferences prefs = Util.getPreferences(context); boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false); if (equalizerOn && downloadService != null) { @@ -450,12 +492,32 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis menu.removeItem(R.id.menu_equalizer); } + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M || isRemoteEnabled) { + playbackSpeedButton.setVisibility(View.GONE); + } else { + playbackSpeedButton.setVisibility(View.VISIBLE); + } + if(downloadService != null) { MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute); if(mediaRouteItem != null) { MediaRouteButton mediaRouteButton = (MediaRouteButton) MenuItemCompat.getActionView(mediaRouteItem); + mediaRouteButton.setDialogFactory(new CustomMediaRouteDialogFactory()); mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector()); } + + if(downloadService.isCurrentPlayingSingle()) { + if(!Util.isOffline(context)) { + menu.removeItem(R.id.menu_save_playlist); + } + + menu.removeItem(R.id.menu_batch_mode); + menu.removeItem(R.id.menu_remove_played); + } + } + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) { + menu.findItem(R.id.menu_batch_mode).setChecked(true); } } @@ -474,7 +536,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis menuInflater.inflate(R.menu.nowplaying_context_offline, menu); } else { menuInflater.inflate(R.menu.nowplaying_context, menu); - menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); + menu.findItem(R.id.song_menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); } if (downloadFile.getSong().getParent() == null) { @@ -620,9 +682,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } createNewPlaylist(entries, true); return true; - case R.id.menu_star: - UpdateHelper.toggleStarred(context, song.getSong()); - return true; case R.id.menu_rate: UpdateHelper.setRating(context, song.getSong()); return true; @@ -634,11 +693,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis startTimer(); } return true; - case R.id.menu_add_playlist: - songs = new ArrayList<Entry>(1); - songs.add(song.getSong()); - addToPlaylist(songs); - return true; case R.id.menu_info: displaySongInfo(song.getSong()); return true; @@ -663,14 +717,25 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis // Any failed condition will get here Util.toast(context, "Failed to start equalizer. Try restarting."); return true; - } default: + }case R.id.menu_batch_mode: + if(Util.isBatchMode(context)) { + Util.setBatchMode(context, false); + songListAdapter.notifyDataSetChanged(); + } else { + Util.setBatchMode(context, true); + songListAdapter.notifyDataSetChanged(); + } + context.supportInvalidateOptionsMenu(); + + return true; + default: return false; } } @Override - public void onResume() { - super.onResume(); + public void onStart() { + super.onStart(); if(this.primaryFragment) { onResumeHandlers(); } else { @@ -678,13 +743,13 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } private void onResumeHandlers() { - final Handler handler = new Handler(); executorService = Executors.newSingleThreadScheduledExecutor(); setControlsVisible(true); final DownloadService downloadService = getDownloadService(); if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) { playlistFlipper.setDisplayedChild(1); + startFlipped = false; } if (downloadService != null && downloadService.getKeepScreenOn()) { context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -701,19 +766,20 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis context.runWhenServiceAvailable(new Runnable() { @Override public void run() { - if(primaryFragment) { + if (primaryFragment) { DownloadService downloadService = getDownloadService(); downloadService.startRemoteScan(); downloadService.addOnSongChangedListener(NowPlayingFragment.this, true); } updateRepeatButton(); + updateTitle(); } }); } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); onPauseHandlers(); } private void onPauseHandlers() { @@ -780,6 +846,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } private void setControlsVisible(boolean visible) { + DownloadService downloadService = getDownloadService(); + if(downloadService != null && downloadService.isCurrentPlayingSingle()) { + return; + } + try { long duration = 1700L; FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration); @@ -899,10 +970,14 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private int getMinutes(int progress) { if(progress < 30) { return progress + 1; - } else if(progress < 61) { + } else if(progress < 49) { return (progress - 30) * 5 + getMinutes(29); + } else if(progress < 57) { + return (progress - 48) * 30 + getMinutes(48); + } else if(progress < 81) { + return (progress - 56) * 60 + getMinutes(56); } else { - return (progress - 61) * 15 + getMinutes(60); + return (progress - 80) * 150 + getMinutes(80); } } @@ -938,31 +1013,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } - private void changeProgress(final int ms) { + private void changeProgress(final boolean rewind) { final DownloadService downloadService = getDownloadService(); if(downloadService == null) { return; } new SilentBackgroundTask<Void>(context) { - boolean isJukeboxEnabled; - int msPlayed; - Integer duration; - PlayerState playerState; int seekTo; @Override protected Void doInBackground() throws Throwable { - msPlayed = Math.max(0, downloadService.getPlayerPosition()); - duration = downloadService.getPlayerDuration(); - playerState = getDownloadService().getPlayerState(); - int msTotal = duration == null ? 0 : duration; - if(msPlayed + ms > msTotal) { - seekTo = msTotal; + if(rewind) { + seekTo = downloadService.rewind(); } else { - seekTo = msPlayed + ms; + seekTo = downloadService.fastForward(); } - downloadService.seekTo(seekTo); return null; } @@ -1156,13 +1222,55 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } @Override - public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) { this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + + updateMediaButton(shouldFastForward); + updateTitle(); + setPlaybackSpeed(); + } + + private void updateMediaButton(boolean shouldFastForward) { + DownloadService downloadService = getDownloadService(); + if(downloadService.isCurrentPlayingSingle()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } else { + if (downloadService.shouldFastForward()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + } + } + + private void setupSubtitle(int currentPlayingIndex) { if (currentPlaying != null) { Entry song = currentPlaying.getSong(); songTitleTextView.setText(song.getTitle()); getImageLoader().loadImage(albumArtImageView, song, true, true); - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isCurrentPlayingSingle()) { + setSubtitle(null); + } else if(downloadService.isShufflePlayEnabled()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle)); + } else if(downloadService.isArtistRadio()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_artist_radio)); + } else { + setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + } } else { songTitleTextView.setText(null); getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); @@ -1171,7 +1279,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } @Override - public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) { + public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) { currentPlayingSize = songs.size(); DownloadService downloadService = getDownloadService(); @@ -1199,11 +1307,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis scrollWhenLoaded = false; } - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); if(this.currentPlaying != currentPlaying) { - onSongChanged(currentPlaying, currentPlayingIndex); + onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward); onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + } else { + updateMediaButton(shouldFastForward); + setupSubtitle(currentPlayingIndex); + } + + if(downloadService.isCurrentPlayingSingle()) { + toggleListButton.setVisibility(View.GONE); + repeatButton.setVisibility(View.GONE); + } else { + toggleListButton.setVisibility(View.VISIBLE); + repeatButton.setVisibility(View.VISIBLE); } + setPlaybackSpeed(); } @Override @@ -1258,11 +1377,16 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis break; default: if(currentPlaying != null) { - String artist = ""; - if(currentPlaying.getSong().getArtist() != null) { - artist = currentPlaying.getSong().getArtist() + " - "; + Entry entry = currentPlaying.getSong(); + if(entry.getAlbum() != null) { + String artist = ""; + if (entry.getArtist() != null) { + artist = currentPlaying.getSong().getArtist() + " - "; + } + statusTextView.setText(artist + entry.getAlbum()); + } else { + statusTextView.setText(null); } - statusTextView.setText(artist + currentPlaying.getSong().getAlbum()); } else { statusTextView.setText(null); } @@ -1334,6 +1458,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } bookmarkButton.setImageResource(bookmark); } + + if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + getImageLoader().loadImage(albumArtImageView, song, true, true); + } } public void updateRepeatButton() { @@ -1352,4 +1480,103 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis break; } } + private void updateTitle() { + DownloadService downloadService = getDownloadService(); + float playbackSpeed = downloadService.getPlaybackSpeed(); + + String title = context.getResources().getString(R.string.button_bar_now_playing); + int stringRes = -1; + if(playbackSpeed == 0.5f) { + stringRes = R.string.download_playback_speed_half; + } else if(playbackSpeed == 1.5f) { + stringRes = R.string.download_playback_speed_one_half; + } else if(playbackSpeed == 2.0f) { + stringRes = R.string.download_playback_speed_double; + } else if(playbackSpeed == 3.0f) { + stringRes = R.string.download_playback_speed_tripple; + } + + String playbackSpeedText = null; + if(stringRes != -1) { + playbackSpeedText = context.getResources().getString(stringRes); + } else if(Math.abs(playbackSpeed - 1.0) > 0.01) { + playbackSpeedText = Float.toString(playbackSpeed) + "x"; + } + + if(playbackSpeedText != null) { + title += " (" + playbackSpeedText + ")"; + } + setTitle(title); + } + + @Override + protected List<Entry> getSelectedEntries() { + List<DownloadFile> selected = getCurrentAdapter().getSelected(); + List<Entry> entries = new ArrayList<>(); + + for(DownloadFile downloadFile: selected) { + if(downloadFile.getSong() != null) { + entries.add(downloadFile.getSong()); + } + } + + return entries; + } + + private void setPlaybackSpeed() { + if (playbackSpeedButton.getVisibility() == View.GONE) + return; + speed = new DroppySpeedControl(R.layout.set_playback_speed); + DroppyMenuPopup.Builder builder = new DroppyMenuPopup.Builder(context,playbackSpeedButton); + speed.setClickable(true); + float playbackSpeed; + + playbackSpeed = getDownloadService() != null ? getDownloadService().getPlaybackSpeed() : 1.0f; + + final DroppyMenuPopup popup = builder.triggerOnAnchorClick(true).addMenuItem(speed).setPopupAnimation(new DroppyFadeInAnimation()).build(); + speed.setOnSeekBarChangeListener(context, new DroppyClickCallbackInterface() { + @Override + public void call(View v, int id) { + SeekBar playbackSpeedBar = (SeekBar) v; + int playbackSpeed = playbackSpeedBar.getProgress() +5 ; + setPlaybackSpeed(playbackSpeed/10f); + } + },R.id.playback_speed_bar,R.id.playback_speed_label,playbackSpeed); + speed.setOnClicks(context, + new DroppyClickCallbackInterface() { + @Override + public void call(View v, int id) { + float playbackSpeed = 1.0f; + switch (id) { + case R.id.playback_speed_one_half: + playbackSpeed = 1.5f; + break; + case R.id.playback_speed_double: + playbackSpeed = 2.0f; + break; + case R.id.playback_speed_triple: + playbackSpeed = 3.0f; + break; + default: + break; + } + setPlaybackSpeed(playbackSpeed); + speed.updateSeekBar(playbackSpeed); + popup.dismiss(true); + } + } + ,R.id.playback_speed_normal,R.id.playback_speed_one_half,R.id.playback_speed_double, + R.id.playback_speed_triple); + speed.updateSeekBar(playbackSpeed); + + } + private void setPlaybackSpeed(float playbackSpeed) { + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + + downloadService.setPlaybackSpeed(playbackSpeed); + updateTitle(); + } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java index d21b82e0..dfff45cd 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java @@ -3,10 +3,14 @@ package github.daneren2005.dsub.fragments; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.TreeMap; import android.content.Intent; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; @@ -25,6 +29,7 @@ import github.daneren2005.dsub.adapter.SearchAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.SearchCritera; import github.daneren2005.dsub.domain.SearchResult; import github.daneren2005.dsub.service.MusicService; @@ -39,9 +44,9 @@ import github.daneren2005.dsub.view.UpdateView; public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener<Serializable> { private static final String TAG = SearchFragment.class.getSimpleName(); - private static final int MAX_ARTISTS = 10; - private static final int MAX_ALBUMS = 10; - private static final int MAX_SONGS = 25; + private static final int MAX_ARTISTS = 20; + private static final int MAX_ALBUMS = 20; + private static final int MAX_SONGS = 50; private static final int MIN_CLOSENESS = 1; protected RecyclerView recyclerView; @@ -108,13 +113,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { int viewType = adapter.getItemViewType(position); if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == ArtistAdapter.VIEW_TYPE_ARTIST) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -125,24 +130,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.search, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_search: - context.startSearch(currentQuery, false, null, false); - return true; - } - - return super.onOptionsItemSelected(item); - + onFinishSetupOptionsMenu(menu); } @Override public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) { onCreateContextMenuSupport(menu, menuInflater, updateView, item); - if(item instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) item).isVideo() && !Util.isOffline(context)) { + if(item instanceof Entry && !((Entry) item).isVideo() && !Util.isOffline(context)) { menu.removeItem(R.id.song_menu_remove_playlist); } recreateContextMenu(menu); @@ -162,8 +156,8 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) { if (item instanceof Artist) { onArtistSelected((Artist) item, false); - } else if (item instanceof MusicDirectory.Entry) { - MusicDirectory.Entry entry = (MusicDirectory.Entry) item; + } else if (item instanceof Entry) { + Entry entry = (Entry) item; if (entry.isDirectory()) { onAlbumSelected(entry, false); } else if (entry.isVideo()) { @@ -175,19 +169,24 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } @Override - protected List<MusicDirectory.Entry> getSelectedEntries() { + protected List<Entry> getSelectedEntries() { List<Serializable> selected = adapter.getSelected(); - List<MusicDirectory.Entry> selectedMedia = new ArrayList<>(); + List<Entry> selectedMedia = new ArrayList<>(); for(Serializable ser: selected) { - if(ser instanceof MusicDirectory.Entry) { - selectedMedia.add((MusicDirectory.Entry) ser); + if(ser instanceof Entry) { + selectedMedia.add((Entry) ser); } } return selectedMedia; } - public void search(final String query, final boolean autoplay) { + @Override + protected boolean isShowArtistEnabled() { + return true; + } + + public void search(final String query, final boolean autoplay, final String artist, final String album, final String title) { if(skipSearch) { skipSearch = false; return; @@ -207,12 +206,20 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O searchResult = result; recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, SearchFragment.this)); if (autoplay) { - autoplay(query); + autoplay(query, artist, album, title); } } }; task.execute(); + + if(searchItem != null) { + MenuItemCompat.collapseActionView(searchItem); + } + } + + protected String getCurrentQuery() { + return currentQuery; } private void onArtistSelected(Artist artist, boolean autoplay) { @@ -229,7 +236,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O replaceFragment(fragment); } - private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) { + private void onAlbumSelected(Entry album, boolean autoplay) { SubsonicFragment fragment = new SelectDirectoryFragment(); Bundle args = new Bundle(); args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId()); @@ -242,7 +249,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O replaceFragment(fragment); } - private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) { + private void onSongSelected(Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) { DownloadService downloadService = getDownloadService(); if (downloadService != null) { if (!append) { @@ -257,7 +264,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } } - private void onVideoSelected(MusicDirectory.Entry entry) { + private void onVideoSelected(Entry entry) { int maxBitrate = Util.getMaxVideoBitrate(context); Intent intent = new Intent(Intent.ACTION_VIEW); @@ -265,6 +272,55 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O startActivity(intent); } + private void autoplay(String query, String artistQuery, String albumQuery, String titleQuery) { + Log.i(TAG, "Query: '" + query + "' ( Artist: '" + artistQuery + "', Album: '" + albumQuery + "', Title: '" + titleQuery + "')"); + + if(titleQuery != null && !searchResult.getSongs().isEmpty()) { + titleQuery = titleQuery.toLowerCase(); + + TreeMap<Integer, Entry> tree = new TreeMap<>(); + for(Entry song: searchResult.getSongs()) { + tree.put(Util.getStringDistance(song.getTitle().toLowerCase(), titleQuery), song); + } + + Map.Entry<Integer, Entry> entry = tree.firstEntry(); + if(entry.getKey() <= MIN_CLOSENESS) { + onSongSelected(entry.getValue(), false, false, true, false); + } else { + autoplay(query); + } + } else if(albumQuery != null && !searchResult.getAlbums().isEmpty()) { + albumQuery = albumQuery.toLowerCase(); + + TreeMap<Integer, Entry> tree = new TreeMap<>(); + for(Entry album: searchResult.getAlbums()) { + tree.put(Util.getStringDistance(album.getTitle().toLowerCase(), albumQuery), album); + } + + Map.Entry<Integer, Entry> entry = tree.firstEntry(); + if(entry.getKey() <= MIN_CLOSENESS) { + onAlbumSelected(entry.getValue(), true); + } else { + autoplay(query); + } + } else if(artistQuery != null && !searchResult.getArtists().isEmpty()) { + artistQuery = artistQuery.toLowerCase(); + + TreeMap<Integer, Artist> tree = new TreeMap<>(); + for(Artist artist: searchResult.getArtists()) { + tree.put(Util.getStringDistance(artist.getName().toLowerCase(), artistQuery), artist); + } + Map.Entry<Integer, Artist> entry = tree.firstEntry(); + if(entry.getKey() <= MIN_CLOSENESS) { + onArtistSelected(entry.getValue(), true); + } else { + autoplay(query); + } + } else { + autoplay(query); + } + } + private void autoplay(String query) { query = query.toLowerCase(); @@ -273,12 +329,12 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O artist = searchResult.getArtists().get(0); artist.setCloseness(Util.getStringDistance(artist.getName().toLowerCase(), query)); } - MusicDirectory.Entry album = null; + Entry album = null; if(!searchResult.getAlbums().isEmpty()) { album = searchResult.getAlbums().get(0); album.setCloseness(Util.getStringDistance(album.getTitle().toLowerCase(), query)); } - MusicDirectory.Entry song = null; + Entry song = null; if(!searchResult.getSongs().isEmpty()) { song = searchResult.getSongs().get(0); song.setCloseness(Util.getStringDistance(song.getTitle().toLowerCase(), query)); @@ -286,10 +342,10 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O if(artist != null && (artist.getCloseness() <= MIN_CLOSENESS || (album == null || artist.getCloseness() <= album.getCloseness()) && - (song == null || artist.getCloseness() <= song.getCloseness()))) { + (song == null || artist.getCloseness() <= song.getCloseness()))) { onArtistSelected(artist, true); } else if(album != null && (album.getCloseness() <= MIN_CLOSENESS || - song == null || album.getCloseness() <= song.getCloseness())) { + song == null || album.getCloseness() <= song.getCloseness())) { onAlbumSelected(album, true); } else if(song != null) { onSongSelected(song, false, false, true, false); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java index 3df0a9a9..e971bfb6 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java @@ -17,6 +17,7 @@ import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.Indexes; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.service.MusicService; @@ -29,12 +30,11 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -public class SelectArtistFragment extends SelectRecyclerFragment<Artist> implements ArtistAdapter.OnMusicFolderChanged { +public class SelectArtistFragment extends SelectRecyclerFragment<Serializable> implements ArtistAdapter.OnMusicFolderChanged { private static final String TAG = SelectArtistFragment.class.getSimpleName(); - private static final int MENU_GROUP_MUSIC_FOLDER = 10; private List<MusicFolder> musicFolders = null; - private List<MusicDirectory.Entry> entries; + private List<Entry> entries; private String groupId; private String groupName; @@ -63,12 +63,14 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { Bundle args = getArguments(); if(args != null) { - groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); - groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + if(args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)) { + groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); + groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); - if(groupName != null) { - setTitle(groupName); - context.invalidateOptionsMenu(); + if (groupName != null) { + setTitle(groupName); + context.invalidateOptionsMenu(); + } } } @@ -78,47 +80,56 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) { onCreateContextMenuSupport(menu, menuInflater, updateView, item); recreateContextMenu(menu); } @Override - public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Artist> updateView, Artist item) { + public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Serializable> updateView, Serializable item) { return onContextItemSelected(menuItem, item); } @Override - public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { + public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) { SubsonicFragment fragment; - if((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || "root".equals(artist.getId()) || groupId != null) { - fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - - if ("root".equals(artist.getId())) { - args.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries); - } - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + if(item instanceof Artist) { + Artist artist = (Artist) item; + + if ((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || groupId != null) { + fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + + fragment.setArguments(args); + } else { + fragment = new SelectArtistFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + + fragment.setArguments(args); } - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); + replaceFragment(fragment); } else { - fragment = new SelectArtistFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + Entry entry = (Entry) item; + if (entry.isVideo()) { + playVideo(entry); + } else { + onSongPress(entries, entry); } - - fragment.setArguments(args); } - - replaceFragment(fragment); } @Override @@ -155,15 +166,15 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public SectionAdapter getAdapter(List<Artist> objects) { + public SectionAdapter getAdapter(List<Serializable> objects) { return new ArtistAdapter(context, objects, musicFolders, this, this); } @Override - public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { - List<Artist> artists; + public List<Serializable> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List<Serializable> items; if(groupId == null) { - if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) { + if (!Util.isOffline(context) && (!Util.isTagBrowsing(context) || ServerInfo.checkServerVersion(context, "1.14"))) { musicFolders = musicService.getMusicFolders(refresh, context, listener); // Hide folders option if there is only one @@ -178,14 +189,16 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener); indexes.sortChildren(context); - artists = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); - artists.addAll(indexes.getShortcuts()); - artists.addAll(indexes.getArtists()); + items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); + items.addAll(indexes.getShortcuts()); + items.addAll(indexes.getArtists()); entries = indexes.getEntries(); + items.addAll(entries); } else { - artists = new ArrayList<>(); + List<Artist> artists = new ArrayList<>(); + items = new ArrayList<>(); MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener); - for(MusicDirectory.Entry entry: dir.getChildren(true, false)) { + for(Entry entry: dir.getChildren(true, false)) { Artist artist = new Artist(); artist.setId(entry.getId()); artist.setName(entry.getTitle()); @@ -193,21 +206,17 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme artists.add(artist); } - entries = new ArrayList<>(); - entries.addAll(dir.getChildren(false, true)); - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } - - Indexes indexes = new Indexes(0, artists, new ArrayList<Artist>()); + Indexes indexes = new Indexes(0, new ArrayList<Artist>(), artists); indexes.sortChildren(context); + items.addAll(indexes.getArtists()); + + entries = dir.getChildren(false, true); + for(Entry entry: entries) { + items.add(entry); + } } - return artists; + return items; } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java index 5f3ca38b..c320f3c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java @@ -18,6 +18,7 @@ */ package github.daneren2005.dsub.fragments; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -28,6 +29,7 @@ import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.MenuUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -89,19 +91,29 @@ public class SelectBookmarkFragment extends SelectRecyclerFragment<MusicDirector return; } - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - downloadService.clear(); - downloadService.download(Arrays.asList(bookmark), false, true, false, false, 0, bookmark.getBookmark().getPosition()); - return null; - } - - @Override - protected void done(Void result) { - context.openNowPlaying(); - } - }.execute(); + boolean allowPlayAll = ((!Util.isTagBrowsing(context) && bookmark.getParent() != null) || (Util.isTagBrowsing(context) && bookmark.getAlbumId() != null)) && !bookmark.isPodcast(); + if(allowPlayAll && "all".equals(Util.getSongPressAction(context))) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSiblingsRecursively(bookmark); + + if(songs.isEmpty() || !songs.contains(bookmark)) { + playNowInTask(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition()); + } else { + playNowInTask(songs, bookmark, bookmark.getBookmark().getPosition()); + } + return null; + } + + @Override + protected void done(Boolean result) { + context.openNowPlaying(); + } + }.execute(); + } else { + onSongPress(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition(), false); + } } private void displayBookmarkInfo(final MusicDirectory.Entry entry) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java index d2282117..d3a0bfe8 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -39,6 +39,7 @@ import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.Share; +import github.daneren2005.dsub.service.CachedMusicService; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; @@ -86,6 +87,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section private ArtistInfo artistInfo; private String artistInfoDelayed; + private SilentBackgroundTask updateCoverArtTask; + private ImageView coverArtView; + private Entry coverArtRep; + private String coverArtId; + String id; String name; Entry directory; @@ -184,27 +190,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section recyclerView.setHasFixedSize(true); fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); setupScrollList(recyclerView); - - if(largeAlbums) { - GridLayoutManager gridLayoutManager = new GridLayoutManager(context, getRecyclerColumnCount()); - gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - int viewType = entryGridAdapter.getItemViewType(position); - if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { - return getRecyclerColumnCount(); - } else { - return 1; - } - } - }); - recyclerView.addItemDecoration(new GridSpacingDecoration()); - recyclerView.setLayoutManager(gridLayoutManager); - } else { - LinearLayoutManager layoutManager = new LinearLayoutManager(context); - layoutManager.setOrientation(LinearLayoutManager.VERTICAL); - recyclerView.setLayoutManager(layoutManager); - } + setupLayoutManager(recyclerView, largeAlbums); if(entries == null) { if(primaryFragment || secondaryFragment) { @@ -248,7 +234,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!ServerInfo.hasTopSongs(context)) { menu.removeItem(R.id.menu_top_tracks); } - if(!ServerInfo.checkServerVersion(context, "1.11") || (id != null && "root".equals(id))) { + if(!ServerInfo.checkServerVersion(context, "1.11")) { menu.removeItem(R.id.menu_radio); menu.removeItem(R.id.menu_similar_artists); } else if(!ServerInfo.hasSimilarArtists(context)) { @@ -305,9 +291,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section case R.id.menu_show_all: setShowAll(); return true; - case R.id.menu_unstar: - unstarSelected(); - return true; case R.id.menu_top_tracks: showTopTracks(); return true; @@ -329,10 +312,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) { menu.removeItem(R.id.song_menu_remove_playlist); } - // Remove show artists if parent is not set and if not on a album list - if((albumListType == null || (entry.getParent() == null && entry.getArtistId() == null)) && !Util.isOffline(context)) { - menu.removeItem(R.id.album_menu_show_artist); - } recreateContextMenu(menu); } @@ -380,28 +359,20 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section return; } - playNow(Arrays.asList(entry)); + onSongPress(Arrays.asList(entry), entry, false); } else { - List<Entry> songs = new ArrayList<Entry>(); - - if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true)) { - Iterator it = entries.listIterator(entries.indexOf(entry)); - while(it.hasNext()) { - songs.add((Entry) it.next()); - } - } else { - songs.add(entry); - } - - playNow(songs); + onSongPress(entries, entry, albumListType == null || "starred".equals(albumListType)); } } @Override protected void refresh(boolean refresh) { - if(!"root".equals(id)) { - load(refresh); - } + load(refresh); + } + + @Override + protected boolean isShowArtistEnabled() { + return albumListType != null; } private void load(boolean refresh) { @@ -488,8 +459,12 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } List<Entry> songs = new ArrayList<Entry>(); getSongsRecursively(root, songs); - root.replaceChildren(songs); - return root; + + // CachedMusicService is refreshing this data in the background, so will wipe out the songs list from root + MusicDirectory clonedRoot = new MusicDirectory(songs); + clonedRoot.setId(root.getId()); + clonedRoot.setName(root.getName()); + return clonedRoot; } private void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception { @@ -577,6 +552,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section setTitle(albumListExtra); } else if("alphabeticalByName".equals(albumListType)) { setTitle(R.string.main_albums_alphabetical); + } if (MainFragment.SONGS_NEWEST.equals(albumListType)) { + setTitle(R.string.main_songs_newest); + } else if (MainFragment.SONGS_TOP_PLAYED.equals(albumListType)) { + setTitle(R.string.main_songs_top_played); + } else if (MainFragment.SONGS_RECENT.equals(albumListType)) { + setTitle(R.string.main_songs_recent); + } else if (MainFragment.SONGS_FREQUENT.equals(albumListType)) { + setTitle(R.string.main_songs_frequent); } new LoadTask(true) { @@ -593,6 +576,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } } else if("genres".equals(albumListType) || "genres-songs".equals(albumListType)) { result = service.getSongsByGenre(albumListExtra, size, 0, context, this); + } else if(albumListType.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(albumListType, size, 0, context, this); } else { result = service.getAlbumList(albumListType, size, 0, refresh, context, this); } @@ -657,9 +642,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } @Override - public void updateCache() { - if(entryGridAdapter != null) { + public void updateCache(int changeCode) { + if(entryGridAdapter != null && changeCode == CachedMusicService.CACHE_UPDATE_LIST) { entryGridAdapter.notifyDataSetChanged(); + } else if(changeCode == CachedMusicService.CACHE_UPDATE_METADATA) { + if(coverArtView != null && coverArtRep != null && !Util.equals(coverArtRep.getCoverArt(), coverArtId)) { + synchronized (coverArtRep) { + if (updateCoverArtTask != null && updateCoverArtTask.isRunning()) { + updateCoverArtTask.cancel(); + } + updateCoverArtTask = getImageLoader().loadImage(coverArtView, coverArtRep, false, true); + coverArtId = coverArtRep.getCoverArt(); + } + } } } } @@ -669,6 +664,21 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section return entryGridAdapter; } + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = entryGridAdapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + private void finishLoading() { boolean validData = !entries.isEmpty() || !albums.isEmpty(); if(!validData) { @@ -682,7 +692,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(albumListType == null || "starred".equals(albumListType)) { entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums); entryGridAdapter.setRemoveFromPlaylist(playlistId != null); - entryGridAdapter.setRemoveStarred(albumListType == null); } else { if("alphabeticalByName".equals(albumListType)) { entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums); @@ -728,11 +737,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!artist) { entryGridAdapter.setShowArtist(true); } + if(topTracks || showAll) { + entryGridAdapter.setShowAlbum(true); + } // Show header if not album list type and not root and not artist // For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists boolean addedHeader = false; - if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { + if(albumListType == null && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { View header = createHeader(); if(header != null) { @@ -792,7 +804,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section Bundle args = getArguments(); boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); if (playAll && !restoredInstance) { - playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false); + playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false, false); } } @@ -802,20 +814,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!songs.isEmpty()) { download(songs, append, false, !append, playNext, shuffle); entryGridAdapter.clearSelected(); - } - else { - playAll(shuffle, append); + } else { + playAll(shuffle, append, playNext); } } - private void playAll(final boolean shuffle, final boolean append) { + private void playAll(final boolean shuffle, final boolean append, final boolean playNext) { boolean hasSubFolders = albums != null && !albums.isEmpty(); if (hasSubFolders && (id != null || share != null || "starred".equals(albumListType))) { - downloadRecursively(id, false, append, !append, shuffle, false); + downloadRecursively(id, false, append, !append, shuffle, false, playNext); } else if(hasSubFolders && albumListType != null) { - downloadRecursively(albums, shuffle, append); + downloadRecursively(albums, shuffle, append, playNext); } else { - download(entries, append, false, !append, false, shuffle); + download(entries, append, false, !append, playNext, shuffle); } } @@ -909,7 +920,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section for(Integer index: indexes) { entryGridAdapter.removeAt(index); } - Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name)); + Util.toast(context, context.getResources().getString(R.string.removed_playlist, String.valueOf(indexes.size()), name)); } @Override @@ -953,70 +964,28 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }.execute(); } - public void unstarSelected() { - List<Entry> selected = getSelectedEntries(); - if(selected.size() == 0) { - selected = entries; - } - if(selected.size() == 0) { - return; - } - final List<Entry> unstar = new ArrayList<Entry>(); - unstar.addAll(selected); + @Override + protected void toggleSelectedStarred() { + UpdateHelper.OnStarChange onStarChange = null; + if(albumListType != null && "starred".equals(albumListType)) { + onStarChange = new UpdateHelper.OnStarChange() { + @Override + public void starChange(boolean starred) { - new LoadingTask<Void>(context, true) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - List<Entry> entries = new ArrayList<Entry>(); - List<Entry> artists = new ArrayList<Entry>(); - List<Entry> albums = new ArrayList<Entry>(); - for(Entry entry: unstar) { - if(entry.isDirectory() && Util.isTagBrowsing(context)) { - if(entry.isAlbum()) { - albums.add(entry); - } else { - artists.add(entry); - } - } else { - entries.add(entry); - } } - musicService.setStarred(entries, artists, albums, false, this, context); - for(Entry entry: unstar) { - new UpdateHelper.EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(false); + @Override + public void starCommited(boolean starred) { + if(!starred) { + for (Entry entry : entries) { + entryGridAdapter.removeItem(entry); } - }.execute(); - } - - return null; - } - - @Override - protected void done(Void result) { - Util.toast(context, context.getResources().getString(R.string.starring_content_unstarred, Integer.toString(unstar.size()))); - - for(Entry entry: unstar) { - entryGridAdapter.removeItem(entry); - } - } - - @Override - protected void error(Throwable error) { - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(R.string.starring_content_error, Integer.toString(unstar.size())) + " " + getErrorMessage(error); + } } + }; + } - Util.toast(context, msg, false); - } - }.execute(); + UpdateHelper.toggleStarred(context, getSelectedEntries(), onStarChange); } private void checkLicenseAndTrialPeriod(LoadingTask onValid) { @@ -1105,11 +1074,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section @Override protected Void doInBackground() throws Throwable { DownloadService downloadService = getDownloadService(); + downloadService.clear(); downloadService.setArtistRadio(artistId); - if(downloadService.size() == 0) { - Log.e(TAG, "Failed to create artist radio"); - throw new Exception("Failed to create artist radio"); - } return null; } @@ -1159,22 +1125,22 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }); imageLoader.loadImage(coverArtView, url, false); } else if(entries.size() > 0) { - Entry coverArt = null; - for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) { - coverArt = entries.get(random.nextInt(entries.size())); + coverArtRep = null; + this.coverArtView = coverArtView; + for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) { + coverArtRep = entries.get(random.nextInt(entries.size())); } - final Entry albumRep = coverArt; coverArtView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (albumRep.getCoverArt() == null) { + if (coverArtRep == null || coverArtRep.getCoverArt() == null) { return; } AlertDialog.Builder builder = new AlertDialog.Builder(context); ImageView fullScreenView = new ImageView(context); - imageLoader.loadImage(fullScreenView, albumRep, true, true); + imageLoader.loadImage(fullScreenView, coverArtRep, true, true); builder.setCancelable(true); AlertDialog imageDialog = builder.create(); @@ -1183,7 +1149,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section imageDialog.show(); } }); - imageLoader.loadImage(coverArtView, albumRep, false, true); + synchronized (coverArtRep) { + coverArtId = coverArtRep.getCoverArt(); + updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true); + } } coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { @@ -1349,6 +1318,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); } } + + @Override + public void starCommited(boolean starred) { + + } }); } }); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java new file mode 100644 index 00000000..74c4b269 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java @@ -0,0 +1,157 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.fragments; + +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.adapter.InternetRadioStationAdapter; +import github.daneren2005.dsub.adapter.SectionAdapter; +import github.daneren2005.dsub.domain.InternetRadioStation; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.TabBackgroundTask; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.UpdateView; + +public class SelectInternetRadioStationFragment extends SelectRecyclerFragment<InternetRadioStation> { + private static final String TAG = SelectInternetRadioStationFragment.class.getSimpleName(); + + @Override + public int getOptionsMenu() { + return R.menu.abstract_top_menu; + } + + @Override + public SectionAdapter<InternetRadioStation> getAdapter(List<InternetRadioStation> objs) { + return new InternetRadioStationAdapter(context, objs, this); + } + + @Override + public List<InternetRadioStation> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + return musicService.getInternetRadioStations(refresh, context, listener); + } + + @Override + public int getTitleResource() { + return R.string.button_bar_internet_radio; + } + + @Override + public void onItemClicked(UpdateView<InternetRadioStation> updateView, final InternetRadioStation item) { + new TabBackgroundTask<Void>(this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return null; + } + + getStreamFromPlaylist(item); + downloadService.download(item); + return null; + } + + @Override + protected void done(Void result) { + context.openNowPlaying(); + } + }.execute(); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<InternetRadioStation> updateView, InternetRadioStation item) { + menuInflater.inflate(R.menu.select_internet_radio_context, menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView<InternetRadioStation> updateView, InternetRadioStation item) { + switch (menuItem.getItemId()) { + case R.id.internet_radio_info: + displayInternetRadioStationInfo(item); + break; + } + + return false; + } + + private void getStreamFromPlaylist(InternetRadioStation internetRadioStation) { + if(internetRadioStation.getStreamUrl() != null && (internetRadioStation.getStreamUrl().indexOf(".m3u") != -1 || internetRadioStation.getStreamUrl().indexOf(".pls") != -1)) { + try { + URL url = new URL(internetRadioStation.getStreamUrl()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + try { + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + while((line = in.readLine()) != null) { + // Not blank line or comment + if(line.length() > 0 && line.indexOf('#') != 0) { + if(internetRadioStation.getStreamUrl().indexOf(".m3u") != -1) { + internetRadioStation.setStreamUrl(line); + break; + } else { + if(line.indexOf("File1=") == 0) { + internetRadioStation.setStreamUrl(line.replace("File1=", "")); + } else if(line.indexOf("Title1=") == 0) { + internetRadioStation.setTitle(line.replace("Title1=", "")); + } + } + } + } + } finally { + connection.disconnect(); + } + } catch (Exception e) { + Log.e(TAG, "Failed to get stream data from playlist", e); + } + + } + } + + private void displayInternetRadioStationInfo(final InternetRadioStation station) { + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(station.getTitle()); + + headers.add(R.string.details_home_page); + details.add(station.getHomePageUrl()); + + headers.add(R.string.details_stream_url); + details.add(station.getStreamUrl()); + + Util.showDetailsDialog(context, R.string.details_title_internet_radio_station, headers, details); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java index 6e2c9da5..5cb413fe 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -1,5 +1,8 @@ package github.daneren2005.dsub.fragments; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.support.v7.app.AlertDialog; import android.content.DialogInterface; import android.content.res.Resources; @@ -184,6 +187,22 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { replaceFragment(fragment); } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String playlistId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (playlistId != null && objects != null) { + for (Playlist playlist : objects) { + if (playlistId.equals(playlist.getId())) { + onItemClicked(null, playlist); + break; + } + } + } + } + } + private void deletePlaylist(final Playlist playlist) { Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() { @Override @@ -326,7 +345,24 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { private void syncPlaylist(Playlist playlist) { SyncUtil.addSyncedPlaylist(context, playlist.getId()); - downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true); + + boolean syncImmediately; + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + + if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + syncImmediately = true; + } else { + syncImmediately = false; + } + } else { + syncImmediately = true; + } + + if(syncImmediately) { + downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true); + } } private void stopSyncPlaylist(final Playlist playlist) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java index 3f8f7844..9011b4c5 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -154,25 +154,18 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(newestEpisodes == null || newestEpisodes.getChildrenSize() == 0) { return new PodcastChannelAdapter(context, channels, hasCoverArt ? getImageLoader() : null, this, largeAlbums); } else { - List<String> headers = Arrays.asList(PodcastChannelAdapter.EPISODE_HEADER, PodcastChannelAdapter.CHANNEL_HEADER); + Resources res = context.getResources(); + List<String> headers = Arrays.asList(res.getString(R.string.main_albums_newest), res.getString(R.string.select_podcasts_channels)); List<MusicDirectory.Entry> episodes = newestEpisodes.getChildren(false, true); List<Serializable> serializableEpisodes = new ArrayList<>(); - - // Put 3 in current list - while(serializableEpisodes.size() < 3 && !episodes.isEmpty()) { - serializableEpisodes.add(episodes.remove(0)); - } - - // Put rest in extra set - List<Serializable> extraEpisodes = new ArrayList<>(); - extraEpisodes.addAll(episodes); + serializableEpisodes.addAll(episodes); List<List<Serializable>> sections = new ArrayList<>(); sections.add(serializableEpisodes); sections.add(channels); - return new PodcastChannelAdapter(context, headers, sections, extraEpisodes, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); + return new PodcastChannelAdapter(context, headers, sections, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); } } @@ -182,7 +175,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(!Util.isOffline(context) && ServerInfo.hasNewestPodcastEpisodes(context)) { try { - newestEpisodes = musicService.getNewestPodcastEpisodes(10, context, listener); + newestEpisodes = musicService.getNewestPodcastEpisodes(refresh, context, listener, 10); for(MusicDirectory.Entry entry: newestEpisodes.getChildren()) { for(PodcastChannel channel: channels) { @@ -246,12 +239,12 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> return; } - playNow(Arrays.asList((MusicDirectory.Entry) episode)); + onSongPress(Arrays.asList((MusicDirectory.Entry) episode), episode, false); } } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { @@ -259,7 +252,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(adapter != null) { int viewType = getCurrentAdapter().getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_EPISODE || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_LEGACY) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -270,6 +263,25 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> }; } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String podcastId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (podcastId != null && objects != null) { + for (Serializable ser : objects) { + if (ser instanceof PodcastChannel) { + PodcastChannel podcast = (PodcastChannel) ser; + if (podcastId.equals(podcast.getId())) { + onItemClicked(null, podcast); + break; + } + } + } + } + } + } + public void refreshPodcasts() { new SilentBackgroundTask<Void>(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java index 7ae7fff8..0d4506ac 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java @@ -15,10 +15,15 @@ package github.daneren2005.dsub.fragments; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -102,6 +107,7 @@ public abstract class SelectRecyclerFragment<T> extends SubsonicFragment impleme } menuInflater.inflate(getOptionsMenu(), menu); + onFinishSetupOptionsMenu(menu); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java index cb0e48b9..f231fa33 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java @@ -27,6 +27,7 @@ import android.widget.CompoundButton; import android.widget.DatePicker; import android.widget.EditText; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -104,10 +105,34 @@ public class SelectShareFragment extends SelectRecyclerFragment<Share> { } private void displayShareInfo(final Share share) { - String message = context.getResources().getString(R.string.share_info, - share.getUsername(), (share.getDescription() != null) ? share.getDescription() : "", share.getUrl(), - Util.formatDate(share.getCreated()), Util.formatDate(share.getLastVisited()), Util.formatDate(share.getExpires()), share.getVisitCount()); - Util.info(context, share.getName(), message); + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(share.getName()); + + headers.add(R.string.details_owner); + details.add(share.getUsername()); + + headers.add(R.string.details_description); + details.add(share.getDescription()); + + headers.add(R.string.details_url); + details.add(share.getUrl()); + + headers.add(R.string.details_created); + details.add(Util.formatDate(share.getCreated())); + + headers.add(R.string.details_last_played); + details.add(Util.formatDate(share.getLastVisited())); + + headers.add(R.string.details_expiration); + details.add(Util.formatDate(share.getExpires(), false)); + + headers.add(R.string.details_played_count); + details.add(Long.toString(share.getVisitCount())); + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); } private void updateShareInfo(final Share share) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java index 9853e046..fa2ca340 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java @@ -15,12 +15,14 @@ package github.daneren2005.dsub.fragments; +import android.Manifest; import android.accounts.Account; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; @@ -29,6 +31,8 @@ import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.text.InputType; import android.util.Log; import android.view.LayoutInflater; @@ -47,6 +51,7 @@ import java.util.LinkedHashMap; import java.util.Map; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.activity.SubsonicActivity; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.HeadphoneListenerService; import github.daneren2005.dsub.service.MusicService; @@ -54,8 +59,10 @@ import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; +import github.daneren2005.dsub.util.MediaRouteManager; import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.CacheLocationPreference; import github.daneren2005.dsub.view.ErrorDialog; public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -69,7 +76,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared private ListPreference maxVideoBitrateWifi; private ListPreference maxVideoBitrateMobile; private ListPreference networkTimeout; - private EditTextPreference cacheLocation; + private CacheLocationPreference cacheLocation; private ListPreference preloadCountWifi; private ListPreference preloadCountMobile; private ListPreference keepPlayedCount; @@ -77,6 +84,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared private ListPreference pauseDisconnect; private Preference addServerPreference; private PreferenceCategory serversCategory; + private ListPreference songPressAction; private ListPreference videoPlayer; private ListPreference syncInterval; private CheckBoxPreference syncEnabled; @@ -137,6 +145,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared xml = R.xml.settings_playback; } else if("servers".equals(name)) { xml = R.xml.settings_servers; + } else if ("cast".equals(name)) { + xml = R.xml.settings_cast; } if(xml != 0) { @@ -184,6 +194,25 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared } else { context.stopService(serviceIntent); } + } else if(Constants.PREFERENCES_KEY_THEME.equals(key)) { + String value = sharedPreferences.getString(key, null); + if("day/night".equals(value) || "day/black".equals(value)) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(context, new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION }, SubsonicActivity.PERMISSIONS_REQUEST_LOCATION); + } + } + } else if(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED.equals(key)) { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null) { + MediaRouteManager mediaRouter = downloadService.getMediaRouter(); + + Boolean enabled = sharedPreferences.getBoolean(key, true); + if (enabled) { + mediaRouter.addDLNAProvider(); + } else { + mediaRouter.removeDLNAProvider(); + } + } } scheduleBackup(); @@ -205,7 +234,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI); maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE); networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); - cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); + cacheLocation = (CacheLocationPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); keepPlayedCount = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT); @@ -214,6 +243,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); videoPlayer = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); + songPressAction = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION); syncInterval = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL); syncEnabled = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED); syncWifi = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI); @@ -350,6 +380,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared if(theme != null) { theme.setSummary(theme.getEntry()); + } + if(openToTab != null) { openToTab.setSummary(openToTab.getEntry()); } @@ -379,6 +411,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared keepPlayedCount.setSummary(keepPlayedCount.getEntry()); tempLoss.setSummary(tempLoss.getEntry()); pauseDisconnect.setSummary(pauseDisconnect.getEntry()); + songPressAction.setSummary(songPressAction.getEntry()); videoPlayer.setSummary(videoPlayer.getEntry()); if(replayGain.isChecked()) { @@ -423,6 +456,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared for (ServerSettings ss : serverSettings.values()) { if(!ss.update()) { serversCategory.removePreference(ss.getScreen()); + serverCount--; } } } @@ -633,7 +667,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared } catch(Exception e) { Log.w(TAG, "Failed to create " + musicNoMedia, e); } - } else if (nomediaDir.exists()) { + } else if (!hide && nomediaDir.exists()) { if (!nomediaDir.delete()) { Log.w(TAG, "Failed to delete " + nomediaDir); } @@ -664,8 +698,11 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared SharedPreferences.Editor editor = prefs.edit(); editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); editor.commit(); - cacheLocation.setSummary(defaultPath); - cacheLocation.setText(defaultPath); + + if(cacheLocation != null) { + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } } // Clear download queue. diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java index 93e3a93a..a41b9d6f 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java @@ -15,14 +15,16 @@ package github.daneren2005.dsub.fragments; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import github.daneren2005.dsub.R; -import github.daneren2005.dsub.adapter.ArtistAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; +import github.daneren2005.dsub.adapter.SimilarArtistAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; @@ -35,6 +37,8 @@ import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.UpdateView; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -52,18 +56,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - super.onCreateOptionsMenu(menu, menuInflater); - if(!primaryFragment) { - return; - } - - if(info.getMissingArtists().isEmpty()) { - menu.removeItem(R.id.menu_show_missing); - } - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_play_now: @@ -72,9 +64,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { case R.id.menu_shuffle: playAll(true); return true; - case R.id.menu_show_missing: - showMissingArtists(); - break; } return super.onOptionsItemSelected(item); @@ -82,8 +71,10 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { - onCreateContextMenuSupport(menu, menuInflater, updateView, item); - recreateContextMenu(menu); + if(!Artist.MISSING_ID.equals(item.getId())) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + recreateContextMenu(menu); + } } @Override @@ -93,14 +84,21 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { - SubsonicFragment fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); - - replaceFragment(fragment); + if(Artist.MISSING_ID.equals(artist.getId())) { + String url = "http://www.last.fm/music/" + URLEncoder.encode(artist.getName()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + startActivity(intent); + } else { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment); + } } @Override @@ -109,8 +107,22 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public SectionAdapter getAdapter(List<Artist> objects) { - return new ArtistAdapter(context, objects, this); + public SectionAdapter getAdapter(List<Artist> artists) { + if(info.getMissingArtists().isEmpty()) { + return new SimilarArtistAdapter(context, artists, this); + } else { + List<String> headers = new ArrayList<>(); + headers.add(null); + headers.add(context.getResources().getString(R.string.menu_similar_artists_missing)); + + List<Artist> missingArtists = new ArrayList<>(); + for(String artistName: info.getMissingArtists()) { + Artist artist = new Artist(Artist.MISSING_ID, artistName); + missingArtists.add(artist); + } + + return new SimilarArtistAdapter(context, headers, Arrays.asList(artists, missingArtists), this); + } } @Override @@ -124,16 +136,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { return R.string.menu_similar_artists; } - private void showMissingArtists() { - StringBuilder b = new StringBuilder(); - - for(String name: info.getMissingArtists()) { - b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> "); - } - - Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString()); - } - private void playAll(final boolean shuffle) { new RecursiveLoader(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java index c503ec6c..de230309 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -20,6 +20,9 @@ package github.daneren2005.dsub.fragments; import android.annotation.TargetApi; import android.app.Activity; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -37,6 +40,7 @@ import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.GestureDetector; import android.view.Menu; @@ -125,6 +129,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR protected boolean artistOverride = false; protected SwipeRefreshLayout refreshLayout; protected boolean firstRun; + protected MenuItem searchItem; + protected SearchView searchView; public SubsonicFragment() { super(); @@ -177,15 +183,36 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR this.context = context; } + protected void onFinishSetupOptionsMenu(final Menu menu) { + searchItem = menu.findItem(R.id.menu_global_search); + if(searchItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + SearchManager searchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(context.getComponentName()); + if(searchableInfo == null) { + Log.w(TAG, "Failed to get SearchableInfo"); + } else { + searchView.setSearchableInfo(searchableInfo); + } + + String currentQuery = getCurrentQuery(); + if(currentQuery != null) { + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchView.setQuery(getCurrentQuery(), false); + } + }); + } + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_global_shuffle: onShuffleRequested(); return true; - case R.id.menu_global_search: - context.onSearchRequested(); - return true; case R.id.menu_exit: exit(); return true; @@ -221,6 +248,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR addToPlaylist(songs); clearSelected(); return true; + case R.id.menu_star:case R.id.menu_unstar: + toggleSelectedStarred(); + return true; } return false; @@ -269,7 +299,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menu.removeItem(R.id.menu_rate); } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else if(!entry.isVideo()) { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_song_context_offline, menu); @@ -280,8 +309,13 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if(entry.getBookmark() == null) { menu.removeItem(R.id.bookmark_menu_delete); } + + + String songPressAction = Util.getSongPressAction(context); + if(!"next".equals(songPressAction) && !"last".equals(songPressAction)) { + menu.setGroupVisible(R.id.hide_play_now, false); + } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_video_context_offline, menu); @@ -290,6 +324,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menuInflater.inflate(R.menu.select_video_context, menu); } } + + MenuItem starMenu = menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star); + if(starMenu != null) { + starMenu.setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); + } + + if(!isShowArtistEnabled() || (!Util.isTagBrowsing(context) && entry.getParent() == null) || (Util.isTagBrowsing(context) && entry.getArtistId() == null)) { + menu.setGroupVisible(R.id.hide_show_artist, false); + } } else if(selected instanceof Artist) { Artist artist = (Artist) selected; if(Util.isOffline(context)) { @@ -323,6 +366,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) { Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null; + if(selectedItem instanceof DownloadFile) { + entry = ((DownloadFile) selectedItem).getSong(); + } List<Entry> songs = new ArrayList<Entry>(1); songs.add(entry); @@ -390,6 +436,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR case R.id.album_menu_share: createShare(songs); break; + case R.id.song_menu_play_now: + playNow(songs); + break; case R.id.song_menu_play_next: getDownloadService().download(songs, false, false, true, false); break; @@ -611,7 +660,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }); - refreshLayout.setColorScheme( + refreshLayout.setColorSchemeResources( R.color.holo_blue_light, R.color.holo_orange_light, R.color.holo_green_light, @@ -634,7 +683,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }); - refreshLayout.setColorScheme( + refreshLayout.setColorSchemeResources( R.color.holo_blue_light, R.color.holo_orange_light, R.color.holo_green_light, @@ -656,7 +705,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR final int columns = getRecyclerColumnCount(); GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); - GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); if(spanSizeLookup != null) { gridLayoutManager.setSpanSizeLookup(spanSizeLookup); } @@ -671,15 +720,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR layoutManager.setOrientation(LinearLayoutManager.VERTICAL); return layoutManager; } - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { SectionAdapter adapter = getCurrentAdapter(); if(adapter != null) { - int viewType = getCurrentAdapter().getItemViewType(position); + int viewType = adapter.getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -722,6 +771,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if(downloadService == null) { return; } + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); return; @@ -825,6 +875,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if (downloadService == null) { return; } + + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); } @@ -909,7 +961,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR }.execute(); } - protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append) { + protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append, final boolean playNext) { new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { @@ -937,7 +989,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR downloadService.clear(); } - downloadService.download(songs, false, true, false, false); + downloadService.download(songs, false, true, playNext, false); if(!append) { transition = true; } @@ -965,6 +1017,14 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } protected void addToPlaylist(final List<Entry> songs) { + Iterator<Entry> it = songs.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + if(entry.isDirectory()) { + it.remove(); + } + } + if(songs.isEmpty()) { Util.toast(context, "No songs selected"); return; @@ -1053,7 +1113,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR @Override protected void done(Void result) { - Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName())); + Util.toast(context, context.getResources().getString(R.string.updated_playlist, String.valueOf(songs.size()), playlist.getName())); } @Override @@ -1075,16 +1135,24 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR final EditText playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name); final CheckBox overwriteCheckBox = (CheckBox) layout.findViewById(R.id.save_playlist_overwrite); if(getSuggestion) { - String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null; + DownloadService downloadService = getDownloadService(); + String playlistName = null; + String playlistId = null; + if(downloadService != null) { + playlistName = downloadService.getSuggestedPlaylistName(); + playlistId = downloadService.getSuggestedPlaylistId(); + } if (playlistName != null) { playlistNameView.setText(playlistName); - try { - if(ServerInfo.checkServerVersion(context, "1.8.0") && Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) { - overwriteCheckBox.setChecked(true); - overwriteCheckBox.setVisibility(View.VISIBLE); + if(playlistId != null) { + try { + if (ServerInfo.checkServerVersion(context, "1.8.0") && Integer.parseInt(playlistId) != -1) { + overwriteCheckBox.setChecked(true); + overwriteCheckBox.setVisibility(View.VISIBLE); + } + } catch (Exception e) { + Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet", e); } - } catch(Exception e) { - Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet"); } } else { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); @@ -1145,6 +1213,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR @Override protected void error(Throwable error) { String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); + Log.e(TAG, "Failed to create playlist", error); Util.toast(context, msg); } }.execute(); @@ -1174,6 +1243,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); } + Log.e(TAG, "Failed to overwrite playlist", error); Util.toast(context, msg, false); } }.execute(); @@ -1572,7 +1642,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR .setPositiveButton(R.string.bookmark_action_resume, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - playNow(songs, song, position); + playNow(songs, song, position, playlistName, playlistId); } }) .setNegativeButton(R.string.bookmark_action_start_over, new DialogInterface.OnClickListener() { @@ -1605,13 +1675,40 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); - playNow(songs, 0); + playNow(songs, 0, playlistName, playlistId); } }); AlertDialog dialog = builder.create(); dialog.show(); } + protected void onSongPress(List<Entry> entries, Entry entry) { + onSongPress(entries, entry, 0, true); + } + protected void onSongPress(List<Entry> entries, Entry entry, boolean allowPlayAll) { + onSongPress(entries, entry, 0, allowPlayAll); + } + protected void onSongPress(List<Entry> entries, Entry entry, int position, boolean allowPlayAll) { + List<Entry> songs = new ArrayList<Entry>(); + + String songPressAction = Util.getSongPressAction(context); + if("all".equals(songPressAction) && allowPlayAll) { + for(Entry song: entries) { + if(!song.isDirectory() && !song.isVideo()) { + songs.add(song); + } + } + playNow(songs, entry, position); + } else if("next".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, true, false); + } else if("last".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, false, false); + } else { + songs.add(entry); + playNow(songs); + } + } + protected void playNow(List<Entry> entries) { playNow(entries, null, null); } @@ -1659,15 +1756,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR new LoadingTask<Void>(context) { @Override protected Void doInBackground() throws Throwable { - DownloadService downloadService = getDownloadService(); - if(downloadService == null) { - return null; - } - - downloadService.clear(); - downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); - downloadService.setSuggestedPlaylistName(playlistName, playlistId); - + playNowInTask(entries, song, position, playlistName, playlistId); return null; } @@ -1677,6 +1766,19 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position) { + playNowInTask(entries, song, position, null, null); + } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position, final String playlistName, final String playlistId) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } protected void deleteBookmark(final MusicDirectory.Entry entry, final SectionAdapter adapter) { Util.confirmDialog(context, R.string.bookmark_delete_title, entry.getTitle(), new DialogInterface.OnClickListener() { @@ -1691,7 +1793,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.deleteBookmark(entry, context, null); - new UpdateHelper.EntryInstanceUpdater(entry) { + new UpdateHelper.EntryInstanceUpdater(entry, DownloadService.METADATA_UPDATED_BOOKMARK) { @Override public void update(Entry found) { found.setBookmark(null); @@ -1898,6 +2000,18 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } + protected void toggleSelectedStarred() { + UpdateHelper.toggleStarred(context, getSelectedEntries()); + } + + protected boolean isShowArtistEnabled() { + return false; + } + + protected String getCurrentQuery() { + return null; + } + public abstract class RecursiveLoader extends LoadingTask<Boolean> { protected MusicService musicService; protected static final int MAX_SONGS = 500; @@ -1909,6 +2023,23 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR musicService = MusicServiceFactory.getMusicService(context); } + protected void getSiblingsRecursively(Entry entry) throws Exception { + MusicDirectory parent = new MusicDirectory(); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + parent.setId(entry.getAlbumId()); + } else { + parent.setId(entry.getParent()); + } + + if(parent.getId() == null) { + songs.add(entry); + } else { + MusicDirectory.Entry dir = new Entry(parent.getId()); + dir.setDirectory(true); + parent.addChild(dir); + getSongsRecursively(parent, songs); + } + } protected void getSongsRecursively(List<Entry> entry) throws Exception { getSongsRecursively(entry, false); } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java index dab104bd..dde76624 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java @@ -20,6 +20,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import github.daneren2005.dsub.R; @@ -78,7 +80,7 @@ public class UserFragment extends SelectRecyclerFragment<User.Setting>{ @Override public SectionAdapter<User.Setting> getAdapter(List<User.Setting> objs) { - return new SettingsAdapter(context, user, getImageLoader(), UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), this); + return SettingsAdapter.getSettingsAdapter(context, user, getImageLoader(), this); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java index c5632362..0ee16723 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java @@ -71,6 +71,7 @@ public class DLNARouteProvider extends MediaRouteProvider { private List<String> removing = new ArrayList<String>(); private AndroidUpnpService dlnaService; private ServiceConnection dlnaServiceConnection; + private RegistryListener registryListener; private boolean searchOnConnect = false; public DLNARouteProvider(Context context) { @@ -84,7 +85,7 @@ public class DLNARouteProvider extends MediaRouteProvider { @Override public void onServiceConnected(ComponentName name, IBinder service) { dlnaService = (AndroidUpnpService) service; - dlnaService.getRegistry().addListener(new RegistryListener() { + dlnaService.getRegistry().addListener(registryListener = new RegistryListener() { @Override public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) { @@ -142,6 +143,7 @@ public class DLNARouteProvider extends MediaRouteProvider { @Override public void onServiceDisconnected(ComponentName name) { dlnaService = null; + registryListener = null; } }; @@ -166,7 +168,7 @@ public class DLNARouteProvider extends MediaRouteProvider { DLNADevice device = deviceEntry.getValue(); int volume; - if(device.volumeMax == 0) { + if(device.volumeMax <= 0) { volume = 5; } else { int increments = (int) Math.ceil(device.volumeMax / 10.0); @@ -290,6 +292,17 @@ public class DLNARouteProvider extends MediaRouteProvider { } } + public void destroy() { + if(dlnaService != null) { + dlnaService.getRegistry().removeListener(registryListener); + registryListener = null; + } + + if(dlnaServiceConnection != null) { + getContext().getApplicationContext().unbindService(dlnaServiceConnection); + } + } + private class DLNARouteController extends RouteController { private DLNADevice device; diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java index f91c364e..ba8c80c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java @@ -58,6 +58,10 @@ public class DSubSearchProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if(selectionArgs[0].isEmpty()) { + return null; + } + String query = selectionArgs[0] + "*"; SearchResult searchResult = search(query); return createCursor(selectionArgs[0], searchResult); diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java index 18660fa2..5c90c250 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java @@ -278,7 +278,7 @@ public class DSubWidgetProvider extends AppWidgetProvider { private void linkButtons(Context context, RemoteViews views, boolean playerActive) { Intent intent = new Intent(context, SubsonicFragmentActivity.class); intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); diff --git a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java index d579ef54..35f6d37a 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java @@ -21,6 +21,7 @@ package github.daneren2005.dsub.service; import android.annotation.TargetApi; import android.content.Intent; import android.media.MediaDescription; +import android.media.MediaMetadata; import android.media.browse.MediaBrowser; import android.os.Build; import android.os.Bundle; @@ -33,7 +34,14 @@ import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; +import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -48,8 +56,14 @@ public class AutoMediaBrowserService extends MediaBrowserService { private static final String BROWSER_ALBUM_LISTS = "albumLists"; private static final String BROWSER_LIBRARY = "library"; private static final String BROWSER_PLAYLISTS = "playlists"; + private static final String BROWSER_PODCASTS = "podcasts"; + private static final String BROWSER_BOOKMARKS = "bookmarks"; private static final String PLAYLIST_PREFIX = "pl-"; + private static final String PODCAST_PREFIX = "po-"; private static final String ALBUM_TYPE_PREFIX = "ty-"; + private static final String MUSIC_DIRECTORY_PREFIX = "md-"; + private static final String MUSIC_FOLDER_PREFIX = "mf-"; + private static final String MUSIC_DIRECTORY_CONTENTS_PREFIX = "mdc-"; private DownloadService downloadService; private Handler handler = new Handler(); @@ -76,12 +90,29 @@ public class AutoMediaBrowserService extends MediaBrowserService { } else if(parentId.startsWith(ALBUM_TYPE_PREFIX)) { int id = Integer.valueOf(parentId.substring(ALBUM_TYPE_PREFIX.length())); getAlbumList(result, id); + } else if(parentId.startsWith(MUSIC_DIRECTORY_PREFIX)) { + String id = parentId.substring(MUSIC_DIRECTORY_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_ID); } else if(BROWSER_LIBRARY.equals(parentId)) { getLibrary(result); + } else if(parentId.startsWith(MUSIC_FOLDER_PREFIX)) { + String id = parentId.substring(MUSIC_FOLDER_PREFIX.length()); + getIndexes(result, id); + } else if(parentId.startsWith(MUSIC_DIRECTORY_CONTENTS_PREFIX)) { + String id = parentId.substring(MUSIC_DIRECTORY_CONTENTS_PREFIX.length()); + getMusicDirectory(result, id); } else if(BROWSER_PLAYLISTS.equals(parentId)) { getPlaylists(result); } else if(parentId.startsWith(PLAYLIST_PREFIX)) { - getPlayOptions(result, parentId.substring(PLAYLIST_PREFIX.length()), Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + String id = parentId.substring(PLAYLIST_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + } else if(BROWSER_PODCASTS.equals(parentId)) { + getPodcasts(result); + } else if(parentId.startsWith(PODCAST_PREFIX)) { + String id = parentId.substring(PODCAST_PREFIX.length()); + getPodcastEpisodes(result, id); + } else if(BROWSER_BOOKMARKS.equals(parentId)) { + getBookmarks(result); } else { // No idea what it is, send empty result result.sendResult(new ArrayList<MediaBrowser.MediaItem>()); @@ -91,7 +122,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { private void getRootFolders(Result<List<MediaBrowser.MediaItem>> result) { List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); - /*MediaDescription.Builder albumLists = new MediaDescription.Builder(); + MediaDescription.Builder albumLists = new MediaDescription.Builder(); albumLists.setTitle(downloadService.getString(R.string.main_albums_title)) .setMediaId(BROWSER_ALBUM_LISTS); mediaItems.add(new MediaBrowser.MediaItem(albumLists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); @@ -99,13 +130,27 @@ public class AutoMediaBrowserService extends MediaBrowserService { MediaDescription.Builder library = new MediaDescription.Builder(); library.setTitle(downloadService.getString(R.string.button_bar_browse)) .setMediaId(BROWSER_LIBRARY); - mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));*/ + mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); MediaDescription.Builder playlists = new MediaDescription.Builder(); playlists.setTitle(downloadService.getString(R.string.button_bar_playlists)) .setMediaId(BROWSER_PLAYLISTS); mediaItems.add(new MediaBrowser.MediaItem(playlists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_podcasts)) + .setMediaId(BROWSER_PODCASTS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_bookmarks)) + .setMediaId(BROWSER_BOOKMARKS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + result.sendResult(mediaItems); } @@ -113,15 +158,10 @@ public class AutoMediaBrowserService extends MediaBrowserService { List<Integer> albums = new ArrayList<>(); albums.add(R.string.main_albums_newest); albums.add(R.string.main_albums_random); - if(ServerInfo.checkServerVersion(downloadService, "1.8")) { - albums.add(R.string.main_albums_alphabetical); - } if(!Util.isTagBrowsing(downloadService)) { albums.add(R.string.main_albums_highest); } - // albums.add(R.string.main_albums_starred); - // albums.add(R.string.main_albums_genres); - // albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_starred); albums.add(R.string.main_albums_recent); albums.add(R.string.main_albums_frequent); @@ -138,12 +178,158 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.sendResult(mediaItems); } - private void getAlbumList(Result<List<MediaBrowser.MediaItem>> result, int id) { + private void getAlbumList(final Result<List<MediaBrowser.MediaItem>> result, final int id) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + String albumListType; + switch(id) { + case R.string.main_albums_newest: + albumListType = "newest"; + break; + case R.string.main_albums_random: + albumListType = "random"; + break; + case R.string.main_albums_highest: + albumListType = "highest"; + break; + case R.string.main_albums_starred: + albumListType = "starred"; + break; + case R.string.main_albums_recent: + albumListType = "recent"; + break; + case R.string.main_albums_frequent: + albumListType = "frequent"; + break; + default: + albumListType = "newest"; + } + + return musicService.getAlbumList(albumListType, 20, 0, true, downloadService, null); + } + + @Override + protected void done(MusicDirectory albumSet) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry album: albumSet.getChildren(true, false)) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(album.getAlbumDisplay()) + .setSubtitle(album.getArtist()) + .setMediaId(MUSIC_DIRECTORY_PREFIX + album.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); } - private void getLibrary(Result<List<MediaBrowser.MediaItem>> result) { + private void getLibrary(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<List<MusicFolder>>(downloadService) { + @Override + protected List<MusicFolder> doInBackground(MusicService musicService) throws Throwable { + return musicService.getMusicFolders(false, downloadService, null); + } + + @Override + protected void done(List<MusicFolder> folders) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(MusicFolder folder: folders) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(folder.getName()) + .setMediaId(MUSIC_FOLDER_PREFIX + folder.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getIndexes(final Result<List<MediaBrowser.MediaItem>> result, final String musicFolderId) { + new SilentServiceTask<Indexes>(downloadService) { + @Override + protected Indexes doInBackground(MusicService musicService) throws Throwable { + return musicService.getIndexes(musicFolderId, false, downloadService, null); + } + + @Override + protected void done(Indexes indexes) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + // music directories + for(Artist artist : indexes.getArtists()) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(artist.getName()) + .setMediaId(MUSIC_DIRECTORY_CONTENTS_PREFIX + artist.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + // music files + for(Entry entry: indexes.getEntries()) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setMediaId(MUSIC_DIRECTORY_PREFIX + entry.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + + private void getMusicDirectory(final Result<List<MediaBrowser.MediaItem>> result, final String musicDirectoryId) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getMusicDirectory(musicDirectoryId, "", false, downloadService, null); + } + + @Override + protected void done(MusicDirectory directory) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + addPlayOptions(mediaItems, musicDirectoryId, Constants.INTENT_EXTRA_NAME_ID); + + for(Entry entry : directory.getChildren()) { + MediaDescription description; + if (entry.isDirectory()) { + // browse deeper + description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setMediaId(MUSIC_DIRECTORY_CONTENTS_PREFIX + entry.getId()) + .build(); + } else { + // playback options for a single item + description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setMediaId(MUSIC_DIRECTORY_PREFIX + entry.getId()) + .build(); + } + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); } private void getPlaylists(final Result<List<MediaBrowser.MediaItem>> result) { @@ -172,9 +358,101 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.detach(); } - private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) { - List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + private void getPodcasts(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<List<PodcastChannel>>(downloadService) { + @Override + protected List<PodcastChannel> doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastChannels(false, downloadService, null); + } + + @Override + protected void done(List<PodcastChannel> podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(PodcastChannel podcast: podcasts) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getName()) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPodcastEpisodes(final Result<List<MediaBrowser.MediaItem>> result, final String podcastId) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastEpisodes(false, podcastId, downloadService, null); + } + + @Override + protected void done(MusicDirectory podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: podcasts.getChildren(false, true)) { + PodcastEpisode podcast = (PodcastEpisode) entry; + Bundle podcastExtras = new Bundle(); + podcastExtras.putSerializable(Constants.INTENT_EXTRA_ENTRY, podcast); + podcastExtras.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, podcast.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getTitle()) + .setSubtitle(Util.formatDate(downloadService, podcast.getDate(), false)) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .setExtras(podcastExtras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + + private void getBookmarks(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getBookmarks(false, downloadService, null); + } + + @Override + protected void done(MusicDirectory bookmarkList) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: bookmarkList.getChildren(false, true)) { + Bundle extras = new Bundle(); + extras.putSerializable(Constants.INTENT_EXTRA_ENTRY, entry); + extras.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setSubtitle(Util.formatDuration(entry.getBookmark().getPosition() / 1000)) + .setMediaId(entry.getId()) + .setExtras(extras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + + private void addPlayOptions(List<MediaBrowser.MediaItem> mediaItems, String id, String idConstant) { Bundle playAllExtras = new Bundle(); playAllExtras.putString(idConstant, id); @@ -194,7 +472,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { .setExtras(shuffleExtras); mediaItems.add(new MediaBrowser.MediaItem(shuffle.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); - /*Bundle playLastExtras = new Bundle(); + Bundle playLastExtras = new Bundle(); playLastExtras.putString(idConstant, id); playLastExtras.putBoolean(Constants.INTENT_EXTRA_PLAY_LAST, true); @@ -202,7 +480,13 @@ public class AutoMediaBrowserService extends MediaBrowserService { playLast.setTitle(downloadService.getString(R.string.menu_play_last)) .setMediaId("playLast-" + id) .setExtras(playLastExtras); - mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));*/ + mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + addPlayOptions(mediaItems, id, idConstant); result.sendResult(mediaItems); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java index 53433f5c..9fd26fe5 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java @@ -20,6 +20,7 @@ package github.daneren2005.dsub.service; import java.io.File; import java.io.IOException; +import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -27,8 +28,6 @@ import java.util.List; import java.util.ListIterator; import java.util.concurrent.TimeUnit; -import org.apache.http.HttpResponse; - import android.content.Context; import android.graphics.Bitmap; import android.util.Log; @@ -39,6 +38,7 @@ import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.PlayerQueue; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.RemoteStatus; @@ -69,6 +69,9 @@ public class CachedMusicService implements MusicService { private static final int MUSIC_DIR_CACHE_SIZE = 20; private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes + public static final int CACHE_UPDATE_LIST = 1; + public static final int CACHE_UPDATE_METADATA = 2; + private static final int CACHED_LAST_FM = 24 * 60; private final RESTMusicService musicService; private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS); @@ -121,11 +124,13 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class); } - + if(result == null) { result = musicService.getMusicFolders(refresh, context, progressListener); FileUtil.serialize(context, new ArrayList<MusicFolder>(result), getCacheName(context, "musicFolders")); } + + MusicFolder.sort(result); cachedMusicFolders.set(result); } return result; @@ -150,7 +155,7 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, name, Indexes.class); } - + if(result == null) { result = musicService.getIndexes(musicFolderId, refresh, context, progressListener); FileUtil.serialize(context, result, name); @@ -169,12 +174,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getMusicDirectory(id, name, true, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); return null; @@ -185,7 +191,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -201,7 +210,7 @@ public class CachedMusicService implements MusicService { dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); - + // If a cached copy exists to check against, look for removes deleteRemovedEntries(context, dir, cached); } @@ -234,7 +243,7 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); } } } @@ -267,12 +276,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getAlbum(id, name, refresh, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "album", id)); return null; @@ -283,7 +293,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -656,6 +669,11 @@ public class CachedMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongList(type, size, offset, context, progressListener); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { return musicService.getRandomSongs(size, artistId, context, progressListener); } @@ -714,11 +732,16 @@ public class CachedMusicService implements MusicService { @Override public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { - return musicService.getCoverArt(context, entry, size, progressListener, task); + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); + if (bitmap != null) { + return bitmap; + } else { + return musicService.getCoverArt(context, entry, size, progressListener, task); + } } @Override - public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + public HttpURLConnection getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task); } @@ -905,8 +928,18 @@ public class CachedMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - return musicService.getNewestPodcastEpisodes(count, context, progressListener); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + MusicDirectory result = null; + + String cacheName = getCacheName(context, "newestPodcastEpisodes"); + try { + result = musicService.getNewestPodcastEpisodes(refresh, context, progressListener, count); + FileUtil.serialize(context, result, cacheName); + } catch(IOException e) { + result = FileUtil.deserialize(context, cacheName, MusicDirectory.class, 24); + } finally { + return result; + } } @Override @@ -1128,7 +1161,12 @@ public class CachedMusicService implements MusicService { @Override public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { - return musicService.getAvatar(username, size, context, progressListener, task); + Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size); + if(bitmap != null) { + return bitmap; + } else { + return musicService.getAvatar(username, size, context, progressListener, task); + } } @Override @@ -1136,12 +1174,22 @@ public class CachedMusicService implements MusicService { String cacheName = getCacheName(context, "artistInfo", id); ArtistInfo info = null; if(!refresh) { - info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class, CACHED_LAST_FM); } if(info == null && allowNetwork) { - info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); - FileUtil.serialize(context, info, cacheName); + try { + info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); + FileUtil.serialize(context, info, cacheName); + } catch(Exception e) { + Log.w(TAG, "Failed to refresh Artist Info"); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + + // Nothing ever cached, throw error further upstream + if(info == null) { + throw e; + } + } } return info; @@ -1149,7 +1197,12 @@ public class CachedMusicService implements MusicService { @Override public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { - return musicService.getBitmap(url, size, context, progressListener, task); + Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size); + if(bitmap != null) { + return bitmap; + } else { + return musicService.getBitmap(url, size, context, progressListener, task); + } } @Override @@ -1180,6 +1233,22 @@ public class CachedMusicService implements MusicService { } @Override + public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List<InternetRadioStation> result = null; + + if(!refresh) { + result = FileUtil.deserialize(context, getCacheName(context, "internetRadioStations"), ArrayList.class); + } + + if(result == null) { + result = musicService.getInternetRadioStations(refresh, context, progressListener); + FileUtil.serialize(context, new ArrayList<>(result), getCacheName(context, "internetRadioStations")); + } + + return result; + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ return musicService.processOfflineSyncs(context, progressListener); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java index 670ea7d2..f9e2bfb1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java @@ -23,6 +23,7 @@ import android.util.Log; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaStatus; @@ -41,6 +42,7 @@ import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.EnvironmentVariables; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.util.compat.CastCompat; @@ -62,18 +64,15 @@ public class ChromeCastController extends RemoteController { private boolean error = false; private boolean ignoreNextPaused = false; private String sessionId; + private boolean isStopping = false; + private Runnable afterUpdateComplete = null; - private ServerProxy proxy; - private String rootLocation; private RemoteMediaPlayer mediaPlayer; private double gain = 0.5; public ChromeCastController(DownloadService downloadService, CastDevice castDevice) { - this.downloadService = downloadService; + super(downloadService); this.castDevice = castDevice; - - SharedPreferences prefs = Util.getPreferences(downloadService); - rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); } @Override @@ -247,66 +246,44 @@ public class ChromeCastController extends RemoteController { } } - void startSong(DownloadFile currentPlaying, boolean autoStart, int position) { + void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { if(currentPlaying == null) { try { - if (mediaPlayer != null && !error) { - mediaPlayer.stop(apiClient); + if (mediaPlayer != null && !error && !isStopping) { + isStopping = true; + mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { + isStopping = false; + + if(afterUpdateComplete != null) { + afterUpdateComplete.run(); + afterUpdateComplete = null; + } + } + }); } } catch(Exception e) { // Just means it didn't need to be stopped } downloadService.setPlayerState(PlayerState.IDLE); return; + } else if(isStopping) { + afterUpdateComplete = new Runnable() { + @Override + public void run() { + startSong(currentPlaying, autoStart, position); + } + }; + return; } + downloadService.setPlayerState(PlayerState.PREPARING); MusicDirectory.Entry song = currentPlaying.getSong(); try { MusicService musicService = MusicServiceFactory.getMusicService(downloadService); - String url; - // Offline, use file proxy - if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) { - if(proxy == null) { - proxy = new FileProxy(downloadService); - proxy.start(); - } - - // Offline song - if(song.getId().indexOf(rootLocation) != -1) { - url = proxy.getPublicAddress(song.getId()); - } else { - // Playing online song in offline mode - url = proxy.getPublicAddress(currentPlaying.getCompleteFile().getPath()); - } - } else { - // Check if we want a proxy going still - if(Util.isCastProxy(downloadService)) { - if(proxy instanceof FileProxy) { - proxy.stop(); - proxy = null; - } - - if(proxy == null) { - proxy = createWebProxy(); - proxy.start(); - } - } else if(proxy != null) { - proxy.stop(); - proxy = null; - } - - if(song.isVideo()) { - url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService); - } else { - url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate()); - } - - // If proxy is going, it is a WebProxy - if(proxy != null) { - url = proxy.getPublicAddress(url); - } - } + String url = getStreamUrl(musicService, currentPlaying); // Setup song/video information MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK); @@ -367,6 +344,8 @@ public class ChromeCastController extends RemoteController { public void onResult(RemoteMediaPlayer.MediaChannelResult result) { if (result.getStatus().isSuccess()) { // Handled in other handler + } else if(result.getStatus().getStatusCode() == CastStatusCodes.REPLACED) { + Log.w(TAG, "Request was replaced: " + currentPlaying.toString()); } else { Log.e(TAG, "Failed to load: " + result.getStatus().toString()); failedLoad(); @@ -443,14 +422,14 @@ public class ChromeCastController extends RemoteController { void launchApplication() { try { - Cast.CastApi.launchApplication(apiClient, CastCompat.APPLICATION_ID, false).setResultCallback(resultCallback); + Cast.CastApi.launchApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, false).setResultCallback(resultCallback); } catch (Exception e) { Log.e(TAG, "Failed to launch application", e); } } void reconnectApplication() { try { - Cast.CastApi.joinApplication(apiClient, CastCompat.APPLICATION_ID, sessionId).setResultCallback(resultCallback); + Cast.CastApi.joinApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, sessionId).setResultCallback(resultCallback); } catch (Exception e) { Log.e(TAG, "Failed to reconnect application", e); } @@ -483,7 +462,9 @@ public class ChromeCastController extends RemoteController { break; case MediaStatus.PLAYER_STATE_IDLE: if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) { - downloadService.onSongCompleted(); + if(downloadService.getPlayerState() != PlayerState.PREPARING) { + downloadService.onSongCompleted(); + } } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) { if (downloadService.getPlayerState() != PlayerState.PREPARING) { downloadService.setPlayerState(PlayerState.PREPARING); @@ -503,7 +484,7 @@ public class ChromeCastController extends RemoteController { try { Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); - } catch (IOException e) { + } catch (Exception e) { Log.e(TAG, "Exception while creating channel", e); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java index 0673cdeb..143be330 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java @@ -83,9 +83,6 @@ public class DLNAController extends RemoteController { SubscriptionCallback callback; boolean supportsSeek = false; boolean supportsSetupNext = false; - - private ServerProxy proxy; - String rootLocation = ""; boolean error = false; final AtomicLong lastUpdate = new AtomicLong(); @@ -108,12 +105,9 @@ public class DLNAController extends RemoteController { }; public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) { - this.downloadService = downloadService; + super(downloadService); this.controlPoint = controlPoint; this.device = device; - - SharedPreferences prefs = Util.getPreferences(downloadService); - rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); nextSupported = true; } @@ -163,7 +157,10 @@ public class DLNAController extends RemoteController { protected void eventReceived(GENASubscription genaSubscription) { Map<String, StateVariableValue> m = genaSubscription.getCurrentValues(); try { - LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString()); + String lastChangeText = m.get("LastChange").toString(); + lastChangeText = lastChangeText.replace(",X_DLNA_SeekTime","").replace(",X_DLNA_SeekByte", ""); + LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeText); + if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) { return; } @@ -470,49 +467,7 @@ public class DLNAController extends RemoteController { // Get url for entry MusicService musicService = MusicServiceFactory.getMusicService(downloadService); - String url; - // In offline mode or playing offline song - if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) { - if(proxy == null) { - proxy = new FileProxy(downloadService); - proxy.start(); - } - - // Offline song - if(song.getId().indexOf(rootLocation) != -1) { - url = proxy.getPublicAddress(song.getId()); - } else { - // Playing online song in offline mode - url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath()); - } - } else { - // Check if we want a proxy going still - if(Util.isCastProxy(downloadService)) { - if(proxy instanceof FileProxy) { - proxy.stop(); - proxy = null; - } - - if(proxy == null) { - proxy = createWebProxy(); - proxy.start(); - } - } else if(proxy != null) { - proxy.stop(); - proxy = null; - } - - if(song.isVideo()) { - url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService); - } else { - url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate()); - } - - // If proxy is going, it is a WebProxy - if(proxy != null) { - url = proxy.getPublicAddress(url); - } - } + String url = getStreamUrl(musicService, downloadFile); // Create metadata for entry Item track; @@ -582,7 +537,7 @@ public class DLNAController extends RemoteController { Log.w(TAG, "Metadata generation failed", e); } - return new Pair<String, String>(url, metadata); + return new Pair<>(url, metadata); } private void failedLoad() { diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java index 3febfaea..30e06982 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java @@ -24,11 +24,14 @@ import java.io.IOException; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; import android.content.Context; import android.net.wifi.WifiManager; import android.os.PowerManager; import android.util.Log; + +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -37,15 +40,6 @@ import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.util.CacheCleaner; import github.daneren2005.serverproxy.BufferFile; -import org.apache.http.Header; - -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; - -/** - * @author Sindre Mehus - * @version $Id$ - */ public class DownloadFile implements BufferFile { private static final String TAG = DownloadFile.class.getSimpleName(); private static final int MAX_FAILURES = 5; @@ -84,6 +78,9 @@ public class DownloadFile implements BufferFile { public MusicDirectory.Entry getSong() { return song; } + public boolean isSong() { + return song.isSong(); + } public Context getContext() { return context; @@ -111,7 +108,7 @@ public class DownloadFile implements BufferFile { } } else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) { // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs) - if(song.getBitRate() != null && br > song.getBitRate()) { + if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) { br = song.getBitRate(); } } @@ -374,11 +371,37 @@ public class DownloadFile implements BufferFile { } } + public boolean isStream() { + return song != null && song instanceof InternetRadioStation; + } + public String getStream() { + if(song != null && song instanceof InternetRadioStation) { + InternetRadioStation station = (InternetRadioStation) song; + return station.getStreamUrl(); + } else { + return null; + } + } + @Override public String toString() { return "DownloadFile (" + song + ")"; } + // Don't do this. Causes infinite loop if two instances of same song + /*@Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DownloadFile downloadFile = (DownloadFile) o; + return Util.equals(this.getSong(), downloadFile.getSong()); + }*/ + private class DownloadTask extends SilentBackgroundTask<Void> { private MusicService musicService; @@ -436,17 +459,15 @@ public class DownloadFile implements BufferFile { } if(compare) { // Attempt partial HTTP GET, appending to the file if it exists. - HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this); - Header contentLengthHeader = response.getFirstHeader("Content-Length"); - if(contentLengthHeader != null) { - String contentLengthString = contentLengthHeader.getValue(); - if(contentLengthString != null) { - Log.i(TAG, "Content Length: " + contentLengthString); - contentLength = Long.parseLong(contentLengthString); - } + HttpURLConnection connection = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this); + long contentLength = connection.getContentLength(); + if(contentLength > 0) { + Log.i(TAG, "Content Length: " + contentLength); + DownloadFile.this.contentLength = contentLength; } - in = response.getEntity().getContent(); - boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT; + + in = connection.getInputStream(); + boolean partial = connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL; if (partial) { Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes"); } @@ -515,8 +536,9 @@ public class DownloadFile implements BufferFile { } // Only run these if not interrupted, ie: cancelled - if(!isCancelled()) { - new CacheCleaner(context, DownloadService.getInstance()).cleanSpace(); + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !isCancelled()) { + new CacheCleaner(context, downloadService).cleanSpace(); checkDownloads(); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java index 6641d040..8213a7d4 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java @@ -35,6 +35,7 @@ import github.daneren2005.dsub.activity.SubsonicActivity; import github.daneren2005.dsub.audiofx.AudioEffectsController; import github.daneren2005.dsub.audiofx.EqualizerController; import github.daneren2005.dsub.domain.Bookmark; +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.PodcastEpisode; @@ -60,6 +61,7 @@ import github.daneren2005.serverproxy.BufferProxy; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; @@ -76,6 +78,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.MediaPlayer; +import android.media.PlaybackParams; import android.media.audiofx.AudioEffect; import android.net.wifi.WifiManager; import android.os.Build; @@ -87,6 +90,7 @@ import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.util.Log; import android.support.v4.util.LruCache; +import android.view.KeyEvent; /** * @author Sindre Mehus @@ -105,6 +109,7 @@ public class DownloadService extends Service { public static final String START_PLAY = "github.daneren2005.dsub.START_PLAYING"; public static final int FAST_FORWARD = 30000; public static final int REWIND = 10000; + private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L; private static final double DELETE_CUTOFF = 0.84; private static final int REQUIRED_ALBUM_MATCHES = 4; private static final int REMOTE_PLAYLIST_TOTAL = 3; @@ -116,10 +121,11 @@ public class DownloadService extends Service { public static final int METADATA_UPDATED_STAR = 1; public static final int METADATA_UPDATED_RATING = 2; public static final int METADATA_UPDATED_BOOKMARK = 4; + public static final int METADATA_UPDATED_COVER_ART = 8; private RemoteControlClientBase mRemoteControl; - private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); + private final IBinder binder = new SimpleServiceBinder<>(this); private Looper mediaPlayerLooper; private MediaPlayer mediaPlayer; private MediaPlayer nextMediaPlayer; @@ -160,6 +166,7 @@ public class DownloadService extends Service { private int cachedPosition = 0; private boolean downloadOngoing = false; private float volume = 1.0f; + private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS; private AudioEffectsController effectsController; private RemoteControlState remoteState = LOCAL; @@ -190,13 +197,16 @@ public class DownloadService extends Service { mediaPlayer = new MediaPlayer(); mediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK); + // We want to change audio session id's between upgrading Android versions. Upgrading to Android 7.0 is broken (probably updated session id format) audioSessionId = -1; - Integer id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1); - if(id != -1) { + int id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1); + int versionCode = prefs.getInt(Constants.CACHE_AUDIO_SESSION_VERSION_CODE, -1); + if(versionCode == Build.VERSION.SDK_INT && id != -1) { try { audioSessionId = id; mediaPlayer.setAudioSessionId(audioSessionId); } catch (Throwable e) { + Log.w(TAG, "Failed to use cached audio session", e); audioSessionId = -1; } } @@ -205,7 +215,11 @@ public class DownloadService extends Service { mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); try { audioSessionId = mediaPlayer.getAudioSessionId(); - prefs.edit().putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId).commit(); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId); + editor.putInt(Constants.CACHE_AUDIO_SESSION_VERSION_CODE, Build.VERSION.SDK_INT); + editor.commit(); } catch (Throwable t) { // Froyo or lower } @@ -219,14 +233,14 @@ public class DownloadService extends Service { } }); - try { + /*try { Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(i); } catch(Throwable e) { // Froyo or lower - } + }*/ effectsController = new AudioEffectsController(DownloadService.this, audioSessionId); if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { @@ -377,6 +391,10 @@ public class DownloadService extends Service { handler.postDelayed(r, millis); } + public synchronized void download(InternetRadioStation station) { + clear(); + download(Arrays.asList((MusicDirectory.Entry) station), false, true, false, false); + } public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { download(songs, save, autoplay, playNext, shuffle, 0, 0); } @@ -389,7 +407,10 @@ public class DownloadService extends Service { if (songs.isEmpty()) { return; + } else if(isCurrentPlayingSingle()) { + clear(); } + if (playNext) { if (autoplay && getCurrentPlayingIndex() >= 0) { offset = 0; @@ -573,7 +594,6 @@ public class DownloadService extends Service { public synchronized void setShufflePlayEnabled(boolean enabled) { shufflePlay = enabled; if (shufflePlay) { - clear(); checkDownloads(); } SharedPreferences.Editor editor = Util.getPreferences(this).edit(); @@ -600,6 +620,9 @@ public class DownloadService extends Service { } editor.commit(); } + public boolean isArtistRadio() { + return artistRadio; + } public synchronized void shuffle() { Collections.shuffle(downloadList); @@ -739,7 +762,7 @@ public class DownloadService extends Service { int position = getPlayerPosition(); int duration = getPlayerDuration(); boolean cutoff = isPastCutoff(position, duration, true); - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { currentPlaying.delete(); } @@ -757,7 +780,7 @@ public class DownloadService extends Service { checkAddBookmark(); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } reset(); @@ -781,6 +804,10 @@ public class DownloadService extends Service { suggestedPlaylistName = null; suggestedPlaylistId = null; + + setShufflePlayEnabled(false); + setArtistRadio(null); + checkDownloads(); } public synchronized void remove(int which) { @@ -807,6 +834,8 @@ public class DownloadService extends Service { if(downloadFile == nextPlaying) { setNextPlaying(); } + + checkDownloads(); } public synchronized void removeBackground(DownloadFile downloadFile) { if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { @@ -843,6 +872,9 @@ public class DownloadService extends Service { if(this.currentPlaying != null) { this.currentPlaying.setPlaying(false); } + if(delayUpdateProgress != DEFAULT_DELAY_UPDATE_PROGRESS && !isNextPlayingSameAlbum(currentPlaying, this.currentPlaying)) { +// resetPlaybackSpeed(); + } this.currentPlaying = currentPlaying; if(currentPlaying == null) { currentPlayingIndex = -1; @@ -853,7 +885,10 @@ public class DownloadService extends Service { if (currentPlaying != null && currentPlaying.getSong() != null) { Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + + if(mRemoteControl != null) { + mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + } } else { Util.broadcastNewTrackInfo(this, null); Notifications.hidePlayingNotification(this, this, handler); @@ -977,6 +1012,25 @@ public class DownloadService extends Service { public List<DownloadFile> getToDelete() { return toDelete; } + public boolean isCurrentPlayingSingle() { + if(currentPlaying != null && currentPlaying.getSong() instanceof InternetRadioStation) { + return true; + } else { + return false; + } + } + public boolean isCurrentPlayingStream() { + if(currentPlaying != null) { + return currentPlaying.isStream(); + } else { + return false; + } + } + + public synchronized boolean shouldFastForward() { + return size() == 1 || (currentPlaying != null && !currentPlaying.isSong()); + } + public synchronized List<DownloadFile> getDownloads() { List<DownloadFile> temp = new ArrayList<DownloadFile>(); temp.addAll(downloadList); @@ -1033,6 +1087,8 @@ public class DownloadService extends Service { bufferAndPlay(position, start); checkDownloads(); setNextPlaying(); + } else { + checkDownloads(); } } } @@ -1070,6 +1126,7 @@ public class DownloadService extends Service { setCurrentPlaying(nextPlaying, true); setPlayerState(PlayerState.STARTED); setupHandlers(currentPlaying, false, start); + applyPlaybackParamsMain(); setNextPlaying(); // Proxy should not be being used here since the next player was already setup to play @@ -1120,6 +1177,27 @@ public class DownloadService extends Service { handleError(x); } } + public synchronized int rewind() { + return seekToWrapper(-REWIND); + } + public synchronized int fastForward() { + return seekToWrapper(FAST_FORWARD); + } + protected int seekToWrapper(int difference) { + int msPlayed = Math.max(0, getPlayerPosition()); + Integer duration = getPlayerDuration(); + int msTotal = duration == null ? 0 : duration; + + int seekTo; + if(msPlayed + difference > msTotal) { + seekTo = msTotal; + } else { + seekTo = msPlayed + difference; + } + seekTo(seekTo); + + return seekTo; + } public synchronized void previous() { int index = getCurrentPlayingIndex(); @@ -1128,12 +1206,13 @@ public class DownloadService extends Service { } // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() - REWIND); + if(shouldFastForward()) { + rewind(); + return; + } else if(playerState == PREPARING || playerState == PREPARED) { return; } - // Restart song if played more than five seconds. if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) { seekTo(0); @@ -1154,8 +1233,8 @@ public class DownloadService extends Service { } public synchronized void next(boolean forceCutoff, boolean forceStart) { // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() + FAST_FORWARD); + if(shouldFastForward()) { + fastForward(); return; } else if(playerState == PREPARING || playerState == PREPARED) { return; @@ -1170,7 +1249,7 @@ public class DownloadService extends Service { } else { cutoff = isPastCutoff(position, duration); } - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { toDelete.add(currentPlaying); } @@ -1179,7 +1258,7 @@ public class DownloadService extends Service { clearCurrentBookmark(true); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } int index = getCurrentPlayingIndex(); @@ -1258,6 +1337,7 @@ public class DownloadService extends Service { // Only start if done preparing if(playerState != PREPARING) { mediaPlayer.start(); + applyPlaybackParamsMain(); } else { // Otherwise, we need to set it up to start when done preparing autoPlayStart = true; @@ -1372,6 +1452,9 @@ public class DownloadService extends Service { if (playerState == PAUSED) { lifecycleSupport.serializeDownloadQueue(); + if(!isPastCutoff()) { + checkAddBookmark(true); + } } boolean show = playerState == PlayerState.STARTED; @@ -1398,13 +1481,13 @@ public class DownloadService extends Service { Notifications.hidePlayingNotification(this, this, handler); } if(mRemoteControl != null) { - mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState(), getCurrentPlayingIndex(), size()); } if (playerState == STARTED) { - scrobbler.scrobble(this, currentPlaying, false); + scrobbler.scrobble(this, currentPlaying, false, false); } else if (playerState == COMPLETED) { - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); } if(playerState == STARTED && positionCache == null) { @@ -1450,7 +1533,7 @@ public class DownloadService extends Service { positionCache.stop(); positionCache = null; } - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); onStateUpdate(); } @@ -1468,7 +1551,7 @@ public class DownloadService extends Service { while(isRunning) { try { onSongProgress(); - Thread.sleep(1000L); + Thread.sleep(delayUpdateProgress); } catch(Exception e) { isRunning = false; @@ -1510,7 +1593,7 @@ public class DownloadService extends Service { } } onSongProgress(cachedPosition < 2000 ? true: false); - Thread.sleep(1000L); + Thread.sleep(delayUpdateProgress); } catch(Exception e) { Log.w(TAG, "Crashed getting current position", e); @@ -1587,6 +1670,9 @@ public class DownloadService extends Service { return controller; } + public MediaRouteManager getMediaRouter() { + return mediaRouter; + } public MediaRouteSelector getRemoteSelector() { return mediaRouter.getSelector(); } @@ -1699,11 +1785,12 @@ public class DownloadService extends Service { nextPlayingTask.cancel(); nextPlayingTask = null; } - } - if(remoteState == LOCAL) { - checkDownloads(); + if(nextPlayerState != IDLE) { + setNextPlayerState(IDLE); + } } + checkDownloads(); if(routeId != null) { final Runnable delayedReconnect = new Runnable() { @@ -1771,7 +1858,7 @@ public class DownloadService extends Service { bufferAndPlay(position, true); } private synchronized void bufferAndPlay(int position, boolean start) { - if(!currentPlaying.isCompleteFileAvailable()) { + if(!currentPlaying.isCompleteFileAvailable() && !currentPlaying.isStream()) { if(Util.isAllowedToDownload(this)) { reset(); @@ -1787,11 +1874,6 @@ public class DownloadService extends Service { private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { try { - downloadFile.setPlaying(true); - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - boolean isPartial = file.equals(downloadFile.getPartialFile()); - downloadFile.updateModificationDate(); - subtractPosition = 0; mediaPlayer.setOnCompletionListener(null); mediaPlayer.setOnPreparedListener(null); @@ -1803,19 +1885,33 @@ public class DownloadService extends Service { } catch(Throwable e) { mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); } - String dataSource = file.getAbsolutePath(); - if(isPartial && !Util.isOffline(this)) { - if (proxy == null) { - proxy = new BufferProxy(this); - proxy.start(); - } - proxy.setBufferFile(downloadFile); - dataSource = proxy.getPrivateAddress(dataSource); + + String dataSource; + boolean isPartial = false; + if(downloadFile.isStream()) { + dataSource = downloadFile.getStream(); Log.i(TAG, "Data Source: " + dataSource); - } else if(proxy != null) { - proxy.stop(); - proxy = null; + } else { + downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + isPartial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + dataSource = file.getAbsolutePath(); + if (isPartial && !Util.isOffline(this)) { + if (proxy == null) { + proxy = new BufferProxy(this); + proxy.start(); + } + proxy.setBufferFile(downloadFile); + dataSource = proxy.getPrivateAddress(dataSource); + Log.i(TAG, "Data Source: " + dataSource); + } else if (proxy != null) { + proxy.stop(); + proxy = null; + } } + mediaPlayer.setDataSource(dataSource); setPlayerState(PREPARING); @@ -1844,6 +1940,7 @@ public class DownloadService extends Service { if (start || autoPlayStart) { mediaPlayer.start(); + applyPlaybackParamsMain(); setPlayerState(STARTED); // Disable autoPlayStart after done @@ -2111,13 +2208,16 @@ public class DownloadService extends Service { checkArtistRadio(); } - if (!Util.isNetworkConnected(this, true) || Util.isOffline(this)) { + if (!Util.isAllowedToDownload(this)) { return; } if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { return; } + if(currentPlaying != null && currentPlaying.isStream()) { + return; + } // Need to download current playing and not casting? if (currentPlaying != null && remoteState == LOCAL && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { @@ -2138,7 +2238,7 @@ public class DownloadService extends Service { int preloaded = 0; - if(n != 0 && remoteState == LOCAL) { + if(n != 0 && (remoteState == LOCAL || Util.shouldCacheDuringCasting(this))) { int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); if(start == -1) { start = 0; @@ -2342,7 +2442,7 @@ public class DownloadService extends Service { } // Make cutoff a maximum of 10 minutes - int cutoffPoint = Math.min((int) (duration * DELETE_CUTOFF), 10 * 60 * 1000); + int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000); boolean isPastCutoff = duration > 0 && position > cutoffPoint; // Check to make sure song isn't within 10 seconds of where it was created @@ -2423,8 +2523,11 @@ public class DownloadService extends Service { }.execute(); } } - + private void checkAddBookmark() { + checkAddBookmark(false); + } + private void checkAddBookmark(final boolean updateMetadata) { // Don't do anything if no current playing if(currentPlaying == null || !ServerInfo.canBookmark(this)) { return; @@ -2454,6 +2557,9 @@ public class DownloadService extends Service { if(found != null) { found.setBookmark(new Bookmark(position)); } + if(updateMetadata) { + onMetadataUpdate(METADATA_UPDATED_BOOKMARK); + } return null; } @@ -2482,9 +2588,9 @@ public class DownloadService extends Service { SharedPreferences prefs = Util.getPreferences(this); try { - float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ float adjust = 0f; if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) { + float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ boolean singleAlbum = false; String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1"); @@ -2558,6 +2664,63 @@ public class DownloadService extends Service { } } + public void setPlaybackSpeed(float playbackSpeed) { + if(currentPlaying.isSong()) + Util.getPreferences(this).edit().putFloat(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED, playbackSpeed).commit(); + else + Util.getPreferences(this).edit().putFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, playbackSpeed).commit(); + if(mediaPlayer != null && (playerState == PREPARED || playerState == STARTED || playerState == PAUSED || playerState == PAUSED_TEMP)) { + applyPlaybackParamsMain(); + } + + delayUpdateProgress = Math.round(DEFAULT_DELAY_UPDATE_PROGRESS / playbackSpeed); + } + private void resetPlaybackSpeed() { + Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_PLAYBACK_SPEED).commit(); + Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED).commit(); + } + + public float getPlaybackSpeed() { + if (currentPlaying == null) + return 1.0f; + else { + if (currentPlaying.isSong()) + return Util.getPreferences(this).getFloat(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED, 1.0f); + else + return Util.getPreferences(this).getFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, 1.0f); + } + } + + private synchronized void applyPlaybackParamsMain() { + applyPlaybackParams(mediaPlayer); + } + private synchronized boolean isNextPlayingSameAlbum() { + return isNextPlayingSameAlbum(currentPlaying, nextPlaying); + } + private synchronized boolean isNextPlayingSameAlbum(DownloadFile currentPlaying, DownloadFile nextPlaying) { + if(currentPlaying == null || nextPlaying == null) { + return false; + } else { + return currentPlaying.getSong().getAlbum().equals(nextPlaying.getSong().getAlbum()); + } + } + + private synchronized void applyPlaybackParams(MediaPlayer mediaPlayer) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + float playbackSpeed = getPlaybackSpeed(); + + try { + if (Math.abs(playbackSpeed - 1.0) > 0.01 || mediaPlayer.getPlaybackParams() != null) { + PlaybackParams playbackParams = new PlaybackParams(); + playbackParams.setSpeed(playbackSpeed); + mediaPlayer.setPlaybackParams(playbackParams); + } + } catch(Exception e) { + Log.e(TAG, "Error while applying media player params", e); + } + } + } + public void toggleStarred() { final DownloadFile currentPlaying = this.currentPlaying; if(currentPlaying == null) { @@ -2571,6 +2734,11 @@ public class DownloadService extends Service { onMetadataUpdate(METADATA_UPDATED_STAR); } } + + @Override + public void starCommited(boolean starred) { + + } }); } public void toggleRating(int rating) { @@ -2600,7 +2768,7 @@ public class DownloadService extends Service { UpdateHelper.setRating(this, entry, rating, new UpdateHelper.OnRatingChange() { @Override public void ratingChange(int rating) { - if(currentPlaying == DownloadService.this.currentPlaying) { + if (currentPlaying == DownloadService.this.currentPlaying) { onMetadataUpdate(METADATA_UPDATED_RATING); } } @@ -2613,13 +2781,19 @@ public class DownloadService extends Service { wakeLock.acquire(ms); } + public void handleKeyEvent(KeyEvent keyEvent) { + lifecycleSupport.handleKeyEvent(keyEvent); + } + public void addOnSongChangedListener(OnSongChangedListener listener) { addOnSongChangedListener(listener, false); } - public synchronized void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { - int index = onSongChangedListeners.indexOf(listener); - if(index == -1) { - onSongChangedListeners.add(listener); + public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index == -1) { + onSongChangedListeners.add(listener); + } } if(run) { @@ -2630,6 +2804,7 @@ public class DownloadService extends Service { onSongsChanged(); onSongProgress(); onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); } }); } else { @@ -2637,53 +2812,61 @@ public class DownloadService extends Service { } } } - public synchronized void removeOnSongChangeListener(OnSongChangedListener listener) { - int index = onSongChangedListeners.indexOf(listener); - if(index != -1) { - onSongChangedListeners.remove(index); + public void removeOnSongChangeListener(OnSongChangedListener listener) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index != -1) { + onSongChangedListeners.remove(index); + } } } - private synchronized void onSongChanged() { + private void onSongChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongChanged(currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + final boolean shouldFastForward = shouldFastForward(); + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward); - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + } } - } - }); - } + }); + } - if(mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { - mediaPlayerHandler.post(new Runnable() { - @Override - public void run() { - onSongProgress(); - } - }); + if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongProgress(); + } + }); + } } } - private synchronized void onSongsChanged() { + private void onSongsChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + final boolean shouldFastForward = shouldFastForward(); + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex, shouldFastForward); + } } - } - }); + }); + } } } - private synchronized void onSongProgress() { + private void onSongProgress() { onSongProgress(true); } private synchronized void onSongProgress(boolean manual) { @@ -2691,22 +2874,29 @@ public class DownloadService extends Service { final Integer duration = getPlayerDuration(); final boolean isSeekable = isSeekable(); final int position = getPlayerPosition(); - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongProgress(currentPlaying, position, duration, isSeekable); + final int index = getCurrentPlayingIndex(); + final int queueSize = size(); + + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongProgress(currentPlaying, position, duration, isSeekable); + } } - } - }); + }); + } } if(manual) { handler.post(new Runnable() { @Override public void run() { - mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + if(mRemoteControl != null) { + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState(), index, queueSize); + } } }); } @@ -2718,39 +2908,45 @@ public class DownloadService extends Service { } } } - private synchronized void onStateUpdate() { + private void onStateUpdate() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onStateUpdate(currentPlaying, playerState); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onStateUpdate(currentPlaying, playerState); + } } - } - }); + }); + } } } - private synchronized void onMetadataUpdate() { + public void onMetadataUpdate() { onMetadataUpdate(METADATA_UPDATED_ALL); } - private synchronized void onMetadataUpdate(final int updateType) { - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(instance != null) { - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, updateType); + public void onMetadataUpdate(final int updateType) { + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (instance != null) { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } } - } - }); + }); + } } handler.post(new Runnable() { @Override public void run() { - mRemoteControl.metadataChanged(currentPlaying.getSong()); + if(currentPlaying != null) { + mRemoteControl.metadataChanged(currentPlaying.getSong()); + } } }); } @@ -2862,8 +3058,8 @@ public class DownloadService extends Service { } public interface OnSongChangedListener { - void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex); - void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex); + void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward); + void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward); void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable); void onStateUpdate(DownloadFile downloadFile, PlayerState playerState); void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange); diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java index 4989db40..1c80b622 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -36,6 +36,8 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.view.KeyEvent; + +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerQueue; import github.daneren2005.dsub.domain.PlayerState; @@ -56,6 +58,7 @@ import static github.daneren2005.dsub.domain.PlayerState.PREPARING; public class DownloadServiceLifecycleSupport { private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); public static final String FILENAME_DOWNLOADS_SER = "downloadstate2.ser"; + private static final int DEBOUNCE_TIME = 200; private final DownloadService downloadService; private Looper eventLooper; @@ -154,6 +157,8 @@ public class DownloadServiceLifecycleSupport { // Pause temporarily on incoming phone calls. phoneStateListener = new MyPhoneStateListener(); + + // Android 6.0 removes requirement for android.Manifest.permission.READ_PHONE_STATE; TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); @@ -225,6 +230,7 @@ public class DownloadServiceLifecycleSupport { } editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { downloadService.start(); @@ -306,7 +312,7 @@ public class DownloadServiceLifecycleSupport { FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); // If we are on Subsonic 5.2+, save play queue - if(serializeRemote && ServerInfo.canSavePlayQueue(downloadService) && !Util.isOffline(downloadService) && state.songs.size() > 0) { + if(serializeRemote && ServerInfo.canSavePlayQueue(downloadService) && !Util.isOffline(downloadService) && state.songs.size() > 0 && !(state.songs.get(0) instanceof InternetRadioStation)) { // Cancel any currently running tasks if(currentSavePlayQueueTask != null) { currentSavePlayQueueTask.cancel(); @@ -384,14 +390,25 @@ public class DownloadServiceLifecycleSupport { return lastChange; } - private void handleKeyEvent(KeyEvent event) { - if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + public void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.fastForward(); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.rewind(); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { switch (event.getKeyCode()) { case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: downloadService.togglePlayPause(); break; case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: if(lastPressTime < (System.currentTimeMillis() - 500)) { lastPressTime = System.currentTimeMillis(); downloadService.togglePlayPause(); @@ -401,11 +418,23 @@ public class DownloadServiceLifecycleSupport { break; case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - downloadService.previous(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.previous(); + } break; case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_NEXT: - downloadService.next(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + downloadService.rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + downloadService.fastForward(); break; case RemoteControlClient.FLAG_KEY_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_STOP: diff --git a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java index e9d7cbc8..b9f40f32 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java @@ -22,6 +22,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.RemoteStatus; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; +import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.service.parser.SubsonicRESTException; import github.daneren2005.dsub.util.Util; @@ -47,7 +48,7 @@ public class JukeboxController extends RemoteController { private float gain = 0.5f; public JukeboxController(DownloadService downloadService, Handler handler) { - this.downloadService = downloadService; + super(downloadService); this.handler = handler; } @@ -200,11 +201,15 @@ public class JukeboxController extends RemoteController { // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); + int currentPlayingIndex = downloadService.getCurrentPlayingIndex(); if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) { downloadService.setPlayerState(PlayerState.COMPLETED); downloadService.setCurrentPlaying(index, true); if(jukeboxStatus.isPlaying()) { downloadService.setPlayerState(PlayerState.STARTED); + } else if(index == 0 && currentPlayingIndex == downloadService.size() - 1 && downloadService.getRepeatMode() == RepeatMode.ALL) { + // Jukebox does not support any form of auto repeat + start(); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java index 2972bb7c..6a58e340 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java @@ -18,18 +18,18 @@ */ package github.daneren2005.dsub.service; +import java.net.HttpURLConnection; import java.util.List; -import org.apache.http.HttpResponse; import android.content.Context; import android.graphics.Bitmap; import github.daneren2005.dsub.domain.ArtistInfo; -import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.PlayerQueue; import github.daneren2005.dsub.domain.RemoteStatus; import github.daneren2005.dsub.domain.Lyrics; @@ -94,6 +94,8 @@ public interface MusicService { MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception; MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; @@ -101,7 +103,7 @@ public interface MusicService { Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; - HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception; + HttpURLConnection getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception; String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception; @@ -147,7 +149,7 @@ public interface MusicService { MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception; - MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception; void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception; @@ -192,6 +194,8 @@ public interface MusicService { void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception; PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception; + + List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception; int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception; diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java index b8633624..da6c37f1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -21,6 +21,7 @@ package github.daneren2005.dsub.service; import java.io.File; import java.io.Reader; import java.io.FileReader; +import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -34,13 +35,12 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.util.Log; -import org.apache.http.HttpResponse; - import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.PlayerQueue; import github.daneren2005.dsub.domain.PodcastEpisode; @@ -95,7 +95,7 @@ public class OfflineMusicService implements MusicService { artist.setIndex(file.getName().substring(0, 1)); artist.setName(file.getName()); artists.add(artist); - } else { + } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { entries.add(createEntry(context, file)); } } @@ -226,7 +226,7 @@ public class OfflineMusicService implements MusicService { } @Override - public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + public HttpURLConnection getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { throw new OfflineException(ERRORMSG); } @@ -307,7 +307,15 @@ public class OfflineMusicService implements MusicService { } } }); - + + // Respect counts in search criteria + int artistCount = Math.min(artists.size(), criteria.getArtistCount()); + int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); + int songCount = Math.min(songs.size(), criteria.getSongCount()); + artists = artists.subList(0, artistCount); + albums = albums.subList(0, albumCount); + songs = songs.subList(0, songCount); + return new SearchResult(artists, albums, songs); } @@ -359,20 +367,13 @@ public class OfflineMusicService implements MusicService { } } private int matchCriteria(SearchCritera criteria, String name) { - String query = criteria.getQuery().toLowerCase(); - String[] queryParts = query.split(" "); - String[] nameParts = name.toLowerCase().split(" "); - - int closeness = 0; - for(String queryPart : queryParts) { - for(String namePart : nameParts) { - if(namePart.equals(queryPart)) { - closeness++; - } - } + if (criteria.getPattern().matcher(name).matches()) { + return Util.getStringDistance( + criteria.getQuery().toLowerCase(), + name.toLowerCase()); + } else { + return 0; } - - return closeness; } @Override @@ -587,6 +588,11 @@ public class OfflineMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(ERRORMSG); } @@ -769,7 +775,7 @@ public class OfflineMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { throw new OfflineException(ERRORMSG); } @@ -884,6 +890,11 @@ public class OfflineMusicService implements MusicService { } @Override + public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ throw new OfflineException(ERRORMSG); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index 4a6e5108..657ac4a9 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -24,61 +24,37 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.params.ConnManagerParams; -import org.apache.http.conn.params.ConnPerRouteBean; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.scheme.SocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.ExecutionContext; -import org.apache.http.protocol.HttpContext; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.os.Looper; +import android.util.Base64; import android.util.Log; +import com.google.android.gms.security.ProviderInstaller; + import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; -import github.daneren2005.dsub.service.parser.AlbumListParser; +import github.daneren2005.dsub.fragments.MainFragment; +import github.daneren2005.dsub.service.parser.EntryListParser; import github.daneren2005.dsub.service.parser.ArtistInfoParser; import github.daneren2005.dsub.service.parser.BookmarkParser; import github.daneren2005.dsub.service.parser.ChatMessageParser; import github.daneren2005.dsub.service.parser.ErrorParser; import github.daneren2005.dsub.service.parser.GenreParser; import github.daneren2005.dsub.service.parser.IndexesParser; +import github.daneren2005.dsub.service.parser.InternetRadioStationParser; import github.daneren2005.dsub.service.parser.JukeboxStatusParser; import github.daneren2005.dsub.service.parser.LicenseParser; import github.daneren2005.dsub.service.parser.LyricsParser; @@ -95,11 +71,9 @@ import github.daneren2005.dsub.service.parser.SearchResult2Parser; import github.daneren2005.dsub.service.parser.SearchResultParser; import github.daneren2005.dsub.service.parser.ShareParser; import github.daneren2005.dsub.service.parser.StarredListParser; +import github.daneren2005.dsub.service.parser.TopSongsParser; import github.daneren2005.dsub.service.parser.UserParser; import github.daneren2005.dsub.service.parser.VideosParser; -import github.daneren2005.dsub.service.ssl.SSLSocketFactory; -import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy; -import github.daneren2005.dsub.util.BackgroundTask; import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.Constants; @@ -108,19 +82,23 @@ import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.*; +import java.util.Map; import java.util.zip.GZIPInputStream; -/** - * @author Sindre Mehus - */ +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + public class RESTMusicService implements MusicService { private static final String TAG = RESTMusicService.class.getSimpleName(); - private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000; private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000; private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000; - private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000; private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000; // Allow 20 seconds extra timeout per MB offset. @@ -129,51 +107,46 @@ public class RESTMusicService implements MusicService { private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5; private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L; - private final DefaultHttpClient httpClient; + private SSLSocketFactory sslSocketFactory; + private HostnameVerifier selfSignedHostnameVerifier; private long redirectionLastChecked; private int redirectionNetworkType = -1; private String redirectFrom; private String redirectTo; - private final ThreadSafeClientConnManager connManager; private Integer instance; + private boolean hasInstalledGoogleSSL = false; public RESTMusicService() { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + sslSocketFactory = sslContext.getSocketFactory(); + } catch (Exception e) { + } - // Create and initialize default HTTP parameters - HttpParams params = new BasicHttpParams(); - ConnManagerParams.setMaxTotalConnections(params, 20); - ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20)); - HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - // Create and initialize scheme registry - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); - schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443)); - - // Create an HttpClient with the ThreadSafeClientConnManager. - // This connection manager must be used if more than one thread will - // be using the HttpClient. - connManager = new ThreadSafeClientConnManager(params, schemeRegistry); - httpClient = new DefaultHttpClient(connManager, params); - } - - private SocketFactory createSSLSocketFactory() { - try { - return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); - } catch (Throwable x) { - Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x); - return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory(); - } + selfSignedHostnameVerifier = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; } @Override public void ping(Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "ping", null); + Reader reader = getReader(context, progressListener, "ping"); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -184,7 +157,7 @@ public class RESTMusicService implements MusicService { @Override public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getLicense", null); + Reader reader = getReader(context, progressListener, "getLicense"); try { ServerInfo serverInfo = new LicenseParser(context, getInstance(context)).parse(reader); return serverInfo.isLicenseValid(); @@ -194,7 +167,7 @@ public class RESTMusicService implements MusicService { } public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getMusicFolders", null); + Reader reader = getReader(context, progressListener, "getMusicFolders"); try { return new MusicFoldersParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -204,7 +177,17 @@ public class RESTMusicService implements MusicService { @Override public void startRescan(Context context, ProgressListener listener) throws Exception { - Reader reader = getReader(context, listener, "startRescan", null); + String startMethod = ServerInfo.isMadsonic(context, getInstance(context)) ? "startRescan" : "startScan"; + String refreshMethod = null; + if(ServerInfo.isMadsonic(context, getInstance(context))) { + startMethod = "startRescan"; + refreshMethod = "scanstatus"; + } else { + startMethod = "startScan"; + refreshMethod = "getScanStatus"; + } + + Reader reader = getReader(context, listener, startMethod); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -214,7 +197,7 @@ public class RESTMusicService implements MusicService { // Now check if still running boolean done = false; while(!done) { - reader = getReader(context, null, "scanstatus", null); + reader = getReader(context, null, refreshMethod); try { boolean running = new ScanStatusParser(context, getInstance(context)).parse(reader, listener); if(running) { @@ -241,7 +224,7 @@ public class RESTMusicService implements MusicService { parameterValues.add(musicFolderId); } - Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", parameterNames, parameterValues); try { return new IndexesParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -287,7 +270,7 @@ public class RESTMusicService implements MusicService { } private MusicDirectory getMusicDirectoryImpl(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id); + Reader reader = getReader(context, progressListener, "getMusicDirectory", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { @@ -297,7 +280,7 @@ public class RESTMusicService implements MusicService { @Override public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getArtist", null, "id", id); + Reader reader = getReader(context, progressListener, "getArtist", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { @@ -307,7 +290,7 @@ public class RESTMusicService implements MusicService { @Override public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getAlbum", null, "id", id); + Reader reader = getReader(context, progressListener, "getAlbum", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { @@ -331,7 +314,7 @@ public class RESTMusicService implements MusicService { private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = Arrays.asList("any", "songCount"); List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount()); - Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "search", parameterNames, parameterValues); try { return new SearchResultParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -363,7 +346,7 @@ public class RESTMusicService implements MusicService { method = "search2"; } } - Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues); try { return new SearchResult2Parser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -373,10 +356,7 @@ public class RESTMusicService implements MusicService { @Override public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST); - - Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id); + Reader reader = getReader(context, progressListener, "getPlaylist", "id", id, SOCKET_READ_TIMEOUT_GET_PLAYLIST); try { return new PlaylistParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -386,7 +366,7 @@ public class RESTMusicService implements MusicService { @Override public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getPlaylists", null); + Reader reader = getReader(context, progressListener, "getPlaylists"); try { return new PlaylistsParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -412,7 +392,7 @@ public class RESTMusicService implements MusicService { parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener)); } - Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "createPlaylist", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -422,7 +402,7 @@ public class RESTMusicService implements MusicService { @Override public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id); + Reader reader = getReader(context, progressListener, "deletePlaylist", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -441,7 +421,7 @@ public class RESTMusicService implements MusicService { names.add("songIdToAdd"); values.add(getOfflineSongId(song.getId(), context, progressListener)); } - Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -460,7 +440,7 @@ public class RESTMusicService implements MusicService { names.add("songIndexToRemove"); values.add(song); } - Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -485,7 +465,7 @@ public class RESTMusicService implements MusicService { names.add("songIndexToRemove"); values.add(i); } - Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -496,7 +476,7 @@ public class RESTMusicService implements MusicService { @Override public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Updating playlists is not supported."); - Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub)); + Reader reader = getReader(context, progressListener, "updatePlaylist", Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -506,7 +486,7 @@ public class RESTMusicService implements MusicService { @Override public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title)); + Reader reader = getReader(context, progressListener, "getLyrics", Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title)); try { return new LyricsParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -525,10 +505,10 @@ public class RESTMusicService implements MusicService { Reader reader; if(time > 0){ checkServerVersion(context, "1.8", "Scrobbling with a time not supported."); - reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time)); + reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time)); } else - reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission)); + reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -569,9 +549,9 @@ public class RESTMusicService implements MusicService { method = "getAlbumList"; } - Reader reader = getReader(context, progressListener, method, null, names, values, true); + Reader reader = getReader(context, progressListener, method, names, values, true); try { - return new AlbumListParser(context, getInstance(context)).parse(reader, progressListener); + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -606,7 +586,7 @@ public class RESTMusicService implements MusicService { int decade = Integer.parseInt(extra); // Reverse chronological order only supported in 5.3+ - if(ServerInfo.checkServerVersion(context, "1.13", instance) && ServerInfo.isStockSubsonic(context, instance)) { + if(ServerInfo.checkServerVersion(context, "1.13", instance) && !ServerInfo.isMadsonic(context, instance)) { values.add(decade + 9); values.add(decade); } else { @@ -635,15 +615,51 @@ public class RESTMusicService implements MusicService { method = "getAlbumList"; } - Reader reader = getReader(context, progressListener, method, null, names, values, true); + Reader reader = getReader(context, progressListener, method, names, values, true); try { - return new AlbumListParser(context, instance).parse(reader, progressListener); + return new EntryListParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } - @Override + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + String method; + switch(type) { + case MainFragment.SONGS_NEWEST: + method = "getNewaddedSongs"; + break; + case MainFragment.SONGS_TOP_PLAYED: + method = "getTopplayedSongs"; + break; + case MainFragment.SONGS_RECENT: + method = "getLastplayedSongs"; + break; + case MainFragment.SONGS_FREQUENT: + method = "getMostplayedSongs"; + break; + default: + method = "getNewaddedSongs"; + } + + Reader reader = getReader(context, progressListener, method, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.11", "Artist radio is not supported"); @@ -658,7 +674,13 @@ public class RESTMusicService implements MusicService { int instance = getInstance(context); String method; - if(ServerInfo.isMadsonic(context, instance)) { + if(ServerInfo.isMadsonic6(context, instance)) { + if (Util.isTagBrowsing(context, instance)) { + method = "getSimilarSongsID3"; + } else { + method = "getSimilarSongs"; + } + } else if(ServerInfo.isMadsonic(context, instance)) { method = "getPandoraSongs"; } else { if (Util.isTagBrowsing(context, instance)) { @@ -668,7 +690,7 @@ public class RESTMusicService implements MusicService { } } - Reader reader = getReader(context, progressListener, method, null, names, values); + Reader reader = getReader(context, progressListener, method, names, values); try { return new RandomSongsParser(context, instance).parse(reader, progressListener); } finally { @@ -702,7 +724,7 @@ public class RESTMusicService implements MusicService { method = "getStarred"; } - Reader reader = getReader(context, progressListener, method, null, names, values, true); + Reader reader = getReader(context, progressListener, method, names, values, true); try { return new StarredListParser(context, instance).parse(reader, progressListener); } finally { @@ -712,10 +734,7 @@ public class RESTMusicService implements MusicService { @Override public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - - List<String> names = new ArrayList<String>(); + List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("size"); @@ -754,7 +773,7 @@ public class RESTMusicService implements MusicService { values.add(endYear); } - Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values); + Reader reader = getReader(context, progressListener, "getRandomSongs", names, values); try { return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -784,87 +803,24 @@ public class RESTMusicService implements MusicService { @Override public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { - // Synchronize on the entry so that we don't download concurrently for the same song. synchronized (entry) { + String url = getRestUrl(context, "getCoverArt"); + List<String> parameterNames = Arrays.asList("id"); + List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt()); - // Use cached file, if existing. - Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); - if (bitmap != null) { - return bitmap; - } - - String url = getRestUrl(context, "getCoverArt"); - - InputStream in = null; - try { - List<String> parameterNames = Arrays.asList("id"); - List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt()); - HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task); - - in = entity.getContent(); - Header contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { - in = new GZIPInputStream(in); - } - - // If content type is XML, an error occured. Get it. - String contentType = Util.getContentType(entity); - if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { - new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); - return null; // Never reached. - } - - byte[] bytes = Util.toByteArray(in); - - // Handle case where partial was downloaded before being cancelled - if(task != null && task.isCancelled()) { - return null; - } - - OutputStream out = null; - try { - out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry)); - out.write(bytes); - } finally { - Util.close(out); - } - - // Size == 0 -> only want to download - if(size == 0) { - return null; - } else { - return FileUtil.getSampledBitmap(bytes, size); - } - } finally { - Util.close(in); - } + return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAlbumArtFile(context, entry), true, progressListener, task); } } @Override - public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { - + public HttpURLConnection getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { String url = getRestUrl(context, "stream"); - - // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is - // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server. - // In that case, the server uses a long time before sending any data, causing the client to time out. - HttpParams params = new BasicHttpParams(); - int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE); - HttpConnectionParams.setSoTimeout(params, timeout); - - // Add "Range" header if offset is given. - List<Header> headers = new ArrayList<Header>(); - if (offset > 0) { - headers.add(new BasicHeader("Range", "bytes=" + offset + "-")); - } - List<String> parameterNames = new ArrayList<String>(); parameterNames.add("id"); parameterNames.add("maxBitRate"); - List<Object> parameterValues = new ArrayList<Object>(); + List<Object> parameterValues = new ArrayList<>(); parameterValues.add(song.getId()); parameterValues.add(maxBitrate); @@ -884,24 +840,32 @@ public class RESTMusicService implements MusicService { parameterValues.add("raw"); } } - HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task, false); - // If content type is XML, an error occurred. Get it. - String contentType = Util.getContentType(response.getEntity()); - if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { - InputStream in = response.getEntity().getContent(); - Header contentEncoding = response.getEntity().getContentEncoding(); - if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { - in = new GZIPInputStream(in); + // Add "Range" header if offset is given + Map<String, String> headers = new HashMap<>(); + if (offset > 0) { + headers.put("Range", "bytes=" + offset + "-"); + } + + // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is + // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server. + // In that case, the server uses a long time before sending any data, causing the client to time out. + int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE); + HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, headers, timeout); + + // If content type is XML, an error occurred. Get it. + String contentType = connection.getContentType(); + if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { + InputStream in = getInputStreamFromConnection(connection); + + try { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + } finally { + Util.close(in); } - try { - new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); - } finally { - Util.close(in); - } - } + } - return response; + return connection; } @Override @@ -909,10 +873,9 @@ public class RESTMusicService implements MusicService { StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); builder.append("&id=").append(song.getId()); - // If we are doing mp3 to mp3, just specify raw so that stuff works better - if("mp3".equals(song.getSuffix()) && (song.getTranscodedSuffix() == null || "mp3".equals(song.getTranscodedSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { + // Allow user to specify to stream raw formats if available + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_STREAM_ORIGINAL, true) && ("mp3".equals(song.getSuffix()) || "flac".equals(song.getSuffix()) || "wav".equals(song.getSuffix()) || "aac".equals(song.getSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { builder.append("&format=raw"); - builder.append("&estimateContentLength=true"); } else { builder.append("&maxBitRate=").append(maxBitrate); } @@ -1013,7 +976,7 @@ public class RESTMusicService implements MusicService { private RemoteStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception { checkServerVersion(context, "1.7", "Jukebox not supported."); - Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "jukeboxControl", parameterNames, parameterValues); try { return new JukeboxStatusParser(context, getInstance(context)).parse(reader); } finally { @@ -1052,7 +1015,7 @@ public class RESTMusicService implements MusicService { } } - Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, names, values); + Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1064,7 +1027,7 @@ public class RESTMusicService implements MusicService { public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Shares not supported."); - Reader reader = getReader(context, progressListener, "getShares", null); + Reader reader = getReader(context, progressListener, "getShares"); try { return new ShareParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1092,7 +1055,7 @@ public class RESTMusicService implements MusicService { parameterValues.add(expires); } - Reader reader = getReader(context, progressListener, "createShare", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "createShare", parameterNames, parameterValues); try { return new ShareParser(context, getInstance(context)).parse(reader, progressListener); } @@ -1105,16 +1068,13 @@ public class RESTMusicService implements MusicService { public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Shares not supported."); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("id"); parameterValues.add(id); - Reader reader = getReader(context, progressListener, "deleteShare", params, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "deleteShare", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1128,9 +1088,6 @@ public class RESTMusicService implements MusicService { public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Updating share not supported."); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); @@ -1145,7 +1102,7 @@ public class RESTMusicService implements MusicService { parameterNames.add("expires"); parameterValues.add(expires); - Reader reader = getReader(context, progressListener, "updateShare", params, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "updateShare", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } @@ -1158,16 +1115,13 @@ public class RESTMusicService implements MusicService { public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.2", "Chat not supported."); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("since"); parameterValues.add(since); - Reader reader = getReader(context, progressListener, "getChatMessages", params, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "getChatMessages", parameterNames, parameterValues); try { return new ChatMessageParser(context, getInstance(context)).parse(reader, progressListener); @@ -1180,16 +1134,13 @@ public class RESTMusicService implements MusicService { public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.2", "Chat not supported."); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("message"); parameterValues.add(message); - Reader reader = getReader(context, progressListener, "addChatMessage", params, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "addChatMessage", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1202,7 +1153,7 @@ public class RESTMusicService implements MusicService { public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Genres not supported."); - Reader reader = getReader(context, progressListener, "getGenres", null); + Reader reader = getReader(context, progressListener, "getGenres"); try { return new GenreParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1214,9 +1165,6 @@ public class RESTMusicService implements MusicService { public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Genres not supported."); - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); - List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); @@ -1237,7 +1185,7 @@ public class RESTMusicService implements MusicService { } } - Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues, true); + Reader reader = getReader(context, progressListener, "getSongsByGenre", parameterNames, parameterValues, true); try { return new RandomSongsParser(context, instance).parse(reader, progressListener); } finally { @@ -1256,9 +1204,9 @@ public class RESTMusicService implements MusicService { parameterValues.add(size); String method = ServerInfo.isMadsonic(context, getInstance(context)) ? "getTopTrackSongs" : "getTopSongs"; - Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues); try { - return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); + return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -1268,7 +1216,7 @@ public class RESTMusicService implements MusicService { public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Podcasts not supported."); - Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false")); + Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false")); try { List<PodcastChannel> channels = new PodcastChannelParser(context, getInstance(context)).parse(reader, progressListener); @@ -1290,7 +1238,7 @@ public class RESTMusicService implements MusicService { @Override public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("id"), Arrays.<Object>asList(id)); + Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("id"), Arrays.<Object>asList(id)); try { return new PodcastEntryParser(context, getInstance(context)).parse(id, reader, progressListener); } finally { @@ -1299,8 +1247,8 @@ public class RESTMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count)); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + Reader reader = getReader(context, progressListener, "getNewestPodcasts", Arrays.asList("count"), Arrays.<Object>asList(count), true); try { return new PodcastEntryParser(context, getInstance(context)).parse(null, reader, progressListener); @@ -1313,7 +1261,7 @@ public class RESTMusicService implements MusicService { public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Refresh podcasts not supported."); - Reader reader = getReader(context, progressListener, "refreshPodcasts", null); + Reader reader = getReader(context, progressListener, "refreshPodcasts"); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1325,7 +1273,7 @@ public class RESTMusicService implements MusicService { public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{ checkServerVersion(context, "1.9", "Creating podcasts not supported."); - Reader reader = getReader(context, progressListener, "createPodcastChannel", null, "url", url); + Reader reader = getReader(context, progressListener, "createPodcastChannel", "url", url); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1337,7 +1285,7 @@ public class RESTMusicService implements MusicService { public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Deleting podcasts not supported."); - Reader reader = getReader(context, progressListener, "deletePodcastChannel", null, "id", id); + Reader reader = getReader(context, progressListener, "deletePodcastChannel", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1349,7 +1297,7 @@ public class RESTMusicService implements MusicService { public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ checkServerVersion(context, "1.9", "Downloading podcasts not supported."); - Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", null, "id", id); + Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1361,7 +1309,7 @@ public class RESTMusicService implements MusicService { public void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception{ checkServerVersion(context, "1.9", "Deleting podcasts not supported."); - Reader reader = getReader(context, progressListener, "deletePodcastEpisode", null, "id", id); + Reader reader = getReader(context, progressListener, "deletePodcastEpisode", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1373,7 +1321,7 @@ public class RESTMusicService implements MusicService { public void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Setting ratings not supported."); - Reader reader = getReader(context, progressListener, "setRating", null, Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating)); + Reader reader = getReader(context, progressListener, "setRating", Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1385,7 +1333,7 @@ public class RESTMusicService implements MusicService { public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Bookmarks not supported."); - Reader reader = getReader(context, progressListener, "getBookmarks", null); + Reader reader = getReader(context, progressListener, "getBookmarks"); try { return new BookmarkParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1397,7 +1345,7 @@ public class RESTMusicService implements MusicService { public void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Creating bookmarks not supported."); - Reader reader = getReader(context, progressListener, "createBookmark", null, Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment)); + Reader reader = getReader(context, progressListener, "createBookmark", Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1409,7 +1357,7 @@ public class RESTMusicService implements MusicService { public void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Deleting bookmarks not supported."); - Reader reader = getReader(context, progressListener, "deleteBookmark", null, Arrays.asList("id"), Arrays.<Object>asList(entry.getId())); + Reader reader = getReader(context, progressListener, "deleteBookmark", Arrays.asList("id"), Arrays.<Object>asList(entry.getId())); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1419,7 +1367,7 @@ public class RESTMusicService implements MusicService { @Override public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getUser", null, Arrays.asList("username"), Arrays.<Object>asList(username)); + Reader reader = getReader(context, progressListener, "getUser", Arrays.asList("username"), Arrays.<Object>asList(username)); try { List<User> users = new UserParser(context, getInstance(context)).parse(reader, progressListener); if(users.size() > 0) { @@ -1437,7 +1385,7 @@ public class RESTMusicService implements MusicService { public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Getting user list is not supported"); - Reader reader = getReader(context, progressListener, "getUsers", null); + Reader reader = getReader(context, progressListener, "getUsers"); try { return new UserParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1462,7 +1410,16 @@ public class RESTMusicService implements MusicService { values.add(setting.getValue()); } - Reader reader = getReader(context, progressListener, "createUser", null, names, values); + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + Reader reader = getReader(context, progressListener, "createUser", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1487,7 +1444,16 @@ public class RESTMusicService implements MusicService { } } - Reader reader = getReader(context, progressListener, "updateUser", null, names, values); + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + Reader reader = getReader(context, progressListener, "updateUser", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1497,7 +1463,7 @@ public class RESTMusicService implements MusicService { @Override public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "deleteUser", null, Arrays.asList("username"), Arrays.<Object>asList(username)); + Reader reader = getReader(context, progressListener, "deleteUser", Arrays.asList("username"), Arrays.<Object>asList(username)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1507,7 +1473,7 @@ public class RESTMusicService implements MusicService { @Override public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "updateUser", null, Arrays.asList("username", "email"), Arrays.<Object>asList(username, email)); + Reader reader = getReader(context, progressListener, "updateUser", Arrays.asList("username", "email"), Arrays.<Object>asList(username, email)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1517,7 +1483,7 @@ public class RESTMusicService implements MusicService { @Override public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "changePassword", null, Arrays.asList("username", "password"), Arrays.<Object>asList(username, password)); + Reader reader = getReader(context, progressListener, "changePassword", Arrays.asList("username", "password"), Arrays.<Object>asList(username, password)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1535,55 +1501,11 @@ public class RESTMusicService implements MusicService { // Synchronize on the username so that we don't download concurrently for // the same user. synchronized (username) { - // Use cached file, if existing. - Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size); - if(bitmap != null) { - return bitmap; - } - String url = Util.getRestUrl(context, "getAvatar"); - InputStream in = null; - try - { - List<String> parameterNames; - List<Object> parameterValues; - - parameterNames = Collections.singletonList("username"); - parameterValues = Arrays.<Object>asList(username); - - HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task); - in = entity.getContent(); - Header contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { - in = new GZIPInputStream(in); - } - - // If content type is XML, an error occurred. Get it. - String contentType = Util.getContentType(entity); - if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { - new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); - return null; // Never reached. - } - - byte[] bytes = Util.toByteArray(in); - if(task != null && task.isCancelled()) { - // Handle case where partial is downloaded and cancelled - return null; - } - - OutputStream out = null; - try { - out = new FileOutputStream(FileUtil.getAvatarFile(context, username)); - out.write(bytes); - } finally { - Util.close(out); - } + List<String> parameterNames = Collections.singletonList("username"); + List<Object> parameterValues = Arrays.<Object>asList(username); - return FileUtil.getSampledBitmap(bytes, size, false); - } - finally { - Util.close(in); - } + return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAvatarFile(context, username), false, progressListener, task); } } @@ -1603,7 +1525,7 @@ public class RESTMusicService implements MusicService { method = "getArtistInfo"; } - Reader reader = getReader(context, progressListener, method, null, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true")); + Reader reader = getReader(context, progressListener, method, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true")); try { return new ArtistInfoParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1615,53 +1537,13 @@ public class RESTMusicService implements MusicService { public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { // Synchronize on the url so that we don't download concurrently synchronized (url) { - // Use cached file, if existing. - Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size); - if(bitmap != null) { - return bitmap; - } - - InputStream in = null; - try { - HttpEntity entity = getEntityForURL(context, url, null, null, null, progressListener, task); - in = entity.getContent(); - Header contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { - in = new GZIPInputStream(in); - } - - // If content type is XML, an error occurred. Get it. - String contentType = Util.getContentType(entity); - if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { - new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); - return null; // Never reached. - } - - byte[] bytes = Util.toByteArray(in); - if(task != null && task.isCancelled()) { - // Handle case where partial is downloaded and cancelled - return null; - } - - OutputStream out = null; - try { - out = new FileOutputStream(FileUtil.getMiscFile(context, url)); - out.write(bytes); - } finally { - Util.close(out); - } - - return FileUtil.getSampledBitmap(bytes, size, false); - } - finally { - Util.close(in); - } + return getBitmapFromUrl(context, url, null, null, size, FileUtil.getMiscFile(context, url), false, progressListener, task); } } @Override public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getVideos", null, true); + Reader reader = getReader(context, progressListener, "getVideos"); try { return new VideosParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1685,7 +1567,7 @@ public class RESTMusicService implements MusicService { parameterNames.add("position"); parameterValues.add(position); - Reader reader = getReader(context, progressListener, "savePlayQueue", null, parameterNames, parameterValues); + Reader reader = getReader(context, progressListener, "savePlayQueue", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { @@ -1695,7 +1577,7 @@ public class RESTMusicService implements MusicService { @Override public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getPlayQueue", null); + Reader reader = getReader(context, progressListener, "getPlayQueue"); try { return new PlayQueueParser(context, getInstance(context)).parse(reader, progressListener); } finally { @@ -1704,6 +1586,18 @@ public class RESTMusicService implements MusicService { } @Override + public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", null); + + Reader reader = getReader(context, progressListener, "getInternetRadioStations"); + try { + return new InternetRadioStationParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener); } @@ -1714,13 +1608,13 @@ public class RESTMusicService implements MusicService { int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); int retry = 0; for(int i = 1; i <= count; i++) { - String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); - long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); - if(id != null) { - scrobble(id, true, time, context, progressListener); - } else { - String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); - try{ + try { + String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); + long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); + if(id != null) { + scrobble(id, true, time, context, progressListener); + } else { + String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); SearchCritera critera = new SearchCritera(search, 0, 0, 1); SearchResult result = searchNew(critera, context, progressListener); if(result.getSongs().size() == 1){ @@ -1732,10 +1626,10 @@ public class RESTMusicService implements MusicService { throw new Exception("Song not found on server"); } } - catch(Exception e){ - Log.e(TAG, e.toString()); - retry++; - } + } + catch(Exception e){ + Log.e(TAG, e.toString()); + retry++; } } @@ -1815,210 +1709,244 @@ public class RESTMusicService implements MusicService { this.instance = instance; } - private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception { - return getReader(context, progressListener, method, requestParams, false); + protected Bitmap getBitmapFromUrl(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int size, File saveToFile, boolean allowUnscaled, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + InputStream in = null; + try { + HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, progressListener, true); + in = getInputStreamFromConnection(connection); + + String contentType = connection.getContentType(); + if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + } + + byte[] bytes = Util.toByteArray(in); + + // Handle case where partial was downloaded before being cancelled + if(task != null && task.isCancelled()) { + return null; + } + + OutputStream out = null; + try { + out = new FileOutputStream(saveToFile); + out.write(bytes); + } finally { + Util.close(out); + } + + // Size == 0 -> only want to download + if(size == 0) { + return null; + } else { + return FileUtil.getSampledBitmap(bytes, size, allowUnscaled); + } + } finally { + Util.close(in); + } } - private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams, boolean throwsError) throws Exception { - return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList(), throwsError); - } - private Reader getReader(Context context, ProgressListener progressListener, String method, - HttpParams requestParams, String parameterName, Object parameterValue) throws Exception { - return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue)); - } + // Helper classes to get a reader for the request + private Reader getReader(Context context, ProgressListener progressListener, String method) throws Exception { + return getReader(context, progressListener, method, (List<String>)null, null); + } - private Reader getReader(Context context, ProgressListener progressListener, String method, - HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception { - return getReader(context, progressListener, method, requestParams, parameterNames, parameterValues, false); + private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue) throws Exception { + return getReader(context, progressListener, method, parameterName, parameterValue, 0); } - private Reader getReader(Context context, ProgressListener progressListener, String method, - HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception { + private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue, int minNetworkTimeout) throws Exception { + return getReader(context, progressListener, method, Arrays.asList(parameterName), Arrays.asList(parameterValue), minNetworkTimeout); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues) throws Exception { + return getReader(context, progressListener, method, parameterNames, parameterValues, 0); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout) throws Exception { + return getReader(context, progressListener, method, parameterNames, parameterValues, minNetworkTimeout, false); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception { + return getReader(context, progressListener, method, parameterNames, parameterValues, 0, throwErrors); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, boolean throwErrors) throws Exception { if (progressListener != null) { progressListener.updateProgress(R.string.service_connecting); } String url = getRestUrl(context, method); - return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors); + return getReaderForURL(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors); } - private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, - List<Object> parameterValues, ProgressListener progressListener) throws Exception { - return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, true); + private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener) throws Exception { + return getReaderForURL(context, url, parameterNames, parameterValues, progressListener, true); } - private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, - List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { - HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors); - if (entity == null) { - throw new RuntimeException("No entity received for URL " + url); - } + private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { + return getReaderForURL(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors); + } + private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { + InputStream in = getInputStream(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors); + return new InputStreamReader(in, Constants.UTF_8); + } - InputStream in = entity.getContent(); - Header contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + // Helper classes to open a connection to a server + private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwsErrors) throws Exception { + return getInputStream(context, url, parameterNames, parameterValues, 0, progressListener, throwsErrors); + } + private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwsErrors) throws Exception { + HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwsErrors); + return getInputStreamFromConnection(connection); + } + private InputStream getInputStreamFromConnection(HttpURLConnection connection) throws Exception { + InputStream in = connection.getInputStream(); + if("gzip".equals(connection.getContentEncoding())) { in = new GZIPInputStream(in); } - return new InputStreamReader(in, Constants.UTF_8); - } - private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, - List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { + return in; + } - return getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, null, throwErrors); + private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception { + return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, null, true); + } + private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { + return getConnection(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors); + } + private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { + return getConnection(context, url, parameterNames, parameterValues, null, minNetworkTimeout, progressListener, throwErrors); + } + private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { + if(throwErrors) { + SharedPreferences prefs = Util.getPreferences(context); + int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + "")); + return getConnectionDirect(context, url, parameterNames, parameterValues, headers, Math.max(minNetworkTimeout, networkTimeout)); + } else { + return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, HTTP_REQUEST_MAX_ATTEMPTS, 0); + } } - private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, - List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { - return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, false).getEntity(); + private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, int retriesLeft, int attempts) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + "")); + minNetworkTimeout = Math.max(minNetworkTimeout, networkTimeout); + attempts++; + retriesLeft--; + + try { + return getConnectionDirect(context, url, parameterNames, parameterValues, headers, minNetworkTimeout); + } catch (IOException x) { + if(retriesLeft > 0) { + if (progressListener != null) { + String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); + progressListener.updateProgress(msg); + } + + Log.w(TAG, "Got IOException " + x + " (" + attempts + "), will retry"); + Thread.sleep(2000L); + + minNetworkTimeout = (int) (minNetworkTimeout * 1.3); + return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, retriesLeft, attempts); + } else { + throw x; + } + } } - private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, - List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsError) throws Exception { - return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, throwsError).getEntity(); - } - private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams, - List<String> parameterNames, List<Object> parameterValues, - List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsErrors) throws Exception { - // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being - // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus - // loosing its entity. - if (parameterNames != null && parameterNames.size() < 10) { - StringBuilder builder = new StringBuilder(url); - for (int i = 0; i < parameterNames.size(); i++) { - builder.append("&").append(parameterNames.get(i)).append("="); + private HttpURLConnection getConnectionDirect(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception { + // Add params to query + if (parameterNames != null) { + StringBuilder builder = new StringBuilder(url); + for (int i = 0; i < parameterNames.size(); i++) { + builder.append("&").append(parameterNames.get(i)).append("="); String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"); - part = part.replaceAll("\\%27", "'"); - builder.append(part); - } - url = builder.toString(); - parameterNames = null; - parameterValues = null; - } + part = part.replaceAll("\\%27", "'"); + builder.append(part); + } + url = builder.toString(); + } - String rewrittenUrl = rewriteUrlWithRedirect(context, url); - return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task, throwsErrors); - } + // Rewrite url based on redirects + String rewrittenUrl = rewriteUrlWithRedirect(context, url); + if(rewrittenUrl.indexOf("scanstatus") == -1) { + Log.i(TAG, stripUrlInfo(rewrittenUrl)); + } + + return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout); + } + + private HttpURLConnection getConnectionDirect(Context context, String url, Map<String, String> headers, int minNetworkTimeout) throws Exception { + if(!hasInstalledGoogleSSL) { + try { + ProviderInstaller.installIfNeeded(context); + } catch(Exception e) { + // Just continue on anyways, doesn't really harm anything if this fails + Log.w(TAG, "Failed to update to use Google Play SSL", e); + } + hasInstalledGoogleSSL = true; + } - private HttpResponse executeWithRetry(final Context context, String url, String originalUrl, HttpParams requestParams, - List<String> parameterNames, List<Object> parameterValues, - List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwErrors) throws Exception { - // Strip out sensitive information from log - if(url.indexOf("scanstatus") == -1) { - Log.i(TAG, stripUrlInfo(url)); + // Connect and add headers + URL urlObj = new URL(url); + HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); + if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1 && url.indexOf("getAvatar") == -1) { + connection.addRequestProperty("Accept-Encoding", "gzip"); + } + connection.addRequestProperty("User-Agent", Constants.REST_CLIENT_ID); + + // Set timeout + connection.setConnectTimeout(minNetworkTimeout); + connection.setReadTimeout(minNetworkTimeout); + + // Add headers + if(headers != null) { + for(Map.Entry<String, String> header: headers.entrySet()) { + connection.setRequestProperty(header.getKey(), header.getValue()); + } + } + + if(connection instanceof HttpsURLConnection) { + HttpsURLConnection sslConnection = (HttpsURLConnection) connection; + sslConnection.setSSLSocketFactory(sslSocketFactory); + sslConnection.setHostnameVerifier(selfSignedHostnameVerifier); } SharedPreferences prefs = Util.getPreferences(context); - int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); - HttpParams newParams = httpClient.getParams(); - HttpConnectionParams.setSoTimeout(newParams, networkTimeout); - httpClient.setParams(newParams); - - final AtomicReference<Boolean> isCancelled = new AtomicReference<Boolean>(false); - int attempts = 0; - while (true) { - attempts++; - HttpContext httpContext = new BasicHttpContext(); - final HttpRequestBase request = (url.indexOf("rest") == -1) ? new HttpGet(url) : new HttpPost(url); - - if (task != null) { - // Attempt to abort the HTTP request if the task is cancelled. - task.setOnCancelListener(new BackgroundTask.OnCancelListener() { - @Override - public void onCancel() { - try { - isCancelled.set(true); - if(Thread.currentThread() == Looper.getMainLooper().getThread()) { - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - request.abort(); - return null; - } - }.execute(); - } else { - request.abort(); - } - } catch(Exception e) { - Log.e(TAG, "Failed to stop http task", e); - } - } - }); - } - - if (parameterNames != null && request instanceof HttpPost) { - List<NameValuePair> params = new ArrayList<NameValuePair>(); - for (int i = 0; i < parameterNames.size(); i++) { - params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i)))); - } - ((HttpPost) request).setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8)); - } - - if (requestParams != null) { - request.setParams(requestParams); - } - - if (headers != null) { - for (Header header : headers) { - request.addHeader(header); - } - } - if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1 && url.indexOf("getAvatar") == -1) { - request.addHeader("Accept-Encoding", "gzip"); + int instance = getInstance(context); + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + String encoded = Base64.encodeToString((username + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP);; + connection.setRequestProperty("Authorization", "Basic " + encoded); + + // Force the connection to initiate + if(connection.getResponseCode() >= 500) { + throw new IOException("Error code: " + connection.getResponseCode()); + } + if(detectRedirect(context, urlObj, connection)) { + String rewrittenUrl = rewriteUrlWithRedirect(context, url); + if(!rewrittenUrl.equals(url)) { + connection.disconnect(); + return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout); } - request.addHeader("User-Agent", Constants.REST_CLIENT_ID); - - // Set credentials to get through apache proxies that require authentication. - int instance = getInstance(context); - String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); - String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); - httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), - new UsernamePasswordCredentials(username, password)); - - try { - HttpResponse response = httpClient.execute(request, httpContext); - detectRedirect(originalUrl, context, httpContext); - return response; - } catch (IOException x) { - request.abort(); - if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || isCancelled.get() || throwErrors) { - throw x; - } - if (progressListener != null) { - String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); - progressListener.updateProgress(msg); - } - Log.w(TAG, "Got IOException " + x + " (" + attempts + "), will retry"); - increaseTimeouts(requestParams); - Thread.sleep(2000L); - } - } - } + } - private void increaseTimeouts(HttpParams requestParams) { - if (requestParams != null) { - int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams); - if (connectTimeout != 0) { - HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F)); - } - int readTimeout = HttpConnectionParams.getSoTimeout(requestParams); - if (readTimeout != 0) { - HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F)); - } - } - } + return connection; + } - private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) throws Exception { - HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST); - HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST); - - // Sometimes the request doesn't contain the "http://host" part - String redirectedUrl; - if (request.getURI().getScheme() == null) { - redirectedUrl = host.toURI() + request.getURI(); - } else { - redirectedUrl = request.getURI().toString(); + // Returns true when we should immediately retry with the redirect + private boolean detectRedirect(Context context, URL originalUrl, HttpURLConnection connection) throws Exception { + if(connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM) { + String redirectLocation = connection.getHeaderField("Location"); + if(redirectLocation != null) { + detectRedirect(context, originalUrl.toExternalForm(), redirectLocation); + return true; + } } + detectRedirect(context, originalUrl, connection.getURL()); + return false; + } + private void detectRedirect(Context context, URL originalUrl, URL redirectedUrl) throws Exception { + detectRedirect(context, originalUrl.toExternalForm(), redirectedUrl.toExternalForm()); + } + private void detectRedirect(Context context, String originalUrl, String redirectedUrl) throws Exception { if(redirectedUrl != null && "http://subsonic.org/pages/".equals(redirectedUrl)) { throw new Exception("Invalid url, redirects to http://subsonic.org/pages/"); } @@ -2035,7 +1963,7 @@ public class RESTMusicService implements MusicService { redirectionLastChecked = System.currentTimeMillis(); redirectionNetworkType = getCurrentNetworkType(context); } - } + } private String rewriteUrlWithRedirect(Context context, String url) { @@ -2084,7 +2012,10 @@ public class RESTMusicService implements MusicService { } } - public HttpClient getHttpClient() { - return httpClient; + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + public HostnameVerifier getHostNameVerifier() { + return selfSignedHostnameVerifier; } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java index 99502f5e..617144d7 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java @@ -19,18 +19,32 @@ package github.daneren2005.dsub.service; +import android.content.SharedPreferences; import android.util.Log; import java.util.Iterator; import java.util.concurrent.LinkedBlockingQueue; +import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.RemoteStatus; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.serverproxy.FileProxy; +import github.daneren2005.serverproxy.ServerProxy; import github.daneren2005.serverproxy.WebProxy; public abstract class RemoteController { private static final String TAG = RemoteController.class.getSimpleName(); protected DownloadService downloadService; protected boolean nextSupported = false; + protected ServerProxy proxy; + protected String rootLocation = ""; + + public RemoteController(DownloadService downloadService) { + this.downloadService = downloadService; + SharedPreferences prefs = Util.getPreferences(downloadService); + rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + } public abstract void create(boolean playing, int seconds); public abstract void start(); @@ -43,7 +57,11 @@ public abstract class RemoteController { // Really is abstract, just don't want to require RemoteController's support it public void changeNextTrack(DownloadFile song) {} public boolean isNextSupported() { - return this.nextSupported; + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK, true)) { + return this.nextSupported; + } else { + return false; + } } public abstract void setVolume(int volume); public abstract void updateVolume(boolean up); @@ -99,9 +117,62 @@ public abstract class RemoteController { protected WebProxy createWebProxy() { MusicService musicService = MusicServiceFactory.getMusicService(downloadService); if(musicService instanceof CachedMusicService) { - return new WebProxy(downloadService, ((CachedMusicService)musicService).getMusicService().getHttpClient()); + RESTMusicService restMusicService = ((CachedMusicService)musicService).getMusicService(); + return new WebProxy(downloadService, restMusicService.getSSLSocketFactory(), restMusicService.getHostNameVerifier()); } else { return new WebProxy(downloadService); } } + + protected String getStreamUrl(MusicService musicService, DownloadFile downloadFile) throws Exception { + MusicDirectory.Entry song = downloadFile.getSong(); + + String url; + // In offline mode or playing offline song + if(downloadFile.isStream()) { + url = downloadFile.getStream(); + } else if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) { + if(proxy == null) { + proxy = new FileProxy(downloadService); + proxy.start(); + } + + // Offline song + if(song.getId().indexOf(rootLocation) != -1) { + url = proxy.getPublicAddress(song.getId()); + } else { + // Playing online song in offline mode + url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath()); + } + } else { + // Check if we want a proxy going still + if(Util.isCastProxy(downloadService)) { + if(proxy instanceof FileProxy) { + proxy.stop(); + proxy = null; + } + + if(proxy == null) { + proxy = createWebProxy(); + proxy.start(); + } + } else if(proxy != null) { + proxy.stop(); + proxy = null; + } + + if(song.isVideo()) { + url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService); + } else { + url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate()); + } + + // If proxy is going, it is a WebProxy + if(proxy != null) { + url = proxy.getPublicAddress(url); + } + } + + return url; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java index 1d9fecef..c7ad639e 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java +++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java @@ -3,6 +3,8 @@ package github.daneren2005.dsub.service; import android.content.Context; import android.util.Log; +import github.daneren2005.dsub.domain.InternetRadioStation; +import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.SongDBHandler; @@ -21,18 +23,18 @@ public class Scrobbler { private String lastSubmission; private String lastNowPlaying; - public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration) { + public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration, boolean isPastCutoff) { // More than 4 minutes if(playerPosition > FOUR_MINUTES) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } // More than 50% played else if(duration > 0 && playerPosition > (duration / 2)) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } } - public void scrobble(final Context context, final DownloadFile song, final boolean submission) { + public void scrobble(final Context context, final DownloadFile song, final boolean submission, final boolean isPastCutoff) { if(song == null) { return; } @@ -55,7 +57,9 @@ public class Scrobbler { new SilentBackgroundTask<Void>(context) { @Override protected Void doInBackground() { - SongDBHandler.getHandler(context).setSongPlayed(song, submission); + if(isPastCutoff) { + SongDBHandler.getHandler(context).setSongPlayed(song, submission); + } // Scrobbling disabled if (!Util.isScrobblingEnabled(context)) { @@ -66,7 +70,7 @@ public class Scrobbler { return null; } // Ignore podcasts - else if(song.getSong() instanceof PodcastEpisode) { + else if(song.getSong() instanceof PodcastEpisode || song.getSong() instanceof InternetRadioStation) { return null; } diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java index 664adcfb..d4c090c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java @@ -18,6 +18,7 @@ */ package github.daneren2005.dsub.service.parser; +import java.io.IOException; import java.io.Reader; import org.xmlpull.v1.XmlPullParser; @@ -36,6 +37,12 @@ import github.daneren2005.dsub.util.Util; */ public abstract class AbstractParser { private static final String TAG = AbstractParser.class.getSimpleName(); + private static final String SUBSONIC_RESPONSE = "subsonic-response"; + private static final String MADSONIC_RESPONSE = "madsonic-response"; + private static final String SUBSONIC = "subsonic"; + private static final String MADSONIC = "madsonic"; + private static final String AMPACHE = "ampache"; + protected final Context context; protected final int instance; private XmlPullParser parser; @@ -66,6 +73,11 @@ public abstract class AbstractParser { case 40: message = context.getResources().getString(R.string.parser_not_authenticated); break; + case 41: + Util.setBlockTokenUse(context, instance, true); + + // Throw IOException so RESTMusicService knows to retry + throw new IOException(); case 50: message = context.getResources().getString(R.string.parser_not_authorized); break; @@ -127,21 +139,32 @@ public abstract class AbstractParser { } protected int nextParseEvent() throws Exception { - return parser.next(); + try { + return parser.next(); + } catch(Exception e) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + + throw e; + } } protected String getElementName() { String name = parser.getName(); - if ("subsonic-response".equals(name) || "madsonic-response".equals(name)) { + if (SUBSONIC_RESPONSE.equals(name) || MADSONIC_RESPONSE.equals(name)) { rootElementFound = true; String version = get("version"); if (version != null) { ServerInfo server = new ServerInfo(); server.setRestVersion(new Version(version)); - if("madsonic".equals(get("type")) || "madsonic-response".equals(name)) { + if(MADSONIC.equals(get("type")) || MADSONIC_RESPONSE.equals(name)) { server.setRestType(ServerInfo.TYPE_MADSONIC); - } else if("subsonic".equals(get("type")) && server.checkServerVersion(context, "1.13")) { + } if(AMPACHE.equals(get("type"))) { + server.setRestType(ServerInfo.TYPE_AMPACHE); + } else if(SUBSONIC.equals(get("type")) && server.checkServerVersion(context, "1.13")) { // Oh am I going to regret this server.setRestType(ServerInfo.TYPE_MADSONIC); server.setRestVersion(new Version("2.0.0")); @@ -154,6 +177,11 @@ public abstract class AbstractParser { protected void validate() throws Exception { if (!rootElementFound) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + throw new Exception(context.getResources().getString(R.string.background_task_parse_error)); } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java index 773c241b..f91aaae1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java @@ -29,9 +29,9 @@ import java.io.Reader; /** * @author Sindre Mehus */ -public class AlbumListParser extends MusicDirectoryEntryParser { +public class EntryListParser extends MusicDirectoryEntryParser { - public AlbumListParser(Context context, int instance) { + public EntryListParser(Context context, int instance) { super(context, instance); } @@ -46,7 +46,12 @@ public class AlbumListParser extends MusicDirectoryEntryParser { String name = getElementName(); if ("album".equals(name)) { MusicDirectory.Entry entry = parseEntry(""); - entry.setDirectory(true); + if(get("isDir") == null) { + entry.setDirectory(true); + } + dir.addChild(entry); + } else if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); dir.addChild(entry); } else if ("error".equals(name)) { handleError(); diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java index afb05928..1b389f80 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java @@ -27,7 +27,6 @@ import java.io.Reader; * @author Sindre Mehus */ public class ErrorParser extends AbstractParser { - public ErrorParser(Context context, int instance) { super(context, instance); } @@ -45,5 +44,6 @@ public class ErrorParser extends AbstractParser { } while (eventType != XmlPullParser.END_DOCUMENT); validate(); + reader.close(); } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java new file mode 100644 index 00000000..77d7bc4a --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java @@ -0,0 +1,63 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.domain.InternetRadioStation; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +public class InternetRadioStationParser extends ErrorParser { + public InternetRadioStationParser(Context context, int instance) { + super(context, instance); + } + + public List<InternetRadioStation> parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List<InternetRadioStation> result = new ArrayList<>(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("internetRadioStation".equals(name)) { + InternetRadioStation station = new InternetRadioStation(); + + station.setId(get("id")); + station.setTitle(get("name")); + station.setStreamUrl(get("streamUrl")); + station.setHomePageUrl(get("homePageUrl")); + + result.add(station); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + return result; + } + +}
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java index ffb3ba05..acd00661 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java @@ -21,6 +21,7 @@ import org.xmlpull.v1.XmlPullParser; import java.io.Reader; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.ProgressListener; public class ScanStatusParser extends AbstractParser { @@ -32,14 +33,23 @@ public class ScanStatusParser extends AbstractParser { public boolean parse(Reader reader, ProgressListener progressListener) throws Exception { init(reader); - Boolean started = null; + String scanName, scanningName; + if(ServerInfo.isMadsonic(context, instance)) { + scanName = "status"; + scanningName = "started"; + } else { + scanName = "scanStatus"; + scanningName = "scanning"; + } + + Boolean scanning = null; int eventType; do { eventType = nextParseEvent(); if (eventType == XmlPullParser.START_TAG) { String name = getElementName(); - if("status".equals(name)) { - started = getBoolean("started"); + if(scanName.equals(name)) { + scanning = getBoolean(scanningName); String msg = context.getResources().getString(R.string.parser_scan_count, getInteger("count")); progressListener.updateProgress(msg); @@ -51,6 +61,6 @@ public class ScanStatusParser extends AbstractParser { validate(); - return started != null && started; + return scanning != null && scanning; } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java new file mode 100644 index 00000000..2d1f43dc --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java @@ -0,0 +1,58 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; + +public class TopSongsParser extends MusicDirectoryEntryParser { + + public TopSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + int customOrder = 1; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setCustomOrder(customOrder); + dir.addChild(entry); + + customOrder++; + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +}
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java index e20556c0..fc2ddd7e 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java @@ -16,6 +16,7 @@ package github.daneren2005.dsub.service.parser; import android.content.Context; +import android.util.Log; import org.xmlpull.v1.XmlPullParser; @@ -23,10 +24,16 @@ import java.io.Reader; import java.util.ArrayList; import java.util.List; +import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; +import github.daneren2005.dsub.domain.User.Setting; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ProgressListener; public class UserParser extends AbstractParser { + private static final String TAG = UserParser.class.getSimpleName(); public UserParser(Context context, int instance) { super(context, instance); @@ -35,14 +42,17 @@ public class UserParser extends AbstractParser { public List<User> parse(Reader reader, ProgressListener progressListener) throws Exception { init(reader); List<User> result = new ArrayList<User>(); + List<MusicFolder> musicFolders = null; + User user = null; int eventType; + String tagName = null; do { eventType = nextParseEvent(); if (eventType == XmlPullParser.START_TAG) { - String name = getElementName(); - if ("user".equals(name)) { - User user = new User(); + tagName = getElementName(); + if ("user".equals(tagName)) { + user = new User(); user.setUsername(get("username")); user.setEmail(get("email")); @@ -53,9 +63,31 @@ public class UserParser extends AbstractParser { parseSetting(user, User.LASTFM); result.add(user); - } else if ("error".equals(name)) { + } else if ("error".equals(tagName)) { handleError(); } + } else if(eventType == XmlPullParser.TEXT) { + if("folder".equals(tagName)) { + String id = getText(); + if(musicFolders == null) { + musicFolders = getMusicFolders(); + } + + if(user != null) { + if(user.getMusicFolderSettings() == null) { + for (MusicFolder musicFolder : musicFolders) { + user.addMusicFolder(musicFolder); + } + } + + for(Setting musicFolder: user.getMusicFolderSettings()) { + if(musicFolder.getName().equals(id)) { + musicFolder.setValue(true); + break; + } + } + } + } } } while (eventType != XmlPullParser.END_DOCUMENT); @@ -63,6 +95,11 @@ public class UserParser extends AbstractParser { return result; } + + private List<MusicFolder> getMusicFolders() throws Exception{ + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getMusicFolders(false, context, null); + } private void parseSetting(User user, String name) { String value = get(name); diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java deleted file mode 100644 index 830950c8..00000000 --- a/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java +++ /dev/null @@ -1,553 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package github.daneren2005.dsub.service.ssl; - -import android.os.Build; -import android.util.Log; - -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.conn.scheme.HostNameResolver; -import org.apache.http.conn.scheme.LayeredSocketFactory; -import org.apache.http.conn.ssl.AllowAllHostnameVerifier; -import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import org.apache.http.conn.ssl.X509HostnameVerifier; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import java.io.IOException; -import java.lang.reflect.Array; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.Security; -import java.security.UnrecoverableKeyException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Layered socket factory for TLS/SSL connections. - * <p> - * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of - * trusted certificates and to authenticate to the HTTPS server using a private key. - * <p> - * SSLSocketFactory will enable server authentication when supplied with - * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client - * secure socket will reject the connection during the SSL session handshake if the target HTTPS - * server attempts to authenticate itself with a non-trusted certificate. - * <p> - * Use JDK keytool utility to import a trusted certificate and generate a trust-store file: - * <pre> - * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore - * </pre> - * <p> - * In special cases the standard trust verification process can be bypassed by using a custom - * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed - * certificates to be accepted as trusted without having to add them to the trust-store file. - * <p> - * The following parameters can be used to customize the behavior of this - * class: - * <ul> - * <li>{@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li> - * <li>{@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}</li> - * </ul> - * <p> - * SSLSocketFactory will enable client authentication when supplied with - * a {@link KeyStore key-store} file containing a private key/public certificate - * pair. The client secure socket will use the private key to authenticate - * itself to the target HTTPS server during the SSL session handshake if - * requested to do so by the server. - * The target HTTPS server will in its turn verify the certificate presented - * by the client in order to establish client's authenticity - * <p> - * Use the following sequence of actions to generate a key-store file - * </p> - * <ul> - * <li> - * <p> - * Use JDK keytool utility to generate a new key - * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre> - * For simplicity use the same password for the key as that of the key-store - * </p> - * </li> - * <li> - * <p> - * Issue a certificate signing request (CSR) - * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Send the certificate request to the trusted Certificate Authority for signature. - * One may choose to act as her own CA and sign the certificate request using a PKI - * tool, such as OpenSSL. - * </p> - * </li> - * <li> - * <p> - * Import the trusted CA root certificate - * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Import the PKCS#7 file containg the complete certificate chain - * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Verify the content the resultant keystore file - * <pre>keytool -list -v -keystore my.keystore</pre> - * </p> - * </li> - * </ul> - * - * @since 4.0 - */ -public class SSLSocketFactory implements LayeredSocketFactory { - private static final String TAG = SSLSocketFactory.class.getSimpleName(); - public static final String TLS = "TLS"; - - public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER - = new AllowAllHostnameVerifier(); - - public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER - = new BrowserCompatHostnameVerifier(); - - public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER - = new StrictHostnameVerifier(); - - /** - * The default factory using the default JVM settings for secure connections. - */ - private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory(); - - /** - * Gets the default factory, which uses the default JVM settings for secure - * connections. - * - * @return the default factory - */ - public static SSLSocketFactory getSocketFactory() { - return DEFAULT_FACTORY; - } - - private final javax.net.ssl.SSLSocketFactory socketfactory; - private final HostNameResolver nameResolver; - // TODO: make final - private volatile X509HostnameVerifier hostnameVerifier; - - private static SSLContext createSSLContext( - String algorithm, - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore, - final SecureRandom random, - final TrustStrategy trustStrategy) - throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { - if (algorithm == null) { - algorithm = TLS; - } - KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( - KeyManagerFactory.getDefaultAlgorithm()); - kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null); - KeyManager[] keymanagers = kmfactory.getKeyManagers(); - TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - tmfactory.init(keystore); - TrustManager[] trustmanagers = tmfactory.getTrustManagers(); - if (trustmanagers != null && trustStrategy != null) { - for (int i = 0; i < trustmanagers.length; i++) { - TrustManager tm = trustmanagers[i]; - if (tm instanceof X509TrustManager) { - trustmanagers[i] = new TrustManagerDecorator( - (X509TrustManager) tm, trustStrategy); - } - } - } - - SSLContext sslcontext = SSLContext.getInstance(algorithm); - sslcontext.init(keymanagers, trustmanagers, random); - return sslcontext; - } - - /** - * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)} - */ - @Deprecated - public SSLSocketFactory( - final String algorithm, - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore, - final SecureRandom random, - final HostNameResolver nameResolver) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(createSSLContext( - algorithm, keystore, keystorePassword, truststore, random, null), - nameResolver); - } - - /** - * @since 4.1 - */ - public SSLSocketFactory( - String algorithm, - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore, - final SecureRandom random, - final X509HostnameVerifier hostnameVerifier) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(createSSLContext( - algorithm, keystore, keystorePassword, truststore, random, null), - hostnameVerifier); - } - - /** - * @since 4.1 - */ - public SSLSocketFactory( - String algorithm, - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore, - final SecureRandom random, - final TrustStrategy trustStrategy, - final X509HostnameVerifier hostnameVerifier) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(createSSLContext( - algorithm, keystore, keystorePassword, truststore, random, trustStrategy), - hostnameVerifier); - } - - public SSLSocketFactory( - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - } - - public SSLSocketFactory( - final KeyStore keystore, - final String keystorePassword) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{ - this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - } - - public SSLSocketFactory( - final KeyStore truststore) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - } - - /** - * @since 4.1 - */ - public SSLSocketFactory( - final TrustStrategy trustStrategy, - final X509HostnameVerifier hostnameVerifier) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(TLS, null, null, null, null, trustStrategy, hostnameVerifier); - } - - /** - * @since 4.1 - */ - public SSLSocketFactory( - final TrustStrategy trustStrategy) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { - this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - } - - public SSLSocketFactory(final SSLContext sslContext) { - this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - } - - /** - * @deprecated Use {@link #SSLSocketFactory(SSLContext)} - */ - @Deprecated - public SSLSocketFactory( - final SSLContext sslContext, final HostNameResolver nameResolver) { - super(); - this.socketfactory = sslContext.getSocketFactory(); - this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; - this.nameResolver = nameResolver; - } - - /** - * @since 4.1 - */ - public SSLSocketFactory( - final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) { - super(); - this.socketfactory = sslContext.getSocketFactory(); - this.hostnameVerifier = hostnameVerifier; - this.nameResolver = null; - } - - private SSLSocketFactory() { - super(); - this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory(); - this.hostnameVerifier = null; - this.nameResolver = null; - } - - /** - * @param params Optional parameters. Parameters passed to this method will have no effect. - * This method will create a unconnected instance of {@link Socket} class - * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method. - * @since 4.1 - */ - @SuppressWarnings("cast") - public Socket createSocket(final HttpParams params) throws IOException { - // the cast makes sure that the factory is working as expected - SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); - sslSocket.setEnabledProtocols(getProtocols(sslSocket)); - sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); - return sslSocket; - } - - @SuppressWarnings("cast") - public Socket createSocket() throws IOException { - // the cast makes sure that the factory is working as expected - SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); - sslSocket.setEnabledProtocols(getProtocols(sslSocket)); - sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); - return sslSocket; - } - - /** - * @since 4.1 - */ - public Socket connectSocket( - final Socket sock, - final InetSocketAddress remoteAddress, - final InetSocketAddress localAddress, - final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { - if (remoteAddress == null) { - throw new IllegalArgumentException("Remote address may not be null"); - } - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket()); - if (localAddress != null) { -// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params)); - sslsock.bind(localAddress); - } - - setHostName(sslsock, remoteAddress.getHostName()); - int connTimeout = HttpConnectionParams.getConnectionTimeout(params); - int soTimeout = HttpConnectionParams.getSoTimeout(params); - - try { - sslsock.connect(remoteAddress, connTimeout); - } catch (SocketTimeoutException ex) { - throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/" - + remoteAddress.getAddress() + " timed out"); - } - sslsock.setSoTimeout(soTimeout); - if (this.hostnameVerifier != null) { - try { - this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock); - // verifyHostName() didn't blowup - good! - } catch (IOException iox) { - // close the socket before re-throwing the exception - try { sslsock.close(); } catch (Exception x) { /*ignore*/ } - throw iox; - } - } - return sslsock; - } - - - /** - * Checks whether a socket connection is secure. - * This factory creates TLS/SSL socket connections - * which, by default, are considered secure. - * <br/> - * Derived classes may override this method to perform - * runtime checks, for example based on the cypher suite. - * - * @param sock the connected socket - * - * @return <code>true</code> - * - * @throws IllegalArgumentException if the argument is invalid - */ - public boolean isSecure(final Socket sock) throws IllegalArgumentException { - if (sock == null) { - throw new IllegalArgumentException("Socket may not be null"); - } - // This instanceof check is in line with createSocket() above. - if (!(sock instanceof SSLSocket)) { - throw new IllegalArgumentException("Socket not created by this factory"); - } - // This check is performed last since it calls the argument object. - if (sock.isClosed()) { - throw new IllegalArgumentException("Socket is closed"); - } - return true; - } - - /** - * @since 4.1 - */ - public Socket createLayeredSocket( - final Socket socket, - final String host, - final int port, - final boolean autoClose) throws IOException, UnknownHostException { - SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket( - socket, - host, - port, - autoClose - ); - sslSocket.setEnabledProtocols(getProtocols(sslSocket)); - sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); - if (this.hostnameVerifier != null) { - this.hostnameVerifier.verify(host, sslSocket); - } - // verifyHostName() didn't blowup - good! - return sslSocket; - } - - @Deprecated - public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) { - if ( hostnameVerifier == null ) { - throw new IllegalArgumentException("Hostname verifier may not be null"); - } - this.hostnameVerifier = hostnameVerifier; - } - - public X509HostnameVerifier getHostnameVerifier() { - return this.hostnameVerifier; - } - - /** - * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)} - */ - @Deprecated - public Socket connectSocket( - final Socket socket, - final String host, int port, - final InetAddress localAddress, int localPort, - final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { - InetSocketAddress local = null; - if (localAddress != null || localPort > 0) { - // we need to bind explicitly - if (localPort < 0) { - localPort = 0; // indicates "any" - } - local = new InetSocketAddress(localAddress, localPort); - } - InetAddress remoteAddress; - if (this.nameResolver != null) { - remoteAddress = this.nameResolver.resolve(host); - } else { - remoteAddress = InetAddress.getByName(host); - } - InetSocketAddress remote = new InetSocketAddress(remoteAddress, port); - return connectSocket(socket, remote, local, params); - } - - /** - * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)} - */ - @Deprecated - public Socket createSocket( - final Socket socket, - final String host, int port, - boolean autoClose) throws IOException, UnknownHostException { - SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(socket, host, port, autoClose); - sslSocket.setEnabledProtocols(getProtocols(sslSocket)); - sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); - setHostName(sslSocket, host); - return sslSocket; - } - - private void setHostName(SSLSocket sslsock, String hostname){ - try { - java.lang.reflect.Method setHostnameMethod = sslsock.getClass().getMethod("setHostname", String.class); - setHostnameMethod.invoke(sslsock, hostname); - } catch (Exception e) { - Log.w(TAG, "SNI not useable", e); - } - } - - private String[] getProtocols(SSLSocket sslSocket) { - String[] protocols = sslSocket.getEnabledProtocols(); - - // Remove SSLv3 if it is not the only option - if(protocols.length > 1) { - List<String> protocolList = new ArrayList(Arrays.asList(protocols)); - protocolList.remove("SSLv3"); - protocols = protocolList.toArray(new String[protocolList.size()]); - } - - return protocols; - } - - private String[] getCiphers(SSLSocket sslSocket) { - String[] ciphers = sslSocket.getEnabledCipherSuites(); - - List<String> enabledCiphers = new ArrayList(Arrays.asList(ciphers)); - // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers - // Issue seems to have been fixed in M, and now won't work without them. Because Google - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { - enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); - enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"); - } - - ciphers = enabledCiphers.toArray(new String[enabledCiphers.size()]); - return ciphers; - } -} diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java deleted file mode 100644 index f2364368..00000000 --- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ -package github.daneren2005.dsub.service.ssl; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.X509TrustManager; - - -/** - * @since 4.1 - */ -class TrustManagerDecorator implements X509TrustManager { - - private final X509TrustManager trustManager; - private final TrustStrategy trustStrategy; - - TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) { - super(); - this.trustManager = trustManager; - this.trustStrategy = trustStrategy; - } - - public void checkClientTrusted( - final X509Certificate[] chain, final String authType) throws CertificateException { - this.trustManager.checkClientTrusted(chain, authType); - } - - public void checkServerTrusted( - final X509Certificate[] chain, final String authType) throws CertificateException { - if (!this.trustStrategy.isTrusted(chain, authType)) { - this.trustManager.checkServerTrusted(chain, authType); - } - } - - public X509Certificate[] getAcceptedIssuers() { - return this.trustManager.getAcceptedIssuers(); - } - -} diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java deleted file mode 100644 index 637a8931..00000000 --- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ -package github.daneren2005.dsub.service.ssl; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/** - * A trust strategy that accepts self-signed certificates as trusted. Verification of all other - * certificates is done by the trust manager configured in the SSL context. - * - * @since 4.1 - */ -public class TrustSelfSignedStrategy implements TrustStrategy { - - public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { - return true; - } - -} diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java deleted file mode 100644 index 334a97c5..00000000 --- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ -package github.daneren2005.dsub.service.ssl; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/** - * A strategy to establish trustworthiness of certificates without consulting the trust manager - * configured in the actual SSL context. This interface can be used to override the standard - * JSSE certificate verification process. - * - * @since 4.1 - */ -public interface TrustStrategy { - - /** - * Determines whether the certificate chain can be trusted without consulting the trust manager - * configured in the actual SSL context. This method can be used to override the standard JSSE - * certificate verification process. - * <p> - * Please note that, if this method returns <code>false</code>, the trust manager configured - * in the actual SSL context can still clear the certificate as trusted. - * - * @param chain the peer certificate chain - * @param authType the authentication type based on the client certificate - * @return <code>true</code> if the certificate can be trusted without verification by - * the trust manager, <code>false</code> otherwise. - * @throws CertificateException thrown if the certificate is not trusted or invalid. - */ - boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException; - -} diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java index 8da83be1..bcb7b92f 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java @@ -49,7 +49,7 @@ public class MostRecentSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = SyncUtil.getSyncedMostRecent(context, instance); MusicDirectory albumList = musicService.getAlbumList("newest", 20, 0, tagBrowsing, context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java index a0996628..cb3c3877 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java @@ -56,7 +56,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { String serverName = Util.getServerName(context, instance); List<Playlist> remainder = null; @@ -69,6 +69,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { ArrayList<SyncSet> playlistList = SyncUtil.getSyncedPlaylists(context, instance); List<String> updated = new ArrayList<String>(); + String updatedId = null; boolean removed = false; for(int i = 0; i < playlistList.size(); i++) { SyncSet cachedPlaylist = playlistList.get(i); @@ -94,9 +95,13 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { DownloadFile file = new DownloadFile(context, entry, true); String path = file.getCompleteFile().getPath(); while(!file.isSaved() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(file.isSaved() && !updated.contains(playlist.getName())) { updated.add(playlist.getName()); + if(updatedId == null) { + updatedId = playlist.getId(); + } } } @@ -147,7 +152,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } if(updated.size() > 0) { - Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated), updatedId); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java index 985a7267..7afcad25 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java @@ -23,7 +23,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.util.Log; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; @@ -54,7 +53,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance); try { @@ -68,6 +67,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } List<String> updated = new ArrayList<String>(); + String updatedId = null; for(int i = 0; i < podcastList.size(); i++) { SyncSet set = podcastList.get(i); String id = set.id; @@ -80,6 +80,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) { DownloadFile file = new DownloadFile(context, entry, false); while(!file.isCompleteFileAvailable() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); } // Only add if actualy downloaded correctly @@ -87,6 +88,9 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { existingEpisodes.add(entry.getId()); if(!updated.contains(podcasts.getName())) { updated.add(podcasts.getName()); + if(updatedId == null) { + updatedId = podcasts.getId(); + } } } } @@ -104,7 +108,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { // Make sure there are is at least one change before re-syncing if(updated.size() > 0) { FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance)); - Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated), updatedId); } } catch(Exception e) { Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance)); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java index cf985227..0af8886b 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java @@ -20,7 +20,6 @@ package github.daneren2005.dsub.service.sync; import android.annotation.TargetApi; -import android.app.Notification; import android.content.Context; import android.util.Log; @@ -50,7 +49,7 @@ public class StarredSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = new ArrayList<String>(); MusicDirectory starredList = musicService.getStarredList(context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java index 661f126d..4879d032 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java @@ -65,39 +65,54 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - - // Don't try to sync if no network! - if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { - Log.w(TAG, "Not running sync, not connected to network"); + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + Log.w(TAG, "Not running sync: " + invalidMessage); return; } - + // Make sure battery > x% or is charging IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = context.registerReceiver(null, intentFilter); int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - if(status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { + if (status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); - - if((level / (float)scale) < 0.15) { + + if ((level / (float) scale) < 0.15) { Log.w(TAG, "Not running sync, battery too low"); return; } } + executeSync(context); + } + + private String isNetworkValid() { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + + // Don't try to sync if no network! + if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { + return "Not connected to any network"; + } + // Check if user wants to only sync on wifi SharedPreferences prefs = Util.getPreferences(context); if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) { if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - executeSync(context); + return null; } else { - Log.w(TAG, "Not running sync, not connected to wifi"); + return "Not connected to WIFI"; } } else { - executeSync(context); + return null; + } + } + protected void throwIfNetworkInvalid() throws NetworkNotValidException { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + throw new NetworkNotValidException(invalidMessage); } } @@ -106,32 +121,39 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { Log.i(TAG, "Running sync for " + className); long start = System.currentTimeMillis(); int servers = Util.getServerCount(context); - for(int i = 1; i <= servers; i++) { - try { - if(isValidServer(context, i) && Util.isSyncEnabled(context, i)) { - tagBrowsing = Util.isTagBrowsing(context, i); - musicService.setInstance(i); - onExecuteSync(context, i); - } else { - Log.i(TAG, "Skipped sync for " + i); + try { + for (int i = 1; i <= servers; i++) { + try { + throwIfNetworkInvalid(); + + if (isValidServer(context, i) && Util.isSyncEnabled(context, i)) { + tagBrowsing = Util.isTagBrowsing(context, i); + musicService.setInstance(i); + onExecuteSync(context, i); + } else { + Log.i(TAG, "Skipped sync for " + i); + } + } catch (Exception e) { + Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } - } catch(Exception e) { - Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } + } catch (NetworkNotValidException e) { + Log.e(TAG, "Stopped sync due to network loss", e); } Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms"); } - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { } - protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception { + protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException { boolean downloaded = false; for (MusicDirectory.Entry song: parent.getChildren(false, true)) { if (!song.isVideo()) { DownloadFile file = new DownloadFile(context, song, save); while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(!file.isFailed()) { downloaded = true; @@ -171,4 +193,10 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { String url = Util.getRestUrl(context, "null", instance, false); return !(url.contains("demo.subsonic.org") || url.contains("yourhost")); } + + public class NetworkNotValidException extends Throwable { + public NetworkNotValidException(String reason) { + super("Not running sync: " + reason); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java index a2870941..2dabb624 100644 --- a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java +++ b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java @@ -37,13 +37,18 @@ public class Updater { protected Context context; public Updater(int version) { + // 5.2 should show as 520 instead of 52 + if(version < 100) { + version *= 10; + } this.version = version; } public void checkUpdates(Context context) { this.context = context; List<Updater> updaters = new ArrayList<Updater>(); - updaters.add(new Updater403()); + updaters.add(new UpdaterSongPress()); + updaters.add(new UpdaterNoDLNA()); SharedPreferences prefs = Util.getPreferences(context); int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0); diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java deleted file mode 100644 index 4f2cbf43..00000000 --- a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java +++ /dev/null @@ -1,58 +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 github.daneren2005.dsub.updates; - -import android.content.Context; -import android.util.Log; -import github.daneren2005.dsub.updates.Updater; -import github.daneren2005.dsub.util.Constants; -import github.daneren2005.dsub.util.FileUtil; -import java.io.File; - -/** - * - * @author Scott - */ -public class Updater403 extends Updater { - public Updater403() { - super(403); - TAG = Updater403.class.getSimpleName(); - } - - @Override - public void update(Context context) { - // Rename cover.jpeg to cover.jpg - Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg"); - File dir = FileUtil.getMusicDirectory(context); - if(dir != null) { - moveArt(dir); - } - } - - private void moveArt(File dir) { - for(File file: dir.listFiles()) { - if(file.isDirectory()) { - moveArt(file); - } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) { - File renamed = new File(dir, Constants.ALBUM_ART_FILE); - file.renameTo(renamed); - } - } - } -} diff --git a/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java new file mode 100644 index 00000000..a060c4fd --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java @@ -0,0 +1,41 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.updates; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; + +public class UpdaterNoDLNA extends Updater { + public UpdaterNoDLNA() { + super(534); + TAG = this.getClass().getSimpleName(); + } + + @Override + public void update(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED, false); + editor.commit(); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java new file mode 100644 index 00000000..7efa18e4 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java @@ -0,0 +1,42 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.updates; + +import android.content.Context; +import android.content.SharedPreferences; + +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; + +public class UpdaterSongPress extends Updater { + public UpdaterSongPress() { + super(521); + TAG = this.getClass().getSimpleName(); + } + + @Override + public void update(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + boolean playNowAfter = prefs.getBoolean("playNowAfter", true); + + // Migrate the old preference so behavior stays the same + if(playNowAfter == false) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "single"); + editor.commit(); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java index 2af468f6..bdd961b4 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java @@ -66,7 +66,6 @@ public class ArtistRadioBuffer { buffer.clear(); } - context.clear(); this.artistId = artistId; awaitingResults = true; refill(); @@ -108,7 +107,7 @@ public class ArtistRadioBuffer { } private void refill() { - if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { + if (buffer != null && executorService != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { executorService.shutdown(); return; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java index 18f245d5..2b0c6279 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java +++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.xmlpull.v1.XmlPullParserException; @@ -54,6 +55,7 @@ public abstract class BackgroundTask<T> implements ProgressListener { private static final Collection<Thread> threads = Collections.synchronizedCollection(new ArrayList<Thread>()); protected static final BlockingQueue<BackgroundTask.Task> queue = new LinkedBlockingQueue<BackgroundTask.Task>(10); private static Handler handler = null; + private static AtomicInteger currentlyRunning = new AtomicInteger(0); static { try { handler = new Handler(Looper.getMainLooper()); @@ -71,6 +73,11 @@ public abstract class BackgroundTask<T> implements ProgressListener { threads.add(thread); thread.start(); } + } else if(currentlyRunning.get() >= threads.size()) { + Log.w(TAG, "Emergency add new thread: " + (threads.size() + 1)); + Thread thread = new Thread(new TaskRunnable(), String.format("BackgroundTask_%d", threads.size())); + threads.add(thread); + thread.start(); } if(handler == null) { try { @@ -176,7 +183,7 @@ public abstract class BackgroundTask<T> implements ProgressListener { } @Override - public void updateCache() { + public void updateCache(int changeCode) { } @@ -208,8 +215,18 @@ public abstract class BackgroundTask<T> implements ProgressListener { handler.post(new Runnable() { @Override public void run() { - if(!isCancelled()) { - onDone(result); + if (!isCancelled()) { + try { + onDone(result); + } catch (Throwable t) { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + } } taskStart.set(false); @@ -294,22 +311,30 @@ public abstract class BackgroundTask<T> implements ProgressListener { @Override public void run() { Looper.prepare(); + final Thread currentThread = Thread.currentThread(); while(running) { try { Task task = queue.take(); + currentlyRunning.incrementAndGet(); task.execute(); } catch(InterruptedException stop) { Log.e(TAG, "Thread died"); running = false; - threads.remove(Thread.currentThread()); } catch(Throwable t) { Log.e(TAG, "Unexpected crash in BackgroundTask thread", t); + running = false; } + + currentlyRunning.decrementAndGet(); + } + + if(threads.contains(currentThread)) { + threads.remove(currentThread); } } } - public static interface OnCancelListener { + public interface OnCancelListener { void onCancel(); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java index 89e7de3b..21adce8c 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java @@ -67,6 +67,7 @@ public final class Constants { public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks"; public static final String INTENT_EXTRA_SHOW_ALL = "showAll"; public static final String INTENT_EXTRA_PLAY_LAST = "playLast"; + public static final String INTENT_EXTRA_ENTRY = "passedEntry"; // Preferences keys. public static final String PREFERENCES_KEY_SERVER_KEY = "server"; @@ -133,6 +134,7 @@ public final class Constants { public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget"; public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled"; public static final String PREFERENCES_KEY_BOOKMARKS_ENABLED = "bookmarksEnabled"; + public static final String PREFERENCES_KEY_INTERNET_RADIO_ENABLED = "internetRadioEnabled"; public static final String PREFERENCES_KEY_CUSTOM_SORT_ENABLED = "customSortEnabled"; public static final String PREFERENCES_KEY_MENU_PLAY_NOW = "showPlayNow"; public static final String PREFERENCES_KEY_MENU_PLAY_SHUFFLED = "showPlayShuffled"; @@ -147,7 +149,8 @@ public final class Constants { public static final String PREFERENCES_KEY_BROWSE_TAGS = "browseTags"; public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab"; public static final String PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE = "overrideSystemLanguage"; - public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + // public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + public static final String PREFERENCES_KEY_SONG_PRESS_ACTION = "songPressAction"; public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt"; public static final String PREFERENCES_KEY_ADMIN_ENABLED = "adminEnabled"; public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName"; @@ -168,6 +171,14 @@ public final class Constants { public static final String PREFERENCES_KEY_COLOR_ACTION_BAR = "colorActionBar"; public static final String PREFERENCES_KEY_SHUFFLE_BY_ALBUM = "shuffleByAlbum"; public static final String PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER = "neverResumePlayQueue"; + public static final String PREFERENCES_KEY_BATCH_MODE = "batchMode"; + public static final String PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK = "castingGaplessPlayback"; + public static final String PREFERENCES_KEY_CAST_STREAM_ORIGINAL = "castStreamOriginal"; + public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification"; + public static final String PREFERENCES_KEY_CAST_CACHE = "castCache"; + public static final String PREFERENCES_KEY_PLAYBACK_SPEED = "playbackSpeed"; + public static final String PREFERENCES_KEY_SONG_PLAYBACK_SPEED = "songPlaybackSpeed"; + public static final String PREFERENCES_KEY_DLNA_CASTING_ENABLED = "dlnaCastingEnabled"; public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount"; public static final String OFFLINE_SCROBBLE_ID = "scrobbleID"; @@ -180,10 +191,13 @@ public final class Constants { public static final String CACHE_KEY_IGNORE = "ignoreArticles"; public static final String CACHE_AUDIO_SESSION_ID = "audioSessionId"; + public static final String CACHE_AUDIO_SESSION_VERSION_CODE = "audioSessionVersionCode"; + public static final String CACHE_BLOCK_TOKEN_USE = "blockTokenUse"; public static final String MAIN_BACK_STACK = "backStackIds"; public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize"; public static final String MAIN_NOW_PLAYING = "nowPlayingId"; + public static final String MAIN_NOW_PLAYING_SECONDARY = "nowPlayingSecondaryId"; public static final String MAIN_SLIDE_PANEL_STATE = "slidePanelState"; public static final String FRAGMENT_LIST = "fragmentList"; public static final String FRAGMENT_LIST2 = "fragmentList2"; diff --git a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java index 2da72579..f03906a8 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java +++ b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.support.annotation.AttrRes; +import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; import android.util.TypedValue; @@ -48,6 +49,17 @@ public class DrawableTint { tintedDrawables.put(drawableRes, background); return background; } + public static Drawable getTintedDrawableFromColor(Context context, @DrawableRes int drawableRes, @ColorRes int colorRes) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = context.getResources().getColor(colorRes); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } public static int getColorRes(Context context, @AttrRes int colorAttr) { int color; if(attrMap.containsKey(colorAttr)) { @@ -83,7 +95,7 @@ public class DrawableTint { return getTintedDrawable(context, drawableRes, colorAttr); } - public static void wipeTintCache() { + public static void clearCache() { attrMap.clear(); tintedDrawables.clear(); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java b/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java new file mode 100644 index 00000000..8f58e60e --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java @@ -0,0 +1,82 @@ +package github.daneren2005.dsub.util; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; +import com.shehabic.droppy.DroppyClickCallbackInterface; +import com.shehabic.droppy.DroppyMenuCustomItem; + +/** + * Created by marcus on 2/14/2017. + */ +public class DroppySpeedControl extends DroppyMenuCustomItem { + + private Context context; + private SeekBar seekBar; + private DroppyClickCallbackInterface updateBarCallback; + public DroppySpeedControl(int customResourceId) { + super(customResourceId); + + } + + @Override + public View render(Context context) { + return super.render(context); + + + } + + public DroppySpeedControl setOnClicks(Context context, final DroppyClickCallbackInterface callback, int ... elementsByID){ + render(context); + View.OnClickListener listener = new View.OnClickListener() { + @Override + public void onClick(View v) { + callback.call(v, v.getId()); + } + }; + for (Integer element : elementsByID) { + renderedView.findViewById(element).setOnClickListener(listener); + } + return this; + } + + + public void updateSeekBar(float playbackSpeed){ + TextView tv = (TextView)seekBar.getTag(); + tv.setText(Float.toString(playbackSpeed)); + seekBar.setProgress((int)(playbackSpeed*10)-5); + } + + public DroppySpeedControl setOnSeekBarChangeListener(Context context, final DroppyClickCallbackInterface callback, int seekBarByID, int textViewByID, float playbackSpeed) { + updateBarCallback = callback; + render(context); + final TextView textBox = (TextView) renderedView.findViewById(textViewByID); + textBox.setText(Float.toString(playbackSpeed)); + SeekBar seekBar = ((SeekBar) renderedView.findViewById(seekBarByID)); + this.seekBar = seekBar; + seekBar.setTag(textBox); + seekBar.setProgress((int)(playbackSpeed*10)-5); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + textBox.setText(new Float((progress + 5) / 10.0).toString()); + seekBar.setProgress(progress); + callback.call(seekBar,seekBar.getId()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + seekBar.setProgress((int)((playbackSpeed/10.0) - 5)); + return this; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java new file mode 100644 index 00000000..710d5232 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java @@ -0,0 +1,21 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.util; + +public final class EnvironmentVariables { + public static final String PASTEBIN_DEV_KEY = ""; + public static final String CAST_APPLICATION_ID = ""; +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java index 85844360..2321e69e 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java @@ -43,6 +43,7 @@ import java.lang.ref.WeakReference; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.ArtistInfo; +import github.daneren2005.dsub.domain.InternetRadioStation; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.Playlist; import github.daneren2005.dsub.domain.PodcastChannel; @@ -221,8 +222,11 @@ public class ImageLoader { return loadImage(view, entry, large, size, crossfade); } public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossfade) { - // TODO: If we know this a artist, try to load artist info instead - if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) { + if(entry != null && entry instanceof InternetRadioStation) { + // Continue on and load a null bitmap + } + // If we know this a artist, try to load artist info instead + else if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) { SilentBackgroundTask task = new ArtistImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade); task.execute(); return task; @@ -524,44 +528,49 @@ public class ImageLoader { @Override protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(mContext); - ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); - String url = artistInfo.getImageUrl(); - - // Figure out whether we are going to get a artist image or the standard image - if(url != null && !"".equals(url.trim())) { - // If getting the artist image fails for any reason, retry for the standard version - subTask = new ViewUrlTask(mContext, mView, url, mSize) { - @Override - protected void failedToDownload() { - // Call loadImage so we can take advantage of all of it's logic checks - loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); - - // Delete subTask so it doesn't get called in done - subTask = null; + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); + String url = artistInfo.getImageUrl(); + + // Figure out whether we are going to get a artist image or the standard image + if (url != null && !"".equals(url.trim())) { + // If getting the artist image fails for any reason, retry for the standard version + subTask = new ViewUrlTask(mContext, mView, url, mSize) { + @Override + protected void failedToDownload() { + // Call loadImage so we can take advantage of all of it's logic checks + loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); + + // Delete subTask so it doesn't get called in done + subTask = null; + } + }; + } else { + if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { + // Try to lookup child cover art + MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); + if (firstChild != null) { + mEntry.setCoverArt(firstChild.getCoverArt()); + } } - }; - } else { - if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { - // Try to lookup child cover art - MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); - if (firstChild != null) { - mEntry.setCoverArt(firstChild.getCoverArt()); + + if (mEntry != null && mEntry.getCoverArt() != null) { + subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); + } else { + // If entry is null as well, we need to just set as a blank image + Bitmap bitmap = getUnknownImage(mEntry, mSize); + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + return null; } } - if (mEntry != null && mEntry.getCoverArt() != null) { - subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); - } else { - // If entry is null as well, we need to just set as a blank image - Bitmap bitmap = getUnknownImage(mEntry, mSize); - mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); - return null; - } + // Execute whichever way we decided to go + subTask.doInBackground(); + } catch (Throwable x) { + Log.e(TAG, "Failed to get artist info", x); + cancelled.set(true); } - - // Execute whichever way we decided to go - subTask.doInBackground(); return null; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java index 9aa54c4b..73ec6aec 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java +++ b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java @@ -48,6 +48,7 @@ public class MediaRouteManager extends MediaRouter.Callback { private MediaRouteSelector selector; private List<MediaRouteProvider> providers = new ArrayList<MediaRouteProvider>(); private List<MediaRouteProvider> onlineProviders = new ArrayList<MediaRouteProvider>(); + private DLNARouteProvider dlnaProvider; static { try { @@ -159,10 +160,8 @@ public class MediaRouteManager extends MediaRouter.Callback { addOnlineProviders(); } - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService); - router.addProvider(dlnaProvider); - providers.add(dlnaProvider); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED, true)) { + addDLNAProvider(); } } public void buildSelector() { @@ -178,4 +177,20 @@ public class MediaRouteManager extends MediaRouter.Callback { } selector = builder.build(); } + + public void addDLNAProvider() { + if(dlnaProvider == null) { + dlnaProvider = new DLNARouteProvider(downloadService); + router.addProvider(dlnaProvider); + providers.add(dlnaProvider); + } + } + public void removeDLNAProvider() { + if(dlnaProvider != null) { + router.removeProvider(dlnaProvider); + providers.remove(dlnaProvider); + dlnaProvider.destroy(); + dlnaProvider = null; + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java index 375c9966..750ab40c 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java @@ -41,6 +41,7 @@ import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.provider.DSubWidgetProvider; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.view.UpdateView; public final class Notifications { private static final String TAG = Notifications.class.getSimpleName(); @@ -66,23 +67,29 @@ public final class Notifications { notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; } boolean remote = downloadService.isRemoteEnabled(); + boolean isSingle = downloadService.isCurrentPlayingSingle(); + boolean shouldFastForward = downloadService.shouldFastForward(); if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){ RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded); - setupViews(expandedContentView ,context, song, true, playing, remote); + setupViews(expandedContentView ,context, song, true, playing, remote, isSingle, shouldFastForward); notification.bigContentView = expandedContentView; notification.priority = Notification.PRIORITY_HIGH; } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notification.visibility = Notification.VISIBILITY_PUBLIC; + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HEADS_UP_NOTIFICATION, false) && !UpdateView.hasActiveActivity()) { + notification.vibrate = new long[0]; + } } RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); - setupViews(smallContentView, context, song, false, playing, remote); + setupViews(smallContentView, context, song, false, playing, remote, isSingle, shouldFastForward); notification.contentView = smallContentView; Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); playShowing = true; @@ -93,21 +100,35 @@ public final class Notifications { public void run() { downloadService.stopForeground(true); showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size()); - downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + + try { + downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + } catch(Exception e) { + Log.e(TAG, "Failed to start notifications after stopping foreground download"); + } } }); } else { handler.post(new Runnable() { @Override public void run() { - if(playing) { - downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + if (playing) { + try { + downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); + } catch(Exception e) { + Log.e(TAG, "Failed to start notifications while playing"); + } } else { playShowing = false; persistentPlayingShowing = true; NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); downloadService.stopForeground(false); - notificationManager.notify(NOTIFICATION_ID_PLAYING, notification); + + try { + notificationManager.notify(NOTIFICATION_ID_PLAYING, notification); + } catch(Exception e) { + Log.e(TAG, "Failed to start notifications while paused"); + } } } }); @@ -117,8 +138,7 @@ public final class Notifications { DSubWidgetProvider.notifyInstances(context, downloadService, playing); } - private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){ - + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote, boolean isSingleFile, boolean shouldFastForward) { // Use the same text for the ticker and the expanded notification String title = song.getTitle(); String arist = song.getArtist(); @@ -152,49 +172,112 @@ public final class Notifications { if(persistent) { if(expanded) { rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_start); + + if(shouldFastForward) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } } else { rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_start); - rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + if(shouldFastForward) { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + } rv.setImageViewResource(R.id.control_next, R.drawable.notification_close); } + } else if(shouldFastForward) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + // Necessary for switching back since it appears to re-use the same layout + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); } // Create actions for media buttons - PendingIntent pendingIntent; - int previous = 0, pause = 0, next = 0, close = 0; - if(persistent && !expanded) { - pause = R.id.control_previous; - next = R.id.control_pause; - close = R.id.control_next; + int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 0; + if (expanded) { + if (shouldFastForward) { + rewind = R.id.control_previous; + pause = R.id.control_pause; + fastForward = R.id.control_next; + } else { + previous = R.id.control_previous; + pause = R.id.control_pause; + next = R.id.control_next; + } + + if (remote || persistent) { + close = R.id.notification_close; + rv.setViewVisibility(close, View.VISIBLE); + } } else { - previous = R.id.control_previous; - pause = R.id.control_pause; - next = R.id.control_next; + if (persistent) { + pause = R.id.control_previous; + if(shouldFastForward) { + fastForward = R.id.control_pause; + } else { + next = R.id.control_pause; + } + close = R.id.control_next; + } else { + rewind = R.id.control_previous; + pause = R.id.control_pause; + fastForward = R.id.control_next; + } } - if((remote || persistent) && close == 0 && expanded) { - close = R.id.notification_close; - rv.setViewVisibility(close, View.VISIBLE); + if(isSingleFile) { + if(previous > 0) { + rv.setViewVisibility(previous, View.GONE); + previous = 0; + } + if(rewind > 0) { + rv.setViewVisibility(rewind, View.GONE); + rewind = 0; + } + + if(next > 0) { + rv.setViewVisibility(next, View.GONE); + next = 0; + } + + if(fastForward > 0) { + rv.setViewVisibility(fastForward, View.GONE); + fastForward = 0; + } } + PendingIntent pendingIntent; if(previous > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(previous, pendingIntent); } + if(rewind > 0) { + Intent rewindIntent = new Intent("KEYCODE_MEDIA_REWIND"); + rewindIntent.setComponent(new ComponentName(context, DownloadService.class)); + rewindIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)); + pendingIntent = PendingIntent.getService(context, 0, rewindIntent, 0); + rv.setOnClickPendingIntent(rewind, pendingIntent); + } if(pause > 0) { if(playing) { Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE"); pauseIntent.setComponent(new ComponentName(context, DownloadService.class)); - pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } else { Intent prevIntent = new Intent("KEYCODE_MEDIA_START"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } @@ -202,14 +285,21 @@ public final class Notifications { if(next > 0) { Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT"); nextIntent.setComponent(new ComponentName(context, DownloadService.class)); - nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); + nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); rv.setOnClickPendingIntent(next, pendingIntent); } + if(fastForward > 0) { + Intent fastForwardIntent = new Intent("KEYCODE_MEDIA_FAST_FORWARD"); + fastForwardIntent.setComponent(new ComponentName(context, DownloadService.class)); + fastForwardIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + pendingIntent = PendingIntent.getService(context, 0, fastForwardIntent, 0); + rv.setOnClickPendingIntent(fastForward, pendingIntent); + } if(close > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(close, pendingIntent); } @@ -270,7 +360,7 @@ public final class Notifications { Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); builder.setContentIntent(PendingIntent.getActivity(context, 2, notificationIntent, 0)); final Notification notification = builder.build(); @@ -306,6 +396,9 @@ public final class Notifications { } public static void showSyncNotification(final Context context, int stringId, String extra) { + showSyncNotification(context, stringId, extra, null); + } + public static void showSyncNotification(final Context context, int stringId, String extra, String extraId) { if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) { if(extra == null) { extra = ""; @@ -323,7 +416,7 @@ public final class Notifications { .setAutoCancel(true); Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); String tab = null, type = null; switch(stringId) { @@ -346,6 +439,9 @@ public final class Notifications { if(type != null) { notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type); } + if(extraId != null) { + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ID, extraId); + } builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0)); diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java index 22f35efc..603b1ccb 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java @@ -24,5 +24,5 @@ package github.daneren2005.dsub.util; public interface ProgressListener { void updateProgress(String message); void updateProgress(int messageId); - void updateCache(); + void updateCache(int changeCode); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java index def97cac..e57658c4 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java +++ b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java @@ -21,6 +21,7 @@ package github.daneren2005.dsub.util; import android.app.backup.BackupAgentHelper; import android.app.backup.BackupDataInput; import android.app.backup.SharedPreferencesBackupHelper; +import android.content.SharedPreferences; import android.os.ParcelFileDescriptor; import java.io.IOError; @@ -39,6 +40,10 @@ public class SettingsBackupAgent extends BackupAgentHelper { @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException{ super.onRestore(data, appVersionCode, newState); - Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_CACHE_LOCATION).apply(); + + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.remove(Constants.PREFERENCES_KEY_CACHE_LOCATION); + editor.remove(Constants.CACHE_AUDIO_SESSION_ID); + editor.apply(); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java index 8d91a251..1309ee69 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java +++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java @@ -150,11 +150,23 @@ public class SongDBHandler extends SQLiteOpenHelper { db.close(); } + public boolean hasBeenPlayed(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[0] != null && lastPlayed[0] > 0; + } + public boolean hasBeenCompleted(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[1] != null && lastPlayed[1] > 0; + } public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { return getLastPlayed(getOnlineSongId(entry)); } protected synchronized Long[] getLastPlayed(Pair<Integer, String> pair) { - return getLastPlayed(pair.getFirst(), pair.getSecond()); + if(pair == null) { + return null; + } else { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } } public synchronized Long[] getLastPlayed(int serverKey, String id) { SQLiteDatabase db = this.getReadableDatabase(); @@ -169,9 +181,12 @@ public class SongDBHandler extends SQLiteOpenHelper { dates[0] = cursor.getLong(0); dates[1] = cursor.getLong(1); return dates; - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getOnlineSongId(MusicDirectory.Entry entry) { @@ -210,9 +225,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getIdFromPath(int serverKey, String path) { SQLiteDatabase db = this.getReadableDatabase(); @@ -223,9 +241,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public static SongDBHandler getHandler(Context context) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java b/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java new file mode 100644 index 00000000..7de4f928 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java @@ -0,0 +1,112 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; + +import java.util.Locale; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.activity.SettingsActivity; +import github.daneren2005.dsub.activity.SubsonicFragmentActivity; + +public final class ThemeUtil { + public static final String THEME_DARK = "dark"; + public static final String THEME_BLACK = "black"; + public static final String THEME_LIGHT = "light"; + public static final String THEME_HOLO = "holo"; + public static final String THEME_DAY_NIGHT = "day/night"; + public static final String THEME_DAY_BLACK_NIGHT = "day/black"; + + public static String getTheme(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + String theme = prefs.getString(Constants.PREFERENCES_KEY_THEME, null); + + if(THEME_DAY_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_DARK; + } else { + theme = THEME_LIGHT; + } + } else if(THEME_DAY_BLACK_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_BLACK; + } else { + theme = THEME_LIGHT; + } + } + + return theme; + } + public static int getThemeRes(Context context) { + return getThemeRes(context, getTheme(context)); + } + public static int getThemeRes(Context context, String theme) { + if(context instanceof SubsonicFragmentActivity || context instanceof SettingsActivity) { + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_DSub_Dark_No_Actionbar; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_DSub_Black_No_Actionbar; + } else if (THEME_HOLO.equals(theme)) { + return R.style.Theme_DSub_Holo_No_Actionbar; + } else { + return R.style.Theme_DSub_Light_No_Actionbar; + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_DSub_Dark_No_Color; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_DSub_Black_No_Color; + } else if (THEME_HOLO.equals(theme)) { + return R.style.Theme_DSub_Holo_No_Color; + } else { + return R.style.Theme_DSub_Light_No_Color; + } + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_DSub_Dark; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_DSub_Black; + } else if (THEME_HOLO.equals(theme)) { + return R.style.Theme_DSub_Holo; + } else { + return R.style.Theme_DSub_Light; + } + } + } + public static void setTheme(Context context, String theme) { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putString(Constants.PREFERENCES_KEY_THEME, theme); + editor.commit(); + } + + public static void applyTheme(Context context, String theme) { + context.setTheme(getThemeRes(context, theme)); + + SharedPreferences prefs = Util.getPreferences(context); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) { + Configuration config = new Configuration(); + config.locale = Locale.ENGLISH; + context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics()); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java index c7e0a04b..4cf25b30 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.View; import android.widget.RatingBar; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,9 +52,24 @@ public final class UpdateHelper { } public static void toggleStarred(final Context context, final Entry entry, final OnStarChange onStarChange) { - final boolean starred = !entry.isStarred(); - entry.setStarred(starred); + toggleStarred(context, Arrays.asList(entry), onStarChange); + } + + public static void toggleStarred(Context context, List<Entry> entries) { + toggleStarred(context, entries, null); + } + public static void toggleStarred(final Context context, final List<Entry> entries, final OnStarChange onStarChange) { + if(entries.isEmpty()) { + return; + } + + final Entry firstEntry = entries.get(0); + final boolean starred = !firstEntry.isStarred(); + for(Entry entry: entries) { + entry.setStarred(starred); + } if(onStarChange != null) { + onStarChange.entries = entries; onStarChange.starChange(starred); } @@ -61,22 +77,30 @@ public final class UpdateHelper { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); - if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) { - if(entry.isAlbum()) { - musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context); + List<Entry> songs = new ArrayList<Entry>(); + List<Entry> artists = new ArrayList<Entry>(); + List<Entry> albums = new ArrayList<Entry>(); + for(Entry entry: entries) { + if(entry.isDirectory() && Util.isTagBrowsing(context)) { + if(entry.isAlbum()) { + albums.add(entry); + } else { + artists.add(entry); + } } else { - musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context); + songs.add(entry); } - } else { - musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context); } - - new EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(starred); - } - }.execute(); + musicService.setStarred(songs, artists, albums, starred, this, context); + + for(Entry entry: entries) { + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setStarred(starred); + } + }.execute(); + } return null; } @@ -84,13 +108,21 @@ public final class UpdateHelper { @Override protected void done(Void result) { // UpdateView - Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle())); + int starMsgId = starred ? R.string.starring_content_starred : R.string.starring_content_unstarred; + String starMsgBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + Util.toast(context, context.getResources().getString(starMsgId, starMsgBody)); + + if(onStarChange != null) { + onStarChange.starCommited(starred); + } } @Override protected void error(Throwable error) { Log.w(TAG, "Failed to star", error); - entry.setStarred(!starred); + for(Entry entry: entries) { + entry.setStarred(!starred); + } if(onStarChange != null) { onStarChange.starChange(!starred); } @@ -99,7 +131,8 @@ public final class UpdateHelper { if (error instanceof OfflineException || error instanceof ServerTooOldException) { msg = getErrorMessage(error); } else { - msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error); + String errorBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + msg = context.getResources().getString(R.string.starring_content_error, errorBody) + " " + getErrorMessage(error); } Util.toast(context, msg, false); @@ -215,6 +248,7 @@ public final class UpdateHelper { msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error); } + Log.e(TAG, "Failed to setRating", error); Util.toast(context, msg, false); } }.execute(); @@ -222,10 +256,15 @@ public final class UpdateHelper { public static abstract class EntryInstanceUpdater { private Entry entry; + protected int metadataUpdate = DownloadService.METADATA_UPDATED_ALL; public EntryInstanceUpdater(Entry entry) { this.entry = entry; } + public EntryInstanceUpdater(Entry entry, int metadataUpdate) { + this.entry = entry; + this.metadataUpdate = metadataUpdate; + } public abstract void update(Entry found); @@ -234,11 +273,17 @@ public final class UpdateHelper { if(downloadService != null && !entry.isDirectory()) { boolean serializeChanges = false; List<DownloadFile> downloadFiles = downloadService.getDownloads(); + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + for(DownloadFile file: downloadFiles) { Entry check = file.getSong(); if(entry.getId().equals(check.getId())) { - update(entry); + update(check); serializeChanges = true; + + if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) { + downloadService.onMetadataUpdate(metadataUpdate); + } } } @@ -255,7 +300,10 @@ public final class UpdateHelper { } public static abstract class OnStarChange { + protected List<Entry> entries; + public abstract void starChange(boolean starred); + public abstract void starCommited(boolean starred); } public static abstract class OnRatingChange { public abstract void ratingChange(int rating); diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java index 6417dc81..db1c628f 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java @@ -29,8 +29,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.ListView; import android.widget.TextView; import github.daneren2005.dsub.R; @@ -159,8 +157,11 @@ public final class UserUtil { return defaultValue; } - - public static void confirmCredentials(final Activity context, final Runnable onSuccess) { + + public static void confirmCredentials(Activity context, Runnable onSuccess) { + confirmCredentials(context, onSuccess, null); + } + public static void confirmCredentials(final Activity context, final Runnable onSuccess, final Runnable onCancel) { final long currentTime = System.currentTimeMillis(); // If already ran this check within last x time, just go ahead and auth if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) { @@ -175,12 +176,7 @@ public final class UserUtil { .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - String password = passwordView.getText().toString(); - - SharedPreferences prefs = Util.getPreferences(context); - String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); - - if(password != null && password.equals(correctPassword)) { + if(isPasswordCorrect(context, passwordView)) { lastVerifiedTime = currentTime; onSuccess.run(); } else { @@ -188,7 +184,14 @@ public final class UserUtil { } } }) - .setNegativeButton(R.string.common_cancel, null) + .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if(onCancel != null) { + onCancel.run(); + } + } + }) .setCancelable(true); AlertDialog dialog = builder.create(); @@ -199,8 +202,14 @@ public final class UserUtil { public static void changePassword(final Activity context, final User user) { View layout = context.getLayoutInflater().inflate(R.layout.change_password, null); + View currentPasswordLayout = layout.findViewById(R.id.current_password_layout); + final TextView currentPasswordView = (TextView) layout.findViewById(R.id.current_password); final TextView passwordView = (TextView) layout.findViewById(R.id.new_password); + if(isCurrentAdmin()) { + currentPasswordLayout.setVisibility(View.GONE); + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.admin_change_password) .setView(layout) @@ -215,8 +224,12 @@ public final class UserUtil { @Override public void onClick(View v) { final String password = passwordView.getText().toString(); + if(!isCurrentAdmin() && !isPasswordCorrect(context, currentPasswordView)) { + Util.toast(context, R.string.admin_confirm_password_bad); + return; + } // Don't allow blank passwords - if ("".equals(password)) { + else if ("".equals(password)) { Util.toast(context, R.string.admin_change_password_invalid); return; } @@ -252,6 +265,16 @@ public final class UserUtil { }); } + private static boolean isPasswordCorrect(Context context, TextView passwordView) { + return isPasswordCorrect(context, passwordView.getText().toString()); + } + private static boolean isPasswordCorrect(Context context, String password) { + SharedPreferences prefs = Util.getPreferences(context); + String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); + + return password != null && password.equals(correctPassword); + } + public static void updateSettings(final Context context, final User user) { new SilentBackgroundTask<Void>(context) { @Override @@ -373,7 +396,7 @@ public final class UserUtil { }); } - public static void addNewUser(final Activity context, final SubsonicFragment fragment) { + public static void addNewUser(final Activity context, final SubsonicFragment fragment, User sampleUser) { final User user = new User(); for(String role: User.ROLES) { if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) { @@ -383,6 +406,13 @@ public final class UserUtil { } } + if(sampleUser != null && sampleUser.getMusicFolderSettings() != null) { + for(User.Setting setting: sampleUser.getMusicFolderSettings()) { + User.MusicFolderSetting musicFolderSetting = (User.MusicFolderSetting) setting; + user.addMusicFolder(musicFolderSetting, true); + } + } + View layout = context.getLayoutInflater().inflate(R.layout.create_user, null); final TextView usernameView = (TextView) layout.findViewById(R.id.username); final TextView emailView = (TextView) layout.findViewById(R.id.email); @@ -391,7 +421,7 @@ public final class UserUtil { LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(new SettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { + recyclerView.setAdapter(SettingsAdapter.getSettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { @Override public void onItemClicked(UpdateView<User.Setting> updateView, User.Setting item) { if(updateView.isCheckable()) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java index bf4af20d..7f8a168d 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -19,15 +19,15 @@ package github.daneren2005.dsub.util; import android.annotation.TargetApi; import android.app.Activity; -import android.graphics.Color; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; +import android.content.ClipboardManager; +import android.content.ClipData; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -46,15 +46,13 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.util.SparseArray; +import android.view.View; import android.view.Gravity; -import android.view.Window; -import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import github.daneren2005.dsub.R; -import github.daneren2005.dsub.activity.SettingsActivity; -import github.daneren2005.dsub.activity.SubsonicFragmentActivity; import github.daneren2005.dsub.adapter.DetailsAdapter; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; @@ -63,8 +61,6 @@ import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; import github.daneren2005.dsub.service.DownloadService; -import org.apache.http.HttpEntity; - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -76,7 +72,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -107,6 +102,7 @@ public final class Util { private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a"); private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a"); + private static SimpleDateFormat DATE_FORMAT_NO_TIME = new SimpleDateFormat("MMM d, yyyy"); private static int CURRENT_YEAR = new Date().getYear(); public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED"; @@ -269,65 +265,6 @@ public final class Util { editor.putBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, perFolder); editor.commit(); } - - public static String getTheme(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getString(Constants.PREFERENCES_KEY_THEME, null); - } - public static int getThemeRes(Context context) { - return getThemeRes(context, getTheme(context)); - } - public static int getThemeRes(Context context, String theme) { - if(context instanceof SubsonicFragmentActivity || context instanceof SettingsActivity) { - if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { - if ("dark".equals(theme)) { - return R.style.Theme_DSub_Dark_No_Actionbar; - } else if ("black".equals(theme)) { - return R.style.Theme_DSub_Black_No_Actionbar; - } else if ("holo".equals(theme)) { - return R.style.Theme_DSub_Holo_No_Actionbar; - } else { - return R.style.Theme_DSub_Light_No_Actionbar; - } - } else { - if ("dark".equals(theme)) { - return R.style.Theme_DSub_Dark_No_Color; - } else if ("black".equals(theme)) { - return R.style.Theme_DSub_Black_No_Color; - } else if ("holo".equals(theme)) { - return R.style.Theme_DSub_Holo_No_Color; - } else { - return R.style.Theme_DSub_Light_No_Color; - } - } - } else { - if ("dark".equals(theme)) { - return R.style.Theme_DSub_Dark; - } else if ("black".equals(theme)) { - return R.style.Theme_DSub_Black; - } else if ("holo".equals(theme)) { - return R.style.Theme_DSub_Holo; - } else { - return R.style.Theme_DSub_Light; - } - } - } - public static void setTheme(Context context, String theme) { - SharedPreferences.Editor editor = getPreferences(context).edit(); - editor.putString(Constants.PREFERENCES_KEY_THEME, theme); - editor.commit(); - } - - public static void applyTheme(Context context, String theme) { - context.setTheme(getThemeRes(context, theme)); - - SharedPreferences prefs = Util.getPreferences(context); - if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) { - Configuration config = new Configuration(); - config.locale = Locale.ENGLISH; - context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics()); - } - } public static boolean getDisplayTrack(Context context) { SharedPreferences prefs = getPreferences(context); @@ -376,6 +313,12 @@ public final class Util { int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; } + public static boolean isBatchMode(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false); + } + public static void setBatchMode(Context context, boolean batchMode) { + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, batchMode).commit(); + } public static String getRestUrl(Context context, String method) { return getRestUrl(context, method, true); @@ -401,13 +344,15 @@ public final class Util { String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); if(allowAltAddress && Util.isWifiConnected(context)) { String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, ""); - String currentSSID = Util.getSSID(context); - - String[] ssidParts = SSID.split(","); - if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { - String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); - if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { - serverUrl = internalUrl; + if(!SSID.isEmpty()) { + String currentSSID = Util.getSSID(context); + + String[] ssidParts = SSID.split(","); + if ("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { + String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); + if (internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { + serverUrl = internalUrl; + } } } } @@ -467,6 +412,18 @@ public final class Util { return builder.toString().hashCode(); } + public static String getBlockTokenUsePref(Context context, int instance) { + return Constants.CACHE_BLOCK_TOKEN_USE + Util.getRestUrl(context, null, instance, false); + } + public static boolean getBlockTokenUse(Context context, int instance) { + return getPreferences(context).getBoolean(getBlockTokenUsePref(context, instance), false); + } + public static void setBlockTokenUse(Context context, int instance, boolean block) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putBoolean(getBlockTokenUsePref(context, instance), block); + editor.commit(); + } + public static String replaceInternalUrl(Context context, String url) { // Only change to internal when using https if(url.indexOf("https") != -1) { @@ -637,13 +594,6 @@ public final class Util { } } - public static String getContentType(HttpEntity entity) { - if (entity == null || entity.getContentType() == null) { - return null; - } - return entity.getContentType().getValue(); - } - public static int getRemainingTrialDays(Context context) { SharedPreferences prefs = getPreferences(context); long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L); @@ -683,11 +633,19 @@ public final class Util { editor.commit(); } + public static boolean shouldCacheDuringCasting(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_CACHE, false); + } + public static boolean shouldStartOnHeadphones(Context context) { SharedPreferences prefs = getPreferences(context); return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false); } + public static String getSongPressAction(Context context) { + return getPreferences(context).getString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "all"); + } + /** * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>. * <p/> @@ -901,26 +859,42 @@ public final class Util { } public static String formatDate(Context context, String dateString) { + return formatDate(context, dateString, true); + } + public static String formatDate(Context context, String dateString, boolean includeTime) { + if(dateString == null) { + return ""; + } + try { + dateString = dateString.replace(' ', 'T'); boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); if (isDateNormalized) { dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } - return formatDate(dateFormat.parse(dateString)); + return formatDate(dateFormat.parse(dateString), includeTime); } catch(ParseException e) { + Log.e(TAG, "Failed to parse date string", e); return dateString; } } public static String formatDate(Date date) { + return formatDate(date, true); + } + public static String formatDate(Date date, boolean includeTime) { if(date == null) { return "Never"; } else { - if(date.getYear() != CURRENT_YEAR) { - return DATE_FORMAT_LONG.format(date); + if(includeTime) { + if (date.getYear() != CURRENT_YEAR) { + return DATE_FORMAT_LONG.format(date); + } else { + return DATE_FORMAT_SHORT.format(date); + } } else { - return DATE_FORMAT_SHORT.format(date); + return DATE_FORMAT_NO_TIME.format(date); } } } @@ -1135,7 +1109,7 @@ public final class Util { } public static boolean isAllowedToDownload(Context context) { - return !isWifiRequiredForDownload(context) || isWifiConnected(context); + return isNetworkConnected(context, true) && !isOffline(context); } public static boolean isWifiRequiredForDownload(Context context) { SharedPreferences prefs = getPreferences(context); @@ -1161,22 +1135,22 @@ public final class Util { showDialog(context, android.R.drawable.ic_dialog_info, title, message, linkify); } - private static void showDialog(Context context, int icon, int titleId, int messageId) { + public static void showDialog(Context context, int icon, int titleId, int messageId) { showDialog(context, icon, titleId, messageId, true); } - private static void showDialog(Context context, int icon, int titleId, String message) { + public static void showDialog(Context context, int icon, int titleId, String message) { showDialog(context, icon, titleId, message, true); } - private static void showDialog(Context context, int icon, String title, String message) { + public static void showDialog(Context context, int icon, String title, String message) { showDialog(context, icon, title, message, true); } - private static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) { + public static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) { showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId), linkify); } - private static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) { + public static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) { showDialog(context, icon, context.getResources().getString(titleId), message, linkify); } - private static void showDialog(Context context, int icon, String title, String message, boolean linkify) { + public static void showDialog(Context context, int icon, String title, String message, boolean linkify) { SpannableString ss = new SpannableString(message); if(linkify) { Linkify.addLinks(ss, Linkify.ALL); @@ -1222,12 +1196,36 @@ public final class Util { } showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); } - public static void showDetailsDialog(Context context, String title, List<String> headers, List<String> details) { + public static void showDetailsDialog(Context context, String title, List<String> headers, final List<String> details) { ListView listView = new ListView(context); listView.setAdapter(new DetailsAdapter(context, R.layout.details_item, headers, details)); listView.setDivider(null); listView.setScrollbarFadingEnabled(false); + // Let the user long-click on a row to copy its value to the clipboard + final Context contextRef = context; + listView.setOnItemLongClickListener(new ListView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int pos, long id) { + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + if(nameView == null || detailsView == null) { + return false; + } + + CharSequence name = nameView.getText(); + CharSequence value = detailsView.getText(); + + ClipboardManager clipboard = (ClipboardManager) contextRef.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(name, value); + clipboard.setPrimaryClip(clip); + + toast(contextRef, "Copied " + name + " to clipboard"); + + return true; + } + }); + new AlertDialog.Builder(context) // .setIcon(android.R.drawable.ic_dialog_info) .setTitle(title) @@ -1367,72 +1365,79 @@ public final class Util { * <p>Broadcasts the given song info as the new song being played.</p> */ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { - DownloadService downloadService = (DownloadService)context; - Intent intent = new Intent(EVENT_META_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); - - if (song != null) { - intent.putExtra("title", song.getTitle()); - intent.putExtra("artist", song.getArtist()); - intent.putExtra("album", song.getAlbum()); - - File albumArtFile = FileUtil.getAlbumArtFile(context, song); - intent.putExtra("coverart", albumArtFile.getAbsolutePath()); - avrcpIntent.putExtra("playing", true); - } else { - intent.putExtra("title", ""); - intent.putExtra("artist", ""); - intent.putExtra("album", ""); - intent.putExtra("coverart", ""); - avrcpIntent.putExtra("playing", false); - } - addTrackInfo(context, song, avrcpIntent); + try { + Intent intent = new Intent(EVENT_META_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); + + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); - context.sendBroadcast(intent); - context.sendBroadcast(avrcpIntent); + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + avrcpIntent.putExtra("playing", true); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + avrcpIntent.putExtra("playing", false); + } + addTrackInfo(context, song, avrcpIntent); + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastNewTrackInfo", e); + } } /** * <p>Broadcasts the given player state as the one being set.</p> */ public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) { - Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); - - switch (state) { - case STARTED: - intent.putExtra("state", "play"); - avrcpIntent.putExtra("playing", true); - break; - case STOPPED: - intent.putExtra("state", "stop"); - avrcpIntent.putExtra("playing", false); - break; - case PAUSED: - intent.putExtra("state", "pause"); - avrcpIntent.putExtra("playing", false); - break; - case PREPARED: - // Only send quick pause event for samsung devices, causes issues for others - if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + try { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + intent.putExtra("state", "stop"); avrcpIntent.putExtra("playing", false); - } else { - return; // Don't broadcast anything - } - break; - case COMPLETED: - intent.putExtra("state", "complete"); - avrcpIntent.putExtra("playing", false); - break; - default: - return; // No need to broadcast. - } - addTrackInfo(context, song, avrcpIntent); + break; + case PAUSED: + intent.putExtra("state", "pause"); + avrcpIntent.putExtra("playing", false); + break; + case PREPARED: + // Only send quick pause event for samsung devices, causes issues for others + if (Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + avrcpIntent.putExtra("playing", false); + } else { + return; // Don't broadcast anything + } + break; + case COMPLETED: + intent.putExtra("state", "complete"); + avrcpIntent.putExtra("playing", false); + break; + default: + return; // No need to broadcast. + } + addTrackInfo(context, song, avrcpIntent); - if(state != PlayerState.PREPARED) { - context.sendBroadcast(intent); + if (state != PlayerState.PREPARED) { + context.sendBroadcast(intent); + } + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e); } - context.sendBroadcast(avrcpIntent); } private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) { @@ -1448,6 +1453,7 @@ public final class Util { intent.putExtra("duration", (long) downloadService.getPlayerDuration()); intent.putExtra("position", (long) downloadService.getPlayerPosition()); intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + intent.putExtra("package","github.daneren2005.dsub"); } else { intent.putExtra("track", ""); intent.putExtra("artist", ""); @@ -1457,6 +1463,7 @@ public final class Util { intent.putExtra("duration", (long) 0); intent.putExtra("position", (long) 0); intent.putExtra("coverart", ""); + intent.putExtra("package","github.daneren2005.dsub"); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java index ab64bca9..415106db 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java @@ -23,13 +23,9 @@ import com.google.android.gms.cast.CastMediaControlIntent; import github.daneren2005.dsub.service.ChromeCastController; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.RemoteController; +import github.daneren2005.dsub.util.EnvironmentVariables; -/** - * Created by owner on 2/9/14. - */ public final class CastCompat { - public static final String APPLICATION_ID = "5F85EBEB"; - static { try { Class.forName("com.google.android.gms.cast.CastDevice"); @@ -52,6 +48,6 @@ public final class CastCompat { } public static String getCastControlCategory() { - return CastMediaControlIntent.categoryForCast(APPLICATION_ID); + return CastMediaControlIntent.categoryForCast(EnvironmentVariables.CAST_APPLICATION_ID); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java index 1f7035dc..4f9a27f0 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java @@ -29,7 +29,7 @@ public abstract class RemoteControlClientBase { public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent); public abstract void unregister(final Context context); - public abstract void setPlaybackState(int state); + public abstract void setPlaybackState(int state, int index, int queueSize); public abstract void updateMetadata(Context context, MusicDirectory.Entry currentSong); public abstract void metadataChanged(MusicDirectory.Entry currentSong); public abstract void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap); diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java index 2a06e798..74076afb 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java @@ -54,7 +54,7 @@ public class RemoteControlClientICS extends RemoteControlClientBase { audioManager.unregisterRemoteControlClient(mRemoteControl); } - public void setPlaybackState(final int state) { + public void setPlaybackState(final int state, int index, int queueSize) { if(mRemoteControl == null) { return; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java index e61e9a47..d10c8594 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java @@ -1,17 +1,10 @@ package github.daneren2005.dsub.util.compat; -import github.daneren2005.dsub.domain.MusicDirectory; -import github.daneren2005.dsub.util.ImageLoader; import android.annotation.TargetApi; -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; -import github.daneren2005.dsub.activity.SubsonicActivity; -import github.daneren2005.dsub.service.DownloadService; + import github.daneren2005.dsub.util.SilentBackgroundTask; @TargetApi(18) @@ -36,13 +29,13 @@ public class RemoteControlClientJB extends RemoteControlClientICS { return null; } }.execute(); - setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, 0, 0); } }); } @Override - public void setPlaybackState(final int state) { + public void setPlaybackState(final int state, int index, int queueSize) { if(mRemoteControl == null) { return; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java index 456446f3..00bca833 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java @@ -34,7 +34,10 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.support.annotation.NonNull; import android.support.v7.media.MediaRouter; +import android.util.Log; +import android.view.KeyEvent; import java.util.ArrayList; import java.util.List; @@ -42,7 +45,9 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.activity.SubsonicActivity; import github.daneren2005.dsub.activity.SubsonicFragmentActivity; +import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.Playlist; import github.daneren2005.dsub.domain.SearchCritera; import github.daneren2005.dsub.domain.SearchResult; @@ -87,7 +92,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { Intent activityIntent = new Intent(context, SubsonicFragmentActivity.class); activityIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0); mediaSession.setSessionActivity(activityPendingIntent); @@ -116,8 +121,12 @@ public class RemoteControlClientLP extends RemoteControlClientBase { mediaSession.release(); } + private void setPlaybackState(int state) { + setPlaybackState(state, downloadService.getCurrentPlayingIndex(), downloadService.size()); + } + @Override - public void setPlaybackState(int state) { + public void setPlaybackState(int state, int index, int queueSize) { PlaybackState.Builder builder = new PlaybackState.Builder(); int newState = PlaybackState.STATE_NONE; @@ -141,11 +150,17 @@ public class RemoteControlClientLP extends RemoteControlClientBase { position = downloadService.getPlayerPosition(); } builder.setState(newState, position, 1.0f); - builder.setActions(getPlaybackActions()); - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + Entry entry = null; + boolean isSong = true; if(downloadFile != null) { - MusicDirectory.Entry entry = downloadFile.getSong(); + entry = downloadFile.getSong(); + isSong = entry.isSong(); + } + + builder.setActions(getPlaybackActions(isSong, index, queueSize)); + + if(entry != null) { addCustomActions(entry, builder); builder.setActiveQueueItemId(entry.getId().hashCode()); } @@ -156,7 +171,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateMetadata(Context context, MusicDirectory.Entry currentSong) { + public void updateMetadata(Context context, Entry currentSong) { setMetadata(currentSong, null); if(currentSong != null && imageLoader != null) { @@ -165,11 +180,11 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void metadataChanged(MusicDirectory.Entry currentSong) { + public void metadataChanged(Entry currentSong) { setPlaybackState(previousState); } - public void setMetadata(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void setMetadata(Entry currentSong, Bitmap bitmap) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) .putString(MediaMetadata.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) @@ -189,7 +204,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void updateAlbumArt(Entry currentSong, Bitmap bitmap) { setMetadata(currentSong, bitmap); } @@ -208,7 +223,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { List<MediaSession.QueueItem> queue = new ArrayList<>(); for(DownloadFile file: playlist) { - MusicDirectory.Entry entry = file.getSong(); + Entry entry = file.getSong(); MediaDescription description = new MediaDescription.Builder() .setMediaId(entry.getId()) @@ -227,24 +242,27 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return mediaSession; } - protected long getPlaybackActions() { + protected long getPlaybackActions(boolean isSong, int currentIndex, int size) { long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO | PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM; - int currentIndex = downloadService.getCurrentPlayingIndex(); - int size = downloadService.size(); - if(currentIndex > 0) { + if(isSong) { + if (currentIndex > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if (currentIndex < size - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + } else { actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; - } - if(currentIndex < size - 1) { actions |= PlaybackState.ACTION_SKIP_TO_NEXT; } return actions; } - protected void addCustomActions(MusicDirectory.Entry currentSong, PlaybackState.Builder builder) { + protected void addCustomActions(Entry currentSong, PlaybackState.Builder builder) { Bundle showOnWearExtras = new Bundle(); showOnWearExtras.putBoolean(SHOW_ON_WEAR, true); @@ -296,7 +314,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { SearchResult results = musicService.search(searchCritera, downloadService, null); if(results.hasArtists()) { - playFromParent(new MusicDirectory.Entry(results.getArtists().get(0))); + playFromParent(new Entry(results.getArtists().get(0))); } else if(results.hasAlbums()) { playFromParent(results.getAlbums().get(0)); } else if(results.hasSongs()) { @@ -307,13 +325,13 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return null; } - - private void playFromParent(MusicDirectory.Entry parent) throws Exception { - List<MusicDirectory.Entry> songs = new ArrayList<>(); + + private void playFromParent(Entry parent) throws Exception { + List<Entry> songs = new ArrayList<>(); getSongsRecursively(parent, songs); playSongs(songs); } - private void getSongsRecursively(MusicDirectory.Entry parent, List<MusicDirectory.Entry> songs) throws Exception { + private void getSongsRecursively(Entry parent, List<Entry> songs) throws Exception { MusicDirectory musicDirectory; if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { musicDirectory = musicService.getAlbum(parent.getId(), parent.getTitle(), false, downloadService, this); @@ -321,7 +339,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { musicDirectory = musicService.getMusicDirectory(parent.getId(), parent.getTitle(), false, downloadService, this); } - for (MusicDirectory.Entry dir : musicDirectory.getChildren(true, false)) { + for (Entry dir : musicDirectory.getChildren(true, false)) { if (dir.getRating() == 1) { continue; } @@ -329,7 +347,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { getSongsRecursively(dir, songs); } - for (MusicDirectory.Entry song : musicDirectory.getChildren(false, true)) { + for (Entry song : musicDirectory.getChildren(false, true)) { if (!song.isVideo() && song.getRating() != 1) { songs.add(song); } @@ -349,24 +367,75 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } }.execute(); } + private void playMusicDirectory(Entry dir, boolean shuffle, boolean append, boolean playFromBookmark) { + playMusicDirectory(dir.getId(), shuffle, append, playFromBookmark); + } + private void playMusicDirectory(final String dirId, final boolean shuffle, final boolean append, final boolean playFromBookmark) { + new SilentServiceTask<Void>(downloadService) { + @Override + protected Void doInBackground(MusicService musicService) throws Throwable { + MusicDirectory musicDirectory; + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + musicDirectory = musicService.getAlbum(dirId, "dir", false, downloadService, null); + } else { + musicDirectory = musicService.getMusicDirectory(dirId, "dir", false, downloadService, null); + } + + List<Entry> playEntries = new ArrayList<>(); + List<Entry> allEntries = musicDirectory.getChildren(false, true); + for(Entry song: allEntries) { + if (!song.isVideo() && song.getRating() != 1) { + playEntries.add(song); + } + } + playSongs(playEntries, shuffle, append, playFromBookmark); + + return null; + } + }.execute(); + } + + private void playSong(Entry entry) { - private void playSong(MusicDirectory.Entry entry) { - List<MusicDirectory.Entry> entries = new ArrayList<>(); + } + private void playSong(Entry entry, boolean resumeFromBookmark) { + List<Entry> entries = new ArrayList<>(); entries.add(entry); - playSongs(entries); + playSongs(entries, false, false, resumeFromBookmark); } - private void playSongs(List<MusicDirectory.Entry> entries) { + private void playSongs(List<Entry> entries) { playSongs(entries, false, false); } - private void playSongs(List<MusicDirectory.Entry> entries, boolean shuffle, boolean append) { + private void playSongs(List<Entry> entries, boolean shuffle, boolean append) { + playSongs(entries, shuffle, append, false); + } + private void playSongs(List<Entry> entries, boolean shuffle, boolean append, boolean resumeFromBookmark) { if(!append) { downloadService.clear(); } - downloadService.download(entries, false, true, false, shuffle); + + int startIndex = 0; + int startPosition = 0; + if(resumeFromBookmark) { + int bookmarkIndex = 0; + for(Entry entry: entries) { + if(entry.getBookmark() != null) { + Bookmark bookmark = entry.getBookmark(); + startIndex = bookmarkIndex; + startPosition = bookmark.getPosition(); + break; + } + bookmarkIndex++; + } + } + + downloadService.download(entries, false, !append, false, shuffle, startIndex, startPosition); } private void noResults() { - + // Keep getting emails from Google that not playing something with no results is bad + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); } private class EventCallback extends MediaSession.Callback { @@ -415,6 +484,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { public void onPlayFromSearch (String query, Bundle extras) { // User just asked to playing something if("".equals(query)) { + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); @@ -435,6 +505,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { @@ -467,6 +538,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } } + @Override public void onPlayFromMediaId (String mediaId, Bundle extras) { if(extras == null) { return; @@ -474,11 +546,33 @@ public class RemoteControlClientLP extends RemoteControlClientBase { boolean shuffle = extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false); boolean playLast = extras.getBoolean(Constants.INTENT_EXTRA_PLAY_LAST, false); + Entry entry = (Entry) extras.getSerializable(Constants.INTENT_EXTRA_ENTRY); + String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null); if(playlistId != null) { Playlist playlist = new Playlist(playlistId, null); playPlaylist(playlist, shuffle, playLast); } + String musicDirectoryId = extras.getString(Constants.INTENT_EXTRA_NAME_ID); + if(musicDirectoryId != null) { + Entry dir = new Entry(musicDirectoryId); + playMusicDirectory(dir, shuffle, playLast, true); + } + + String podcastId = extras.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, null); + if(podcastId != null) { + playSong(entry, true); + } + + // Currently only happens when playing bookmarks so we should be looking up parent + String childId = extras.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID, null); + if(childId != null) { + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + playMusicDirectory(entry.getAlbumId(), shuffle, playLast, true); + } else { + playMusicDirectory(entry.getParent(), shuffle, playLast, true); + } + } } @Override @@ -491,5 +585,18 @@ public class RemoteControlClientLP extends RemoteControlClientBase { downloadService.toggleStarred(); } } + + @Override + public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { + if (getMediaSession() != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { + KeyEvent keyEvent = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent != null) { + downloadService.handleKeyEvent(keyEvent); + return true; + } + } + + return super.onMediaButtonEvent(mediaButtonIntent); + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java index ea61f36c..69668475 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java +++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java @@ -134,7 +134,11 @@ public class ID3v2File extends Common { if(endValue != -1) { parts.add(txData[1].substring(endName + 1, endValue)); nextStartIndex = endValue + 1; + } else { + break; } + } else { + break; } startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex); diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java index 048d5a75..3084e962 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java @@ -30,6 +30,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.Util; import java.io.File; @@ -40,6 +41,7 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { private TextView titleView; private TextView artistView; private boolean showArtist = true; + private String coverArtId; public AlbumView(Context context, boolean cell) { super(context); @@ -82,12 +84,13 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { artist += album.getYear(); } artistView.setText(album.getArtist() == null ? "" : artist); - imageTask = imageLoader.loadImage(coverArtView, album, false, true); + onUpdateImageView(); file = null; } public void onUpdateImageView() { imageTask = item2.loadImage(coverArtView, item, false, true); + coverArtId = item.getCoverArt(); } @Override @@ -101,6 +104,15 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { isRated = item.getRating(); } + @Override + public void update() { + super.update(); + + if(!Util.equals(item.getCoverArt(), coverArtId)) { + onUpdateImageView(); + } + } + public MusicDirectory.Entry getEntry() { return item; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java new file mode 100644 index 00000000..35ce71bc --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java @@ -0,0 +1,146 @@ +/* + 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 2015 (C) Scott Jackson +*/ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.preference.DialogPreference; +import android.preference.EditTextPreference; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.File; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.FileUtil; + +public class CacheLocationPreference extends EditTextPreference { + private static final String TAG = CacheLocationPreference.class.getSimpleName(); + private Context context; + + public CacheLocationPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + public CacheLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + public CacheLocationPreference(Context context) { + super(context); + this.context = context; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setLayoutParams(new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + ViewGroup vg = (ViewGroup) editText.getParent(); + + LinearLayout cacheButtonsWrapper = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.cache_location_buttons, vg, true); + Button internalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_internal); + Button externalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_external); + + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + // Past 5.0 we can query directly for SD Card + File internalDir = null, externalDir = null; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null) { + if(Environment.isExternalStorageRemovable(dirs[i])) { + if(externalDir != null) { + externalDir = dirs[i]; + } + } else { + internalDir = dirs[i]; + } + + if(internalDir != null && externalDir != null) { + break; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + if(externalDir == null) { + for (int i = dirs.length - 1; i >= 0; i--) { + if (dirs[i] != null) { + externalDir = dirs[i]; + break; + } + } + } + if(internalDir == null) { + for (int i = 0; i < dirs.length; i++) { + if (dirs[i] != null) { + internalDir = dirs[i]; + break; + } + } + } + final File finalInternalDir = new File(internalDir, "music"); + final File finalExternalDir = new File(externalDir, "music"); + + final EditText editTextBox = (EditText)view.findViewById(android.R.id.edit); + if(finalInternalDir != null && (finalInternalDir.exists() || finalInternalDir.mkdirs())) { + internalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalInternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + internalLocation.setEnabled(false); + } + + if(finalExternalDir != null && !finalInternalDir.equals(finalExternalDir) && (finalExternalDir.exists() || finalExternalDir.mkdirs())) { + externalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalExternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + externalLocation.setEnabled(false); + } + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/CardView.java b/app/src/main/java/github/daneren2005/dsub/view/CardView.java new file mode 100644 index 00000000..d6bca330 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CardView.java @@ -0,0 +1,67 @@ +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; + +public class CardView extends FrameLayout{ + private static final String TAG = CardView.class.getSimpleName(); + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + @Override + public void onDraw(Canvas canvas) { + try { + Path clipPath = new Path(); + float roundedDp = getResources().getDimension(R.dimen.Card_Radius); + clipPath.addRoundRect(new RectF(canvas.getClipBounds()), roundedDp, roundedDp, Path.Direction.CW); + canvas.clipPath(clipPath); + } catch(Exception e) { + Log.e(TAG, "Failed to clip path on canvas", e); + } + super.onDraw(canvas); + } + + private void init(Context context) { + setClipChildren(true); + setBackgroundResource(DrawableTint.getDrawableRes(context, R.attr.cardBackgroundDrawable)); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(getResources().getInteger(R.integer.Card_Elevation)); + } + + // clipPath is not supported with Hardware Acceleration before API 18 + // http://stackoverflow.com/questions/8895677/work-around-canvas-clippath-that-is-not-supported-in-android-any-more/8895894#8895894 + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && isHardwareAccelerated()) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java index 7cb29835..d3eacefc 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java +++ b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java @@ -95,7 +95,7 @@ public class FastScroller extends LinearLayout { switch(action) { case MotionEvent.ACTION_DOWN: - if(event.getX() < (handle.getX() - 20)) { + if(event.getX() < (handle.getX() - 30)) { return false; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java index b59e7157..45b34b9f 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java +++ b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java @@ -18,10 +18,17 @@ package github.daneren2005.dsub.view; import android.graphics.Rect; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import static android.widget.LinearLayout.*; public class GridSpacingDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = GridSpacingDecoration.class.getSimpleName(); public static final int SPACING = 10; @Override @@ -39,30 +46,52 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration { } int spanCount = getTotalSpan(view, parent); int spanIndex = childIndex % spanCount; + + // If we can, use the SpanSizeLookup since headers screw up the index calculation + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); + if(spanSizeLookup != null) { + spanIndex = spanSizeLookup.getSpanIndex(childIndex, spanCount); + } + } int spanSize = getSpanSize(parent, childIndex); /* INVALID SPAN */ if (spanCount < 1 || spanSize > 1) return; - outRect.top = halfSpacing; - outRect.bottom = halfSpacing; - outRect.left = halfSpacing; - outRect.right = halfSpacing; + int margins = 0; + if(view instanceof UpdateView) { + View firstChild = ((ViewGroup) view).getChildAt(0); + ViewGroup.LayoutParams layoutParams = firstChild.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + margins = ((LinearLayout.LayoutParams) layoutParams).bottomMargin; + } else if (layoutParams instanceof FrameLayout.LayoutParams) { + margins = ((FrameLayout.LayoutParams) layoutParams).bottomMargin; + } + } + int doubleMargins = margins * 2; + + outRect.top = halfSpacing - margins; + outRect.bottom = halfSpacing - margins; + outRect.left = halfSpacing - margins; + outRect.right = halfSpacing - margins; - if (isTopEdge(childIndex, spanCount)) { - outRect.top = spacing; + if (isTopEdge(childIndex, spanIndex, spanCount)) { + outRect.top = spacing - doubleMargins; } if (isLeftEdge(spanIndex, spanCount)) { - outRect.left = spacing; + outRect.left = spacing - doubleMargins; } if (isRightEdge(spanIndex, spanCount)) { - outRect.right = spacing; + outRect.right = spacing - doubleMargins; } if (isBottomEdge(childIndex, childCount, spanCount)) { - outRect.bottom = spacing; + outRect.bottom = spacing - doubleMargins; } } @@ -94,8 +123,8 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration { return spanIndex == spanCount - 1; } - protected boolean isTopEdge(int childIndex, int spanCount) { - return childIndex < spanCount; + protected boolean isTopEdge(int childIndex, int spanIndex, int spanCount) { + return childIndex < spanCount && childIndex == spanIndex; } protected boolean isBottomEdge(int childIndex, int childCount, int spanCount) { diff --git a/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java b/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java new file mode 100644 index 00000000..36aaa8af --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java @@ -0,0 +1,39 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.TextView; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.InternetRadioStation; + +public class InternetRadioStationView extends UpdateView<InternetRadioStation> { + private TextView titleView; + + public InternetRadioStationView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + } + + protected void setObjectImpl(InternetRadioStation station) { + titleView.setText(station.getTitle()); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java b/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java deleted file mode 100644 index 26a3de08..00000000 --- a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java +++ /dev/null @@ -1,53 +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 github.daneren2005.dsub.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ViewFlipper; - -/** - * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191) - * - * @author Sindre Mehus - * @version $Id$ - */ -public class MyViewFlipper extends ViewFlipper { - - public MyViewFlipper(Context context) { - super(context); - } - - public MyViewFlipper(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - @Override - protected void onDetachedFromWindow() { - try { - super.onDetachedFromWindow(); - } - catch (IllegalArgumentException e) { - // Call stopFlipping() in order to kick off updateRunning() - stopFlipping(); - } - } -} - diff --git a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java index d46dc5d2..6dc116f8 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java @@ -23,6 +23,7 @@ import android.widget.TextView; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; import static github.daneren2005.dsub.domain.User.Setting; @@ -51,12 +52,14 @@ public class SettingView extends UpdateView2<Setting, Boolean> { protected void setObjectImpl(Setting setting, Boolean isEditable) { // Can't edit non-role parts String name = setting.getName(); - if(name.indexOf("Role") == -1) { + if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) { item2 = false; } int res = -1; - if(User.SCROBBLING.equals(name)) { + if(setting instanceof MusicFolderSetting) { + titleView.setText(((MusicFolderSetting) setting).getLabel()); + } else if(User.SCROBBLING.equals(name)) { res = R.string.admin_scrobblingEnabled; } else if(User.ADMIN.equals(name)) { res = R.string.admin_role_admin; @@ -78,6 +81,8 @@ public class SettingView extends UpdateView2<Setting, Boolean> { res = R.string.admin_role_jukebox; } else if(User.SHARE.equals(name)) { res = R.string.admin_role_share; + } else if(User.VIDEO_CONVERSION.equals(name)) { + res = R.string.admin_role_video_conversion; } else if(User.LASTFM.equals(name)) { res = R.string.admin_role_lastfm; } else { diff --git a/app/src/main/java/github/daneren2005/dsub/view/SongView.java b/app/src/main/java/github/daneren2005/dsub/view/SongView.java index 625303b7..320c5933 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SongView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SongView.java @@ -29,6 +29,8 @@ import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.util.SongDBHandler; +import github.daneren2005.dsub.util.ThemeUtil; import github.daneren2005.dsub.util.Util; import java.io.File; @@ -41,12 +43,15 @@ import java.io.File; public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private static final String TAG = SongView.class.getSimpleName(); + private TextView trackTextView; private TextView titleTextView; + private TextView playingTextView; private TextView artistTextView; private TextView durationTextView; private TextView statusTextView; private ImageView statusImageView; private ImageView bookmarkButton; + private ImageView playedButton; private View bottomRowView; private DownloadService downloadService; @@ -63,13 +68,17 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private boolean partialFileExists = false; private boolean loaded = false; private boolean isBookmarked = false; - private boolean bookmarked = false; + private boolean isBookmarkedShown = false; private boolean showPodcast = false; + private boolean isPlayed = false; + private boolean isPlayedShown = false; + private boolean showAlbum = false; public SongView(Context context) { super(context); LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + trackTextView = (TextView) findViewById(R.id.song_track); titleTextView = (TextView) findViewById(R.id.song_title); artistTextView = (TextView) findViewById(R.id.song_artist); durationTextView = (TextView) findViewById(R.id.song_duration); @@ -80,6 +89,7 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { starButton.setFocusable(false); bookmarkButton = (ImageButton) findViewById(R.id.song_bookmark); bookmarkButton.setFocusable(false); + playedButton = (ImageButton) findViewById(R.id.song_played); moreButton = (ImageView) findViewById(R.id.item_more); bottomRowView = findViewById(R.id.song_bottom); } @@ -102,12 +112,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { if(artist.length() != 0) { artist.append(" - "); } - int index = date.indexOf(" "); - artist.append(date.substring(0, index != -1 ? index : date.length())); + artist.append(Util.formatDate(context, date, false)); } } else if(song.getArtist() != null) { - artist.append(song.getArtist()); + if(showAlbum) { + artist.append(song.getAlbum()); + } else { + artist.append(song.getArtist()); + } } if(isPodcast) { @@ -138,8 +151,26 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { String title = song.getTitle(); Integer track = song.getTrack(); + if(song.getCustomOrder() != null) { + track = song.getCustomOrder(); + } + TextView newPlayingTextView; if(track != null && Util.getDisplayTrack(context)) { - title = String.format("%02d", track) + " " + title; + trackTextView.setText(String.format("%02d", track)); + trackTextView.setVisibility(View.VISIBLE); + newPlayingTextView = trackTextView; + } else { + trackTextView.setVisibility(View.GONE); + newPlayingTextView = titleTextView; + } + + if(newPlayingTextView != playingTextView || playingTextView == null) { + if(playing) { + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playing = false; + } + + playingTextView = newPlayingTextView; } titleTextView.setText(title); @@ -191,6 +222,10 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { item.loadMetadata(downloadFile.getCompleteFile()); loaded = true; } + + if(item instanceof PodcastEpisode || item.isAudioBook() || item.isPodcast()) { + isPlayed = SongDBHandler.getHandler(context).hasBeenCompleted(item); + } } @Override @@ -242,32 +277,48 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { rightImage = false; } - boolean playing = downloadService.getCurrentPlaying() == downloadFile; + boolean playing = Util.equals(downloadService.getCurrentPlaying(), downloadFile); if (playing) { if(!this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); } } else { if(this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } } if(isBookmarked) { - if(!bookmarked) { + if(!isBookmarkedShown) { if(bookmarkButton.getDrawable() == null) { bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected)); } bookmarkButton.setVisibility(View.VISIBLE); - bookmarked = true; + isBookmarkedShown = true; } } else { - if(bookmarked) { + if(isBookmarkedShown) { bookmarkButton.setVisibility(View.GONE); - bookmarked = false; + isBookmarkedShown = false; + } + } + + if(isPlayed) { + if(!isPlayedShown) { + if(playedButton.getDrawable() == null) { + playedButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_played)); + } + + playedButton.setVisibility(View.VISIBLE); + isPlayedShown = true; + } + } else { + if(isPlayedShown) { + playedButton.setVisibility(View.GONE); + isPlayedShown = false; } } @@ -288,7 +339,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { // Still highlight red if a 1-star if(isRated == 1) { this.setBackgroundColor(Color.RED); - this.getBackground().setAlpha(20); + + String theme = ThemeUtil.getTheme(context); + if("black".equals(theme)) { + this.getBackground().setAlpha(80); + } else if("dark".equals(theme) || "holo".equals(theme)) { + this.getBackground().setAlpha(60); + } else { + this.getBackground().setAlpha(20); + } } else if(rating == 1) { this.setBackgroundColor(0x00000000); } @@ -304,4 +363,8 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { public void setShowPodcast(boolean showPodcast) { this.showPodcast = showPodcast; } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java index 8f3b5271..0041eac5 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java @@ -201,6 +201,10 @@ public abstract class UpdateView<T> extends LinearLayout { }); } + public static boolean hasActiveActivity() { + return activeActivities > 0; + } + public static void addActiveActivity() { activeActivities++; diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java new file mode 100644 index 00000000..a2c898b9 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java @@ -0,0 +1,16 @@ +package github.daneren2005.dsub.view.compat; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.MediaRouteChooserDialog; +import android.support.v7.app.MediaRouteChooserDialogFragment; + +import github.daneren2005.dsub.util.ThemeUtil; +import github.daneren2005.dsub.util.Util; + +public class CustomMediaRouteChooserDialogFragment extends MediaRouteChooserDialogFragment { + @Override + public MediaRouteChooserDialog onCreateChooserDialog(Context context, Bundle savedInstanceState) { + return new MediaRouteChooserDialog(context, ThemeUtil.getThemeRes(context)); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java new file mode 100644 index 00000000..ea890b9f --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java @@ -0,0 +1,16 @@ +package github.daneren2005.dsub.view.compat; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.MediaRouteControllerDialog; +import android.support.v7.app.MediaRouteControllerDialogFragment; + +import github.daneren2005.dsub.util.ThemeUtil; +import github.daneren2005.dsub.util.Util; + +public class CustomMediaRouteControllerDialogFragment extends MediaRouteControllerDialogFragment { + @Override + public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) { + return new MediaRouteControllerDialog(context, ThemeUtil.getThemeRes(context)); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java new file mode 100644 index 00000000..8bc890cb --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java @@ -0,0 +1,17 @@ +package github.daneren2005.dsub.view.compat; + +import android.support.v7.app.MediaRouteChooserDialogFragment; +import android.support.v7.app.MediaRouteControllerDialogFragment; +import android.support.v7.app.MediaRouteDialogFactory; + +public class CustomMediaRouteDialogFactory extends MediaRouteDialogFactory { + @Override + public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() { + return new CustomMediaRouteChooserDialogFragment(); + } + + @Override + public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() { + return new CustomMediaRouteControllerDialogFragment(); + } +} |