Refactored DownloadActivity

This commit is contained in:
Nite 2021-02-05 21:45:50 +01:00
parent 95773c7994
commit a395bd6feb
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
29 changed files with 1969 additions and 3287 deletions

View File

@ -30,10 +30,11 @@
<activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="standard">
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
@ -43,59 +44,11 @@
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<activity
android:name=".activity.MainActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="standard">
<!--<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>-->
</activity>
<activity
android:name=".activity.SelectAlbumActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.SearchActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/search.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.DownloadActivity"
android:configChanges="keyboardHidden"
android:launchMode="singleTask"
android:exported="true" />
<activity
android:name=".activity.LyricsActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.EqualizerActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/equalizer.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.VoiceQueryReceiverActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!-- <activity
android:name=".activity.QueryReceiverActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
-->
<activity
android:name=".activity.ServerSelectorActivity"
android:label="@string/server_selector.label" />

View File

@ -1,89 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Lyrics;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
/**
* Displays song lyrics.
*
* @author Sindre Mehus
*/
public final class LyricsActivity extends SubsonicTabActivity
{
@Override
protected void onCreate(Bundle bundle)
{
super.onCreate(bundle);
setContentView(R.layout.lyrics);
View nowPlayingMenuItem = findViewById(R.id.menu_now_playing);
menuDrawer.setActiveView(nowPlayingMenuItem);
load();
}
private void load()
{
BackgroundTask<Lyrics> task = new TabActivityBackgroundTask<Lyrics>(this, true)
{
@Override
protected Lyrics doInBackground() throws Throwable
{
String artist = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ARTIST);
String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_TITLE);
MusicService musicService = MusicServiceFactory.getMusicService(LyricsActivity.this);
return musicService.getLyrics(artist, title, LyricsActivity.this, this);
}
@Override
protected void done(Lyrics result)
{
TextView artistView = (TextView) findViewById(R.id.lyrics_artist);
TextView titleView = (TextView) findViewById(R.id.lyrics_title);
TextView textView = (TextView) findViewById(R.id.lyrics_text);
if (result != null && result.getArtist() != null)
{
artistView.setText(result.getArtist());
titleView.setText(result.getTitle());
textView.setText(result.getText());
}
else
{
artistView.setText(R.string.lyrics_nomatch);
}
}
};
task.execute();
}
}

View File

@ -1,110 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.Util;
import java.util.Collections;
import kotlin.Lazy;
import static java.util.Arrays.asList;
import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel;
import static org.koin.java.KoinJavaComponent.inject;
public class MainActivity extends SubsonicTabActivity
{
private static boolean infoDialogDisplayed;
private static boolean shouldUseId3;
private static String lastActiveServerProperties;
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> serverSettingsModel = viewModel(this, ServerSettingsModel.class);
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final View homeMenuItem = findViewById(R.id.menu_home);
menuDrawer.setActiveView(homeMenuItem);
setActionBarTitle(R.string.common_appname);
setTitle(R.string.common_appname);
// Remember the current theme.
theme = Util.getTheme(this);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
final MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.main, menu);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
final Intent intent1 = new Intent(this, DownloadActivity.class);
intent1.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent1);
return true;
}
return false;
}
}

View File

@ -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 org.moire.ultrasonic.activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* Receives search queries and forwards to the SelectAlbumActivity.
*
* @author Sindre Mehus
*/
public class QueryReceiverActivity extends ResultActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
String query = getIntent().getStringExtra(SearchManager.QUERY);
if (query != null)
{
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE);
suggestions.saveRecentQuery(query, null);
Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
startActivityForResultWithoutTransition(QueryReceiverActivity.this, intent);
}
finish();
Util.disablePendingTransition(this);
}
}

View File

@ -1,554 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ArtistAdapter;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Performs searches and displays the matching artists, albums and songs.
*
* @author Sindre Mehus
*/
public class SearchActivity extends SubsonicTabActivity
{
private static int DEFAULT_ARTISTS;
private static int DEFAULT_ALBUMS;
private static int DEFAULT_SONGS;
private ListView list;
private View artistsHeading;
private View albumsHeading;
private View songsHeading;
private TextView searchButton;
private View moreArtistsButton;
private View moreAlbumsButton;
private View moreSongsButton;
private SearchResult searchResult;
private MergeAdapter mergeAdapter;
private ArtistAdapter artistAdapter;
private ListAdapter moreArtistsAdapter;
private EntryAdapter albumAdapter;
private ListAdapter moreAlbumsAdapter;
private ListAdapter moreSongsAdapter;
private EntryAdapter songAdapter;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.search_title);
View searchMenuItem = findViewById(R.id.menu_search);
menuDrawer.setActiveView(searchMenuItem);
DEFAULT_ARTISTS = Util.getDefaultArtists(this);
DEFAULT_ALBUMS = Util.getDefaultAlbums(this);
DEFAULT_SONGS = Util.getDefaultSongs(this);
View buttons = LayoutInflater.from(this).inflate(R.layout.search_buttons, null);
if (buttons != null)
{
artistsHeading = buttons.findViewById(R.id.search_artists);
albumsHeading = buttons.findViewById(R.id.search_albums);
songsHeading = buttons.findViewById(R.id.search_songs);
searchButton = (TextView) buttons.findViewById(R.id.search_search);
moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
moreSongsButton = buttons.findViewById(R.id.search_more_songs);
}
list = (ListView) findViewById(R.id.search_list);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == searchButton)
{
onSearchRequested();
}
else if (view == moreArtistsButton)
{
expandArtists();
}
else if (view == moreAlbumsButton)
{
expandAlbums();
}
else if (view == moreSongsButton)
{
expandSongs();
}
else
{
Object item = parent.getItemAtPosition(position);
if (item instanceof Artist)
{
onArtistSelected((Artist) item);
}
else if (item instanceof MusicDirectory.Entry)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
if (entry.isDirectory())
{
onAlbumSelected(entry, false);
}
else if (entry.isVideo())
{
onVideoSelected(entry);
}
else
{
onSongSelected(entry, false, true, true, false);
}
}
}
}
});
registerForContextMenu(list);
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent)
{
super.onNewIntent(intent);
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);
if (query != null)
{
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
search(query, autoPlay);
}
else
{
populateList();
if (requestSearch) onSearchRequested();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
Object selectedItem = list.getItemAtPosition(info.position);
boolean isArtist = selectedItem instanceof Artist;
boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
if (!isArtist && !isAlbum)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_song_context, menu);
}
else
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_album_context, menu);
}
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
if (ActiveServerProvider.Companion.isOffline(this) || isArtist)
{
if (shareButton != null)
{
shareButton.setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return true;
}
Object selectedItem = list.getItemAtPosition(info.position);
Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null;
String entryId = null;
if (entry != null)
{
entryId = entry.getId();
}
String id = artist != null ? artist.getId() : entryId;
if (id == null)
{
return true;
}
List<Entry> songs = new ArrayList<Entry>(1);
switch (menuItem.getItemId())
{
case R.id.album_menu_play_now:
downloadRecursively(id, false, false, true, false, false, false, false, false);
break;
case R.id.album_menu_play_next:
downloadRecursively(id, false, true, false, true, false, true, false, false);
break;
case R.id.album_menu_play_last:
downloadRecursively(id, false, true, false, false, false, false, false, false);
break;
case R.id.album_menu_pin:
downloadRecursively(id, true, true, false, false, false, false, false, false);
break;
case R.id.album_menu_unpin:
downloadRecursively(id, false, false, false, false, false, false, true, false);
break;
case R.id.album_menu_download:
downloadRecursively(id, false, false, false, false, true, false, false, false);
break;
case R.id.song_menu_play_now:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(false, false, true, false, false, songs);
}
break;
case R.id.song_menu_play_next:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(true, false, false, true, false, songs);
}
break;
case R.id.song_menu_play_last:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(true, false, false, false, false, songs);
}
break;
case R.id.song_menu_pin:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
downloadBackground(true, songs);
}
break;
case R.id.song_menu_download:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
downloadBackground(false, songs);
}
break;
case R.id.song_menu_unpin:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getMediaPlayerController().unpin(songs);
}
break;
case R.id.menu_item_share:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
createShare(songs);
}
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
if (getMediaPlayerController() == null)
{
return;
}
Runnable onValid = new Runnable()
{
@Override
public void run()
{
warnIfNetworkOrStorageUnavailable();
getMediaPlayerController().downloadBackground(songs, save);
}
};
checkLicenseAndTrialPeriod(onValid);
}
private void search(final String query, final boolean autoplay)
{
final int maxArtists = Util.getMaxArtists(this);
final int maxAlbums = Util.getMaxAlbums(this);
final int maxSongs = Util.getMaxSongs(this);
BackgroundTask<SearchResult> task = new TabActivityBackgroundTask<SearchResult>(this, true)
{
@Override
protected SearchResult doInBackground() throws Throwable
{
SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs);
MusicService service = MusicServiceFactory.getMusicService(SearchActivity.this);
licenseValid = service.isLicenseValid(SearchActivity.this, this);
return service.search(criteria, SearchActivity.this, this);
}
@Override
protected void done(SearchResult result)
{
searchResult = result;
populateList();
if (autoplay)
{
autoplay();
}
}
};
task.execute();
}
private void populateList()
{
mergeAdapter = new MergeAdapter();
mergeAdapter.addView(searchButton, true);
if (searchResult != null)
{
List<Artist> artists = searchResult.getArtists();
if (!artists.isEmpty())
{
mergeAdapter.addView(artistsHeading);
List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
artistAdapter = new ArtistAdapter(this, displayedArtists);
mergeAdapter.addAdapter(artistAdapter);
if (artists.size() > DEFAULT_ARTISTS)
{
moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
}
}
List<MusicDirectory.Entry> albums = searchResult.getAlbums();
if (!albums.isEmpty())
{
mergeAdapter.addView(albumsHeading);
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
albumAdapter = new EntryAdapter(this, imageLoader.getValue().getImageLoader(), displayedAlbums, false);
mergeAdapter.addAdapter(albumAdapter);
if (albums.size() > DEFAULT_ALBUMS)
{
moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
}
}
List<MusicDirectory.Entry> songs = searchResult.getSongs();
if (!songs.isEmpty())
{
mergeAdapter.addView(songsHeading);
List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
songAdapter = new EntryAdapter(this, imageLoader.getValue().getImageLoader(), displayedSongs, false);
mergeAdapter.addAdapter(songAdapter);
if (songs.size() > DEFAULT_SONGS)
{
moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
}
}
boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
}
list.setAdapter(mergeAdapter);
}
private void expandArtists()
{
artistAdapter.clear();
for (Artist artist : searchResult.getArtists())
{
artistAdapter.add(artist);
}
artistAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreArtistsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandAlbums()
{
albumAdapter.clear();
for (MusicDirectory.Entry album : searchResult.getAlbums())
{
albumAdapter.add(album);
}
albumAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreAlbumsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandSongs()
{
songAdapter.clear();
for (MusicDirectory.Entry song : searchResult.getSongs())
{
songAdapter.add(song);
}
songAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreSongsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void onArtistSelected(Artist artist)
{
Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
startActivityForResultWithoutTransition(this, intent);
}
private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay)
{
Intent intent = new Intent(SearchActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, album.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
intent.putExtra(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
startActivityForResultWithoutTransition(SearchActivity.this, intent);
}
private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext)
{
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController != null)
{
if (!append && !playNext)
{
mediaPlayerController.clear();
}
mediaPlayerController.download(Collections.singletonList(song), save, false, playNext, false, false);
if (autoplay)
{
mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1);
}
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
}
}
private void onVideoSelected(MusicDirectory.Entry entry)
{
videoPlayer.getValue().playVideo(entry);
}
private void autoplay()
{
if (!searchResult.getSongs().isEmpty())
{
onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
}
else if (!searchResult.getAlbums().isEmpty())
{
onAlbumSelected(searchResult.getAlbums().get(0), true);
}
}
}

View File

@ -1,159 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.AlbumHeader;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.AlbumView;
import org.moire.ultrasonic.view.EntryAdapter;
import org.moire.ultrasonic.view.SongView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
public class SelectAlbumActivity extends SubsonicTabActivity
{
public static final String allSongsId = "-1";
private SwipeRefreshLayout refreshAlbumListView;
private ListView albumListView;
private View header;
private View albumButtons;
private View emptyView;
private ImageView selectButton;
private ImageView playNowButton;
private ImageView playNextButton;
private ImageView playLastButton;
private ImageView pinButton;
private ImageView unpinButton;
private ImageView downloadButton;
private ImageView deleteButton;
private ImageView moreButton;
private boolean playAllButtonVisible;
private boolean shareButtonVisible;
private MenuItem playAllButton;
private MenuItem shareButton;
private boolean showHeader = true;
private Random random = new java.security.SecureRandom();
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.select_album);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
super.onPrepareOptionsMenu(menu);
playAllButton = menu.findItem(R.id.select_album_play_all);
if (playAllButton != null)
{
playAllButton.setVisible(playAllButtonVisible);
}
shareButton = menu.findItem(R.id.menu_item_share);
if (shareButton != null)
{
shareButton.setVisible(shareButtonVisible);
}
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_album, menu);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
Intent intent1 = new Intent(this, DownloadActivity.class);
intent1.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent1);
return true;
case R.id.select_album_play_all:
// TODO
//playAll();
return true;
case R.id.menu_item_share:
// TODO
//createShare(getSelectedSongs(albumListView));
return true;
}
return false;
}
}

View File

@ -48,6 +48,7 @@ import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.fragment.SelectAlbumFragment;
import org.moire.ultrasonic.service.*;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.SubsonicImageLoaderProxy;
@ -344,7 +345,8 @@ public class SubsonicTabActivity extends ResultActivity
}
});
final Intent intent = new Intent(context, SelectAlbumActivity.class);
// TODO: Refactor to use navigation
final Intent intent = new Intent(context, SelectAlbumFragment.class);// SelectAlbumActivity.class);
if (Util.getShouldUseId3Tags(context))
{
@ -595,51 +597,6 @@ public class SubsonicTabActivity extends ResultActivity
task.execute();
}
public void setTextViewTextOnUiThread(final RemoteViews view, final int id, final CharSequence text)
{
this.runOnUiThread(new Runnable()
{
@Override
public void run()
{
if (view != null)
{
view.setTextViewText(id, text);
}
}
});
}
public void setImageViewBitmapOnUiThread(final RemoteViews view, final int id, final Bitmap bitmap)
{
this.runOnUiThread(new Runnable()
{
@Override
public void run()
{
if (view != null)
{
view.setImageViewBitmap(id, bitmap);
}
}
});
}
public void setImageViewResourceOnUiThread(final RemoteViews view, final int id, final int resource)
{
this.runOnUiThread(new Runnable()
{
@Override
public void run()
{
if (view != null)
{
view.setImageViewResource(id, resource);
}
}
});
}
public void setOnTouchListenerOnUiThread(final View view, final OnTouchListener listener)
{
this.runOnUiThread(new Runnable()
@ -720,29 +677,6 @@ public class SubsonicTabActivity extends ResultActivity
return instance;
}
public boolean getIsDestroyed()
{
return destroyed;
}
public void setProgressVisible(boolean visible)
{
View view = findViewById(R.id.tab_progress);
if (view != null)
{
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
public void updateProgress(CharSequence message)
{
TextView view = (TextView) findViewById(R.id.tab_progress_message);
if (view != null)
{
view.setText(message);
}
}
public MediaPlayerController getMediaPlayerController()
{
return mediaPlayerControllerLazy.getValue();
@ -760,292 +694,6 @@ public class SubsonicTabActivity extends ResultActivity
}
}
void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs)
{
if (getMediaPlayerController() == null)
{
return;
}
Runnable onValid = new Runnable()
{
@Override
public void run()
{
if (!append && !playNext)
{
getMediaPlayerController().clear();
}
warnIfNetworkOrStorageUnavailable();
getMediaPlayerController().download(songs, save, autoPlay, playNext, shuffle, false);
String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
if (playlistName != null)
{
getMediaPlayerController().setSuggestedPlaylistName(playlistName);
}
if (autoPlay)
{
if (Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this))
{
startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
}
}
else if (save)
{
Util.toast(SubsonicTabActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
}
else if (playNext)
{
Util.toast(SubsonicTabActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_play_next, songs.size(), songs.size()));
}
else if (append)
{
Util.toast(SubsonicTabActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
}
}
};
checkLicenseAndTrialPeriod(onValid);
}
protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext, final boolean unpin, final boolean isArtist)
{
downloadRecursively(id, "", false, true, save, append, autoplay, shuffle, background, playNext, unpin, isArtist);
}
protected void downloadShare(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext, final boolean unpin)
{
downloadRecursively(id, name, true, false, save, append, autoplay, shuffle, background, playNext, unpin, false);
}
protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext, final boolean unpin)
{
downloadRecursively(id, name, false, false, save, append, autoplay, shuffle, background, playNext, unpin, false);
}
protected void downloadRecursively(final String id, final String name, final boolean isShare, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext, final boolean unpin, final boolean isArtist)
{
ModalBackgroundTask<List<Entry>> task = new ModalBackgroundTask<List<Entry>>(this, false)
{
private static final int MAX_SONGS = 500;
@Override
protected List<Entry> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
List<Entry> songs = new LinkedList<Entry>();
MusicDirectory root;
if (!ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && isArtist && Util.getShouldUseId3Tags(SubsonicTabActivity.this))
{
getSongsForArtist(id, songs);
}
else
{
if (isDirectory)
{
root = !ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(id, name, false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(id, name, false, SubsonicTabActivity.this, this);
}
else if (isShare)
{
root = new MusicDirectory();
List<Share> shares = musicService.getShares(true, SubsonicTabActivity.this, this);
for (Share share : shares)
{
if (share.getId().equals(id))
{
for (Entry entry : share.getEntries())
{
root.addChild(entry);
}
break;
}
}
}
else
{
root = musicService.getPlaylist(id, name, SubsonicTabActivity.this, this);
}
getSongsRecursively(root, songs);
}
return songs;
}
private void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception
{
if (songs.size() > MAX_SONGS)
{
return;
}
for (Entry song : parent.getChildren(false, true))
{
if (!song.isVideo())
{
songs.add(song);
}
}
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
for (Entry dir : parent.getChildren(true, false))
{
MusicDirectory root;
root = !ActiveServerProvider.Companion.isOffline(SubsonicTabActivity.this) && Util.getShouldUseId3Tags(SubsonicTabActivity.this) ? musicService.getAlbum(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this) : musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this);
getSongsRecursively(root, songs);
}
}
private void getSongsForArtist(String id, Collection<Entry> songs) throws Exception
{
if (songs.size() > MAX_SONGS)
{
return;
}
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
MusicDirectory artist = musicService.getArtist(id, "", false, SubsonicTabActivity.this, this);
for (Entry album : artist.getChildren())
{
MusicDirectory albumDirectory = musicService.getAlbum(album.getId(), "", false, SubsonicTabActivity.this, this);
for (Entry song : albumDirectory.getChildren())
{
if (!song.isVideo())
{
songs.add(song);
}
}
}
}
@Override
protected void done(List<Entry> songs)
{
if (Util.getShouldSortByDisc(SubsonicTabActivity.this))
{
Collections.sort(songs, new EntryByDiscAndTrackComparator());
}
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (!songs.isEmpty() && mediaPlayerController != null)
{
if (!append && !playNext && !unpin && !background)
{
mediaPlayerController.clear();
}
warnIfNetworkOrStorageUnavailable();
if (!background)
{
if (unpin)
{
mediaPlayerController.unpin(songs);
}
else
{
mediaPlayerController.download(songs, save, autoplay, playNext, shuffle, false);
if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this))
{
startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
}
}
}
else
{
if (unpin)
{
mediaPlayerController.unpin(songs);
}
else
{
mediaPlayerController.downloadBackground(songs, save);
}
}
}
}
};
task.execute();
}
protected void checkLicenseAndTrialPeriod(Runnable onValid)
{
if (licenseValid)
{
onValid.run();
return;
}
int trialDaysLeft = Util.getRemainingTrialDays(this);
Timber.i("%s trial days left.", trialDaysLeft);
if (trialDaysLeft == 0)
{
showDonationDialog(trialDaysLeft, null);
}
else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2)
{
showDonationDialog(trialDaysLeft, onValid);
}
else
{
Util.toast(this, getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
onValid.run();
}
}
private void showDonationDialog(int trialDaysLeft, final Runnable onValid)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_info);
if (trialDaysLeft == 0)
{
builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
}
else
{
builder.setTitle(getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left, trialDaysLeft, trialDaysLeft));
}
builder.setMessage(R.string.select_album_donate_dialog_message);
builder.setPositiveButton(R.string.select_album_donate_dialog_now, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialogInterface, int i)
{
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
}
});
builder.setNegativeButton(R.string.select_album_donate_dialog_later, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialogInterface, int i)
{
dialogInterface.dismiss();
if (onValid != null)
{
onValid.run();
}
}
});
builder.create().show();
}
protected void setActionBarDisplayHomeAsUp(boolean enabled)
{
ActionBar actionBar = getSupportActionBar();
@ -1056,39 +704,6 @@ public class SubsonicTabActivity extends ResultActivity
}
}
protected void setActionBarTitle(CharSequence title)
{
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setTitle(title);
}
}
protected void setActionBarTitle(int id)
{
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setTitle(id);
}
}
protected CharSequence getActionBarTitle()
{
ActionBar actionBar = getSupportActionBar();
CharSequence title = null;
if (actionBar != null)
{
title = actionBar.getTitle();
}
return title;
}
protected void setActionBarSubtitle(CharSequence title)
{
ActionBar actionBar = getSupportActionBar();
@ -1109,19 +724,6 @@ public class SubsonicTabActivity extends ResultActivity
}
}
protected CharSequence getActionBarSubtitle()
{
ActionBar actionBar = getSupportActionBar();
CharSequence subtitle = null;
if (actionBar != null)
{
subtitle = actionBar.getSubtitle();
}
return subtitle;
}
private void setUncaughtExceptionHandler()
{
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
@ -1272,7 +874,8 @@ public class SubsonicTabActivity extends ResultActivity
}
}
SubsonicTabActivity.this.startActivityForResultWithoutTransition(activity, DownloadActivity.class);
// TODO: Refactor this to Navigation. It should automatically go to the PlayerFragment.
//SubsonicTabActivity.this.startActivityForResultWithoutTransition(activity, DownloadActivity.class);
return false;
}
}

View File

@ -1,61 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* Receives voice search queries and forwards to the SearchActivity.
* <p/>
* http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
*
* @author Sindre Mehus
*/
public class VoiceQueryReceiverActivity extends ResultActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
String query = getIntent().getStringExtra(SearchManager.QUERY);
if (query != null)
{
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE);
suggestions.saveRecentQuery(query, null);
Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
startActivityForResultWithoutTransition(VoiceQueryReceiverActivity.this, intent);
}
finish();
Util.disablePendingTransition(this);
}
}

View File

@ -0,0 +1,98 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Lyrics;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import timber.log.Timber;
public class LyricsFragment extends Fragment {
private TextView artistView;
private TextView titleView;
private TextView textView;
private SwipeRefreshLayout swipe;
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.lyrics, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
Timber.d("Lyrics set title");
FragmentTitle.Companion.setTitle(this, R.string.download_menu_lyrics);
swipe = view.findViewById(R.id.lyrics_refresh);
swipe.setEnabled(false);
artistView = view.findViewById(R.id.lyrics_artist);
titleView = view.findViewById(R.id.lyrics_title);
textView = view.findViewById(R.id.lyrics_text);
load();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load()
{
BackgroundTask<Lyrics> task = new TabActivityBackgroundTask<Lyrics>(getActivity(), true, swipe, cancellationToken)
{
@Override
protected Lyrics doInBackground() throws Throwable
{
String artist = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST);
String title = getArguments().getString(Constants.INTENT_EXTRA_NAME_TITLE);
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getLyrics(artist, title, getContext(), this);
}
@Override
protected void done(Lyrics result)
{
if (result != null && result.getArtist() != null)
{
artistView.setText(result.getArtist());
titleView.setText(result.getTitle());
textView.setText(result.getText());
}
else
{
artistView.setText(R.string.lyrics_nomatch);
}
}
};
task.execute();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -177,6 +177,22 @@ public class SearchFragment extends Fragment {
registerForContextMenu(list);
// Fragment was started with a query, try to execute search right away
Bundle arguments = getArguments();
if (arguments != null) {
String query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY);
boolean autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
if (query != null)
{
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
search(query, autoPlay);
return;
}
}
// Fragment was started from the Menu, create empty list
populateList();
}
@ -221,6 +237,7 @@ public class SearchFragment extends Fragment {
@Override
public boolean onQueryTextChange(String newText) { return true; }
});
searchView.setIconifiedByDefault(false);
searchItem.expandActionView();
}

View File

@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
@ -411,7 +412,7 @@ public class SelectAlbumFragment extends Fragment {
}
@Override
public void onPrepareOptionsMenu(Menu menu)
public void onPrepareOptionsMenu(@NotNull Menu menu)
{
super.onPrepareOptionsMenu(menu);
playAllButton = menu.findItem(R.id.select_album_play_all);

View File

@ -86,8 +86,9 @@ public class SettingsFragment extends PreferenceFragmentCompat
private SharedPreferences settings;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -517,7 +518,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
File dir = new File(path);
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
permissionUtil.getValue().handlePermissionFailed(new PermissionUtil.PermissionRequestFinishedCallback() {
@Override
public void onPermissionRequestFinished(boolean hasPermission) {
String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,

View File

@ -14,8 +14,7 @@ import android.view.KeyEvent;
import android.widget.RemoteViews;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController;
@ -181,15 +180,11 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
/**
* Link up various button actions using {@link PendingIntent}.
*
* @param playerActive True if player is active in background, which means
* widget click will launch {@link DownloadActivity},
* otherwise we launch {@link MainActivity}.
*/
private static void linkButtons(Context context, RemoteViews views, boolean playerActive)
{
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
// TODO: If playerActive = true, display the PlayerFragment automatically
Intent intent = new Intent(context, playerActive ? NavigationActivity.class : NavigationActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);
@ -198,21 +193,18 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
// Emulate media button clicks.
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);

View File

@ -19,12 +19,12 @@ import timber.log.Timber;
import android.widget.SeekBar;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.fragment.PlayerFragment;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
@ -598,7 +598,7 @@ public class LocalMediaPlayer
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent)
{
SeekBar progressBar = DownloadActivity.getProgressBar();
SeekBar progressBar = PlayerFragment.getProgressBar();
MusicDirectory.Entry song = downloadFile.getSong();
if (percent == 100)
@ -627,12 +627,12 @@ public class LocalMediaPlayer
setPlayerState(PREPARED);
SeekBar progressBar = DownloadActivity.getProgressBar();
SeekBar progressBar = PlayerFragment.getProgressBar();
if (progressBar != null && downloadFile.isWorkDone())
{
// Populate seek bar secondary progress if we have a complete file for consistency
DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax());
PlayerFragment.getProgressBar().setSecondaryProgress(100 * progressBar.getMax());
}
synchronized (LocalMediaPlayer.this)

View File

@ -20,7 +20,7 @@ import androidx.core.app.NotificationManagerCompat;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
@ -650,7 +650,8 @@ public class MediaPlayerService extends Service
notificationBuilder.setContent(contentView);
Intent notificationIntent = new Intent(this, DownloadActivity.class);
// TODO: Navigate automatically to PlayerFragment
Intent notificationIntent = new Intent(this, NavigationActivity.class);
notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0));
if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) {

View File

@ -60,7 +60,8 @@ public class FileUtil
private static final List<String> PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u");
private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*");
private static Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private static final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private static final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
public static File getSongFile(Context context, MusicDirectory.Entry song)
{
@ -396,7 +397,7 @@ public class FileUtil
File dir = new File(path);
boolean hasAccess = ensureDirectoryExistsAndIsReadWritable(dir);
if (hasAccess == false) PermissionUtil.handlePermissionFailed(context, null);
if (!hasAccess) permissionUtil.getValue().handlePermissionFailed(null);
return hasAccess ? dir : defaultMusicDirectory;
}

View File

@ -21,7 +21,6 @@ import com.karumi.dexter.listener.PermissionRequestErrorListener;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.MainActivity;
import java.util.List;
@ -32,6 +31,13 @@ import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
* Contains static functions for Permission handling
*/
public class PermissionUtil {
private final Context context;
public PermissionUtil(Context context) {
this.context = context;
}
public interface PermissionRequestFinishedCallback {
void onPermissionRequestFinished(boolean hasPermission);
}
@ -42,31 +48,27 @@ public class PermissionUtil {
* It will check if the failure is because the necessary permissions aren't available,
* and it will request them, if necessary.
*
* @param context context for the operation
* @param callback callback function to execute after the permission request is finished
*/
public static void handlePermissionFailed(final Context context, final PermissionRequestFinishedCallback callback) {
public void handlePermissionFailed(final PermissionRequestFinishedCallback callback) {
// TODO: Test with ApplicationContext
String currentCachePath = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(context).getPath());
String defaultCachePath = FileUtil.getDefaultMusicDirectory(context).getPath();
// Ultrasonic can do nothing about this error when the Music Directory is already set to the default.
if (currentCachePath.compareTo(defaultCachePath) == 0) return;
// We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available
final Context mainContext = MainActivity.getInstance();
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
(PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
// While we request permission, the Music Directory is temporarily reset to its default location
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
requestFailedPermission(mainContext, currentCachePath, callback);
requestFailedPermission(context, currentCachePath, callback);
} else {
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
showWarning(context, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
}
});
callback.onPermissionRequestFinished(false);

View File

@ -51,8 +51,7 @@ import android.widget.Toast;
import androidx.annotation.ColorInt;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -1020,8 +1019,8 @@ public class Util
public static void linkButtons(Context context, RemoteViews views, boolean playerActive)
{
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
// TODO: Navigate automatically to PlayerFragment if playerActive = true
Intent intent = new Intent(context, playerActive ? NavigationActivity.class : NavigationActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

View File

@ -1,9 +1,13 @@
package org.moire.ultrasonic.activity
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Intent
import android.content.res.Resources
import android.os.Bundle
import android.preference.PreferenceManager
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
@ -21,6 +25,7 @@ import com.google.android.material.navigation.NavigationView
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@ -59,7 +64,7 @@ class NavigationActivity : AppCompatActivity() {
setOf(R.id.mainFragment, R.id.selectArtistFragment, R.id.searchFragment,
R.id.playlistsFragment, R.id.sharesFragment, R.id.bookmarksFragment,
R.id.chatFragment, R.id.podcastFragment, R.id.settingsFragment,
R.id.aboutFragment),
R.id.aboutFragment, R.id.playerFragment),
drawerLayout)
setupActionBar(navController, appBarConfiguration)
@ -127,6 +132,25 @@ class NavigationActivity : AppCompatActivity() {
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
}
// TODO: Test if this works with external Intents
// android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val query = intent?.getStringExtra(SearchManager.QUERY)
if (query != null) {
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
val suggestions = SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE)
suggestions.saveRecentQuery(query, null)
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_QUERY, query)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(R.id.searchFragment, bundle)
}
}
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
val preferences = Util.getPreferences(this)

View File

@ -6,6 +6,7 @@ import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.di.appPermanentStorage
import org.moire.ultrasonic.di.applicationModule
import org.moire.ultrasonic.di.baseNetworkModule
import org.moire.ultrasonic.di.directoriesModule
import org.moire.ultrasonic.di.featureFlagsModule
@ -35,6 +36,7 @@ class UApp : MultiDexApplication() {
androidContext(this@UApp)
// declare modules to use
modules(
applicationModule,
directoriesModule,
appPermanentStorage,
baseNetworkModule,

View File

@ -31,8 +31,4 @@ val appPermanentStorage = module {
single { get<AppDatabase>().serverSettingDao() }
viewModel { ServerSettingsModel(get(), get(), androidContext()) }
single { ActiveServerProvider(get(), androidContext()) }
single { ImageLoaderProvider(androidContext()) }
}

View File

@ -0,0 +1,16 @@
package org.moire.ultrasonic.di
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.bind
import org.koin.dsl.module
import org.moire.ultrasonic.cache.AndroidDirectories
import org.moire.ultrasonic.cache.Directories
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.PermissionUtil
val applicationModule = module {
single { ActiveServerProvider(get(), androidContext()) }
single { ImageLoaderProvider(androidContext()) }
single { PermissionUtil(androidContext()) }
}

View File

@ -33,9 +33,8 @@ class DownloadHandler(
}
if (autoPlay) {
if (Util.getShouldTransitionOnPlaybackPreference(fragment.activity)) {
fragment.findNavController().popBackStack(R.id.downloadFragment, true)
fragment.findNavController().navigate(R.id.downloadFragment)
//startActivityForResultWithoutTransition(this@SubsonicTabActivity, DownloadActivity::class.java)
fragment.findNavController().popBackStack(R.id.playerFragment, true)
fragment.findNavController().navigate(R.id.playerFragment)
}
} else if (save) {
Util.toast(fragment.context, fragment.resources.getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size, songs.size))
@ -190,9 +189,8 @@ class DownloadHandler(
} else {
mediaPlayerController.download(songs, save, autoPlay, playNext, shuffle, false)
if (!append && Util.getShouldTransitionOnPlaybackPreference(activity)) {
fragment.findNavController().popBackStack(R.id.downloadFragment, true)
fragment.findNavController().navigate(R.id.downloadFragment)
//startActivityForResultWithoutTransition(activity, DownloadActivity::class.java)
fragment.findNavController().popBackStack(R.id.playerFragment, true)
fragment.findNavController().navigate(R.id.playerFragment)
}
}
} else {

View File

@ -32,7 +32,7 @@ class ShareHandler(val context: Context) {
private var saveAsDefaultsCheckBox: CheckBox? = null
private val pattern = Pattern.compile(":")
fun createShare(fragment: Fragment, entries: List<MusicDirectory.Entry?>?, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
fun createShare(fragment: Fragment, entries: List<MusicDirectory.Entry?>?, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken) {
val askForDetails = Util.getShouldAskForShareDetails(context)
val shareDetails = ShareDetails()
shareDetails.Entries = entries
@ -45,7 +45,7 @@ class ShareHandler(val context: Context) {
}
}
fun share(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
fun share(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken) {
val task: BackgroundTask<Share> = object : TabActivityBackgroundTask<Share>(fragment.requireActivity(), true, swipe, cancellationToken) {
@Throws(Throwable::class)
override fun doInBackground(): Share {
@ -76,7 +76,7 @@ class ShareHandler(val context: Context) {
task.execute()
}
private fun showDialog(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout, cancellationToken: CancellationToken) {
private fun showDialog(fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken) {
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
if (layout != null) {

View File

@ -4,53 +4,61 @@
a:layout_width="fill_parent"
a:layout_height="fill_parent">
<include layout="@layout/tab_progress"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/lyrics_refresh"
a:layout_width="fill_parent"
a:layout_height="0dp"
a:layout_weight="1.0">
<ScrollView
a:id="@+id/lyrics_scrollview"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<LinearLayout
a:orientation="vertical"
<androidx.core.widget.NestedScrollView
a:id="@+id/lyrics_scrollview"
a:layout_width="fill_parent"
a:layout_height="fill_parent">
<TextView
a:id="@+id/lyrics_artist"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_horizontal"
a:layout_height="wrap_content"
a:scrollbars="vertical"
a:layout_weight="1.0">
<LinearLayout
a:orientation="vertical"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingLeft="10dip"
a:paddingRight="10dip"
a:paddingTop="10dip"
a:paddingBottom="4dip"
/>
a:paddingVertical="32dip">
<TextView
a:id="@+id/lyrics_artist"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingLeft="10dip"
a:paddingRight="10dip"
a:paddingTop="10dip"
a:paddingBottom="4dip"
/>
<TextView
a:id="@+id/lyrics_title"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingLeft="10dip"
a:paddingRight="10dip"
/>
<TextView
a:id="@+id/lyrics_title"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingLeft="10dip"
a:paddingRight="10dip"
/>
<TextView
a:id="@+id/lyrics_text"
a:textAppearance="?android:attr/textAppearanceSmall"
a:gravity="center_horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingLeft="10dip"
a:paddingRight="10dip"
/>
<TextView
a:id="@+id/lyrics_text"
a:textAppearance="?android:attr/textAppearanceSmall"
a:gravity="center_horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingTop="32dip"
a:paddingLeft="10dip"
a:paddingRight="10dip"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/now_playing"/>

View File

@ -41,7 +41,7 @@
a:icon="?attr/chat"
a:title="@string/button_bar.chat" />
<item
a:id="@+id/menu_now_playing"
a:id="@+id/playerFragment"
a:checkable="true"
a:icon="?attr/media_play"
a:title="@string/button_bar.now_playing" />

View File

@ -22,64 +22,64 @@
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/downloadFragment"
android:name="org.moire.ultrasonic.fragment.DownloadFragment" />
<fragment
android:id="@+id/selectAlbumFragment"
android:name="org.moire.ultrasonic.fragment.SelectAlbumFragment"
android:label="SelectAlbumFragment" >
android:name="org.moire.ultrasonic.fragment.SelectAlbumFragment" >
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="org.moire.ultrasonic.fragment.SearchFragment"
android:label="SearchFragment" >
android:name="org.moire.ultrasonic.fragment.SearchFragment" >
<action
android:id="@+id/searchToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/playlistsFragment"
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment"
android:label="PlaylistsFragment" >
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment" >
<action
android:id="@+id/playlistsToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/sharesFragment"
android:name="org.moire.ultrasonic.fragment.SharesFragment"
android:label="SharesFragment" >
android:name="org.moire.ultrasonic.fragment.SharesFragment" >
<action
android:id="@+id/sharesToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/bookmarksFragment"
android:name="org.moire.ultrasonic.fragment.BookmarksFragment"
android:label="BookmarksFragment" />
android:name="org.moire.ultrasonic.fragment.BookmarksFragment" />
<fragment
android:id="@+id/chatFragment"
android:name="org.moire.ultrasonic.fragment.ChatFragment"
android:label="ChatFragment" />
android:name="org.moire.ultrasonic.fragment.ChatFragment" />
<fragment
android:id="@+id/podcastFragment"
android:name="org.moire.ultrasonic.fragment.PodcastFragment"
android:label="PodcastFragment" >
android:name="org.moire.ultrasonic.fragment.PodcastFragment" >
<action
android:id="@+id/podcastToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="org.moire.ultrasonic.fragment.SettingsFragment"
android:label="SettingsFragment" />
android:name="org.moire.ultrasonic.fragment.SettingsFragment" />
<fragment
android:id="@+id/aboutFragment"
android:name="org.moire.ultrasonic.fragment.AboutFragment"
android:label="AboutFragment" />
android:name="org.moire.ultrasonic.fragment.AboutFragment" />
<fragment
android:id="@+id/selectGenreFragment"
android:name="org.moire.ultrasonic.fragment.SelectGenreFragment"
android:label="SelectGenreFragment" />
android:name="org.moire.ultrasonic.fragment.SelectGenreFragment" />
<fragment
android:id="@+id/playerFragment"
android:name="org.moire.ultrasonic.fragment.PlayerFragment" >
<action
android:id="@+id/playerToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
<action
android:id="@+id/playerToLyrics"
app:destination="@id/lyricsFragment" />
</fragment>
<fragment
android:id="@+id/lyricsFragment"
android:name="org.moire.ultrasonic.fragment.LyricsFragment" />
</navigation>