diff options
Diffstat (limited to 'app')
27 files changed, 399 insertions, 435 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DetailsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DetailsAdapter.java new file mode 100644 index 00000000..efafe27a --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/DetailsAdapter.java @@ -0,0 +1,62 @@ +/* + 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.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +import github.daneren2005.dsub.R; + +public class DetailsAdapter extends ArrayAdapter<String> { + private List<String> headers; + private List<String> details; + + public DetailsAdapter(Context context, int layout, List<String> headers, List<String> details) { + super(context, layout, headers); + + this.headers = headers; + this.details = details; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent){ + View view; + if(convertView == null) { + view = LayoutInflater.from(getContext()).inflate(R.layout.details_item, null); + } else { + view = convertView; + } + + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + + nameView.setText(headers.get(position)); + + detailsView.setText(details.get(position)); + Linkify.addLinks(detailsView, Linkify.ALL); + + return view; + } +} 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 9d577409..3f6086d5 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java @@ -178,9 +178,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH if(sections.size() == 1 && !singleSectionHeader) { T item = sections.get(0).get(position); onBindViewHolder(holder, item, getItemViewType(position)); - if(updateView.isCheckable()) { - setChecked(updateView, selected.contains(item)); - } + postBindView(updateView, item); holder.setItem(item); return; } @@ -199,9 +197,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH T item = section.get(position - subPosition - headerOffset); onBindViewHolder(holder, item, getItemViewType(item)); - if(updateView.isCheckable()) { - setChecked(updateView, selected.contains(item)); - } + postBindView(updateView, item); holder.setItem(item); return; } @@ -214,6 +210,27 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH } } + private void postBindView(UpdateView updateView, T item) { + if(updateView.isCheckable()) { + setChecked(updateView, selected.contains(item)); + } + + View moreButton = updateView.findViewById(R.id.more_button); + if(moreButton == null) { + moreButton = updateView.findViewById(R.id.item_more); + } + if(moreButton != null) { + PopupMenu popup = new PopupMenu(context, moreButton); + Menu menu = popup.getMenu(); + onItemClickedListener.onCreateContextMenu(popup.getMenu(), popup.getMenuInflater(), updateView, item); + if(menu.size() == 0) { + moreButton.setVisibility(View.GONE); + } else { + moreButton.setVisibility(View.VISIBLE); + } + } + } + @Override public int getItemCount() { if(sections.size() == 1 && !singleSectionHeader) { 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 87da5d9c..d25315ac 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java @@ -1,5 +1,6 @@ package github.daneren2005.dsub.fragments; +import android.content.res.Resources; import android.support.v7.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; @@ -277,32 +278,54 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { } private void showAboutDialog() { - new LoadingTask<String>(context) { + new LoadingTask<Void>(context) { + Long[] used; + long bytesTotalFs; + long bytesAvailableFs; + @Override - protected String doInBackground() throws Throwable { + protected Void doInBackground() throws Throwable { File rootFolder = FileUtil.getMusicDirectory(context); StatFs stat = new StatFs(rootFolder.getPath()); - long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); - long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); - - Pair<Long, Long> used = FileUtil.getUsedSize(context, rootFolder); - - return getResources().getString(R.string.main_about_text, - context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName, - used.getFirst(), - Util.formatLocalizedBytes(used.getSecond(), context), - Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context), - Util.formatLocalizedBytes(bytesAvailableFs, context), - Util.formatLocalizedBytes(bytesTotalFs, context)); + bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + + used = FileUtil.getUsedSize(context, rootFolder); + return null; } @Override - protected void done(String msg) { + protected void done(Void result) { + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + headers.add(R.string.details_author); + details.add("Scott Jackson"); + + headers.add(R.string.details_email); + details.add("dsub.android@gmail.com"); + try { - Util.info(context, R.string.main_about_title, msg); + headers.add(R.string.details_version); + details.add(context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName); } catch(Exception e) { - Util.toast(context, "Failed to open dialog"); + details.add(""); } + + Resources res = context.getResources(); + headers.add(R.string.details_files_cached); + details.add(Long.toString(used[0])); + + headers.add(R.string.details_files_permanent); + details.add(Long.toString(used[1])); + + headers.add(R.string.details_used_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(used[2], context), Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context))); + + headers.add(R.string.details_available_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(bytesAvailableFs, context), Util.formatLocalizedBytes(bytesTotalFs, context))); + + Util.showDetailsDialog(context, R.string.main_about_title, headers, details); } }.execute(); } 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 2ab8316c..555e308e 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -360,18 +360,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } @Override public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Entry> updateView, Entry entry) { - if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, false) && menuItem.getItemId() == R.id.song_menu_play_now) { - List<Entry> songs = new ArrayList<Entry>(); - songs.add(entry); - Iterator it = entries.listIterator(entries.indexOf(entry)); - while(it.hasNext()) { - songs.add((Entry) it.next()); - } - - playNow(songs); - return true; - } - if(onContextItemSelected(menuItem, entry)) { return true; } 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 5b7e68a6..b1587e24 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -68,40 +68,13 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { @Override public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Playlist> updateView, Playlist playlist) { - SubsonicFragment fragment; - Bundle args; - FragmentTransaction trans; switch (menuItem.getItemId()) { - case R.id.playlist_menu_download: - downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true); - break; case R.id.playlist_menu_sync: syncPlaylist(playlist); break; case R.id.playlist_menu_stop_sync: stopSyncPlaylist(playlist); break; - case R.id.playlist_menu_play_now: - fragment = new SelectDirectoryFragment(); - args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); - args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); - fragment.setArguments(args); - - replaceFragment(fragment); - break; - case R.id.playlist_menu_play_shuffled: - fragment = new SelectDirectoryFragment(); - args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); - args.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true); - args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); - fragment.setArguments(args); - - replaceFragment(fragment); - break; case R.id.playlist_menu_delete: deletePlaylist(playlist); break; @@ -213,12 +186,33 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { } private void displayPlaylistInfo(final Playlist playlist) { - String message = "Owner: " + playlist.getOwner() + "\nComments: " + - ((playlist.getComment() == null) ? "" : playlist.getComment()) + - "\nSong Count: " + playlist.getSongCount() + - ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic())) + - "\nCreated: " + Util.formatDate(context, playlist.getCreated()); - Util.info(context, playlist.getName(), message); + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + if(playlist.getOwner() != null) { + headers.add(R.string.details_owner); + details.add(playlist.getOwner()); + } + + if(playlist.getComment() != null) { + headers.add(R.string.details_comments); + details.add(playlist.getComment()); + } + + headers.add(R.string.details_song_count); + details.add(playlist.getSongCount()); + + if(playlist.getPublic() != null) { + headers.add(R.string.details_public); + details.add(Boolean.toString(playlist.getPublic())); + } + + if(playlist.getCreated() != null) { + headers.add(R.string.details_created); + details.add(Util.formatDate(context, playlist.getCreated())); + } + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); } private void updatePlaylistInfo(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 f8afce6e..381453c0 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -214,13 +214,29 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<PodcastChanne } private void displayPodcastInfo(final PodcastChannel channel) { - String message = ((channel.getName()) == null ? "" : "Title: " + channel.getName()) + - "\nURL: " + channel.getUrl() + - "\nStatus: " + channel.getStatus() + - ((channel.getErrorMessage()) == null ? "" : "\nError Message: " + channel.getErrorMessage()) + - ((channel.getDescription()) == null ? "" : "\n\nDescription: " + channel.getDescription()); - - Util.info(context, channel.getName(), message); + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + if(channel.getName() != null) { + headers.add(R.string.details_title); + details.add(channel.getName()); + } + + headers.add(R.string.details_url); + details.add(channel.getUrl()); + headers.add(R.string.details_status); + details.add(channel.getStatus()); + + if(channel.getErrorMessage() != null) { + headers.add(R.string.details_error); + details.add(channel.getErrorMessage()); + } + if(channel.getDescription() != null) { + headers.add(R.string.details_description); + details.add(channel.getDescription()); + } + + Util.showDetailsDialog(context, R.string.details_title_podcast, headers, details); } private void deletePodcast(final PodcastChannel channel) { 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 438649c3..6925b2da 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -241,9 +241,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } else if(selected instanceof Artist) { Artist artist = (Artist) selected; if(Util.isOffline(context)) { - menuInflater.inflate(R.menu.select_artist_context_offline, menu); - } - else { + // menuInflater.inflate(R.menu.select_artist_context_offline, menu); + } else { menuInflater.inflate(R.menu.select_artist_context, menu); menu.findItem(R.id.artist_menu_star).setTitle(artist.isStarred() ? R.string.common_unstar : R.string.common_star); @@ -281,56 +280,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_RATING, true)) { menu.setGroupVisible(R.id.hide_rating, false); } - - if(!Util.isOffline(context)) { - // If we are looking at a standard song view, get downloadFile to cache what options to show - if(updateView instanceof SongView) { - SongView songView = (SongView) updateView; - DownloadFile downloadFile = songView.getDownloadFile(); - - try { - if(downloadFile != null) { - if(downloadFile.isWorkDone()) { - // Remove permanent cache menu if already perma cached - if(downloadFile.isSaved()) { - menu.removeItem(R.id.song_menu_pin); - } - - // Remove cache option no matter what if already downloaded - menu.removeItem(R.id.song_menu_download); - } else { - // Remove delete option if nothing to delete - menu.removeItem(R.id.song_menu_delete); - } - } - } catch(Exception e) { - Log.w(TAG, "Failed to lookup downloadFile info", e); - } - } - // Apply similar logic to album views - else if(updateView instanceof AlbumView || updateView instanceof ArtistView || updateView instanceof ArtistEntryView) { - File folder = null; - int id = 0; - if(updateView instanceof AlbumView) { - folder = ((AlbumView) updateView).getFile(); - id = R.id.album_menu_delete; - } else if(updateView instanceof ArtistView) { - folder = ((ArtistView) updateView).getFile(); - id = R.id.artist_menu_delete; - } else if(updateView instanceof ArtistEntryView) { - folder = ((ArtistEntryView) updateView).getFile(); - id = R.id.artist_menu_delete; - } - - try { - if(folder != null && !folder.exists()) { - menu.removeItem(id); - } - } catch(Exception e) { - Log.w(TAG, "Failed to lookup album directory info", e); - } - } - } } protected void recreateContextMenu(Menu menu) { @@ -355,60 +304,12 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR songs.add(entry); switch (menuItem.getItemId()) { - case R.id.artist_menu_play_now: - downloadRecursively(artist.getId(), false, false, true, false, false); - break; - case R.id.artist_menu_play_shuffled: - downloadRecursively(artist.getId(), false, false, true, true, false); - break; - case R.id.artist_menu_play_next: - downloadRecursively(artist.getId(), false, true, false, false, false, true); - break; - case R.id.artist_menu_play_last: - downloadRecursively(artist.getId(), false, true, false, false, false); - break; - case R.id.artist_menu_download: - downloadRecursively(artist.getId(), false, true, false, false, true); - break; - case R.id.artist_menu_pin: - downloadRecursively(artist.getId(), true, true, false, false, true); - break; - case R.id.artist_menu_delete: - deleteRecursively(artist); - break; case R.id.artist_menu_star: toggleStarred(artist); break; - case R.id.album_menu_play_now: - artistOverride = true; - downloadRecursively(entry.getId(), false, false, true, false, false); - break; - case R.id.album_menu_play_shuffled: - artistOverride = true; - downloadRecursively(entry.getId(), false, false, true, true, false); - break; - case R.id.album_menu_play_next: - artistOverride = true; - downloadRecursively(entry.getId(), false, true, false, false, false, true); - break; - case R.id.album_menu_play_last: - artistOverride = true; - downloadRecursively(entry.getId(), false, true, false, false, false); - break; - case R.id.album_menu_download: - artistOverride = true; - downloadRecursively(entry.getId(), false, true, false, false, true); - break; - case R.id.album_menu_pin: - artistOverride = true; - downloadRecursively(entry.getId(), true, true, false, false, true); - break; case R.id.album_menu_star: toggleStarred(entry); break; - case R.id.album_menu_delete: - deleteRecursively(entry); - break; case R.id.album_menu_info: displaySongInfo(entry); break; @@ -418,21 +319,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; - case R.id.song_menu_play_last: - getDownloadService().download(songs, false, false, false, false); - break; case R.id.song_menu_download: getDownloadService().downloadBackground(songs, false); break; - case R.id.song_menu_pin: - getDownloadService().downloadBackground(songs, true); - break; case R.id.song_menu_delete: getDownloadService().delete(songs); break; @@ -1262,7 +1151,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR }.execute(); } - @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) public void displaySongInfo(final Entry song) { Integer duration = null; Integer bitrate = null; @@ -1302,55 +1190,83 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } - String msg = ""; + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); if(song instanceof PodcastEpisode) { - msg += "Podcast: " + song.getArtist() + "\nStatus: " + ((PodcastEpisode)song).getStatus(); + headers.add(R.string.details_podcast); + details.add(song.getArtist()); + + headers.add(R.string.details_status); + details.add(((PodcastEpisode)song).getStatus()); } else if(!song.isVideo()) { if(song.getArtist() != null && !"".equals(song.getArtist())) { - msg += "Artist: " + song.getArtist(); + headers.add(R.string.details_artist); + details.add(song.getArtist()); } if(song.getAlbum() != null && !"".equals(song.getAlbum())) { - msg += "\nAlbum: " + song.getAlbum(); + headers.add(R.string.details_album); + details.add(song.getAlbum()); } } if(song.getTrack() != null && song.getTrack() != 0) { - msg += "\nTrack: " + song.getTrack(); + headers.add(R.string.details_track); + details.add(Integer.toString(song.getTrack())); } if(song.getGenre() != null && !"".equals(song.getGenre())) { - msg += "\nGenre: " + song.getGenre(); + headers.add(R.string.details_genre); + details.add(song.getGenre()); } if(song.getYear() != null && song.getYear() != 0) { - msg += "\nYear: " + song.getYear(); + headers.add(R.string.details_year); + details.add(Integer.toString(song.getYear())); } if(!Util.isOffline(context) && song.getSuffix() != null) { - msg += "\nServer Format: " + song.getSuffix(); + headers.add(R.string.details_server_format); + details.add(song.getSuffix()); + if(song.getBitRate() != null && song.getBitRate() != 0) { - msg += "\nServer Bitrate: " + song.getBitRate() + " kbps"; + headers.add(R.string.details_server_bitrate); + details.add(song.getBitRate() + " kbps"); } } if(format != null && !"".equals(format)) { - msg += "\nCached Format: " + format; + headers.add(R.string.details_cached_format); + details.add(format); } if(bitrate != null && bitrate != 0) { - msg += "\nCached Bitrate: " + bitrate + " kbps"; + headers.add(R.string.details_cached_bitrate); + details.add(bitrate + " kbps"); } if(size != 0) { - msg += "\nSize: " + Util.formatLocalizedBytes(size, context); + headers.add(R.string.details_size); + details.add(Util.formatLocalizedBytes(size, context)); } if(song.getDuration() != null && song.getDuration() != 0) { - msg += "\nLength: " + Util.formatDuration(song.getDuration()); + headers.add(R.string.details_length); + details.add(Util.formatDuration(song.getDuration())); } if(song.getBookmark() != null) { - msg += "\nBookmark Position: " + Util.formatDuration(song.getBookmark().getPosition() / 1000); + headers.add(R.string.details_bookmark_position); + details.add(Util.formatDuration(song.getBookmark().getPosition() / 1000)); } if(song.getRating() != 0) { - msg += "\nRating: " + song.getRating() + " stars"; + headers.add(R.string.details_rating); + details.add(song.getRating() + " stars"); } if(song instanceof PodcastEpisode) { - msg += "\n\nDescription: " + song.getAlbum(); + headers.add(R.string.details_description); + details.add(song.getAlbum()); } - Util.info(context, song.getTitle(), msg); + int title; + if(song.isDirectory()) { + title = R.string.details_title_album; + } else if(song instanceof PodcastEpisode) { + title = R.string.details_title_podcast; + } else { + title = R.string.details_title_song; + } + Util.showDetailsDialog(context, title, headers, details); } protected void playVideo(Entry entry) { 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 b4105d07..8ebf4c13 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -382,6 +382,42 @@ public class OfflineMusicService implements MusicService { String id = file.getName(); String filename = server + ": " + FileUtil.getBaseName(id); Playlist playlist = new Playlist(server, filename); + + Reader reader = null; + BufferedReader buffer = null; + try { + int songCount = 0; + reader = new FileReader(file); + buffer = new BufferedReader(reader); + + String line = buffer.readLine(); + while( (line = buffer.readLine()) != null ){ + // No matter what, end file can't have .complete in it + line = line.replace(".complete", ""); + File entryFile = new File(line); + + // Don't add file to playlist if it doesn't exist as cached or pinned! + File checkFile = entryFile; + if(!checkFile.exists()) { + // If normal file doens't exist, check if .complete version does + checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName()) + + ".complete." + FileUtil.getExtension(entryFile.getName())); + } + + String entryName = getName(entryFile); + if(checkFile.exists() && entryName != null){ + songCount++; + } + } + + playlist.setSongCount(Integer.toString(songCount)); + } catch(Exception e) { + Log.w(TAG, "Failed to count songs in playlist", e); + } finally { + Util.close(buffer); + Util.close(reader); + } + playlists.add(playlist); } } @@ -684,10 +720,16 @@ public class OfflineMusicService implements MusicService { for(File file: dir.listFiles()) { BufferedReader br = new BufferedReader(new FileReader(file)); while ((line = br.readLine()) != null && !"".equals(line)) { + String[] parts = line.split("\t"); + PodcastChannel channel = new PodcastChannel(); - channel.setId(line); - channel.setName(line); + channel.setId(parts[0]); + channel.setName(parts[0]); channel.setStatus("completed"); + + if(parts.length > 1) { + channel.setUrl(parts[1]); + } if(FileUtil.getPodcastDirectory(context, channel).exists() && !channels.contains(channel)) { channels.add(channel); 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 459c8c9e..ed34e1cb 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -1204,7 +1204,7 @@ public class RESTMusicService implements MusicService { String content = ""; for(PodcastChannel channel: channels) { - content += channel.getName() + "\n"; + content += channel.getName() + "\t" + channel.getUrl() + "\n"; } File file = FileUtil.getPodcastFile(context, Util.getServerName(context, getInstance(context))); diff --git a/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java index 990eae06..185a6b29 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java +++ b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java @@ -741,23 +741,28 @@ public class FileUtil { return index == -1 ? name : name.substring(0, index); } - public static Pair<Long, Long> getUsedSize(Context context, File file) { + public static Long[] getUsedSize(Context context, File file) { long number = 0L; + long permanent = 0L; long size = 0L; if(file.isFile()) { if(isMediaFile(file)) { - return new Pair<Long, Long>(1L, file.length()); + if(file.getAbsolutePath().indexOf(".complete") == -1) { + permanent++; + } + return new Long[] {1L, permanent, file.length()}; } else { - return new Pair<Long, Long>(0L, 0L); + return new Long[] {0L, 0L, 0L}; } } else { for (File child : FileUtil.listFiles(file)) { - Pair<Long, Long> pair = getUsedSize(context, child); - number += pair.getFirst(); - size += pair.getSecond(); + Long[] pair = getUsedSize(context, child); + number += pair[0]; + permanent += pair[1]; + size += pair[2]; } - return new Pair<Long, Long>(number, size); + return new Long[] {number, permanent, size}; } } 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 ce26bf1e..db0cb7c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -19,6 +19,7 @@ package github.daneren2005.dsub.util; import android.annotation.TargetApi; import android.app.Activity; +import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.content.ComponentName; import android.content.Context; @@ -45,11 +46,14 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.view.Gravity; +import android.widget.ArrayAdapter; +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; import github.daneren2005.dsub.domain.RepeatMode; @@ -73,8 +77,10 @@ import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -1137,6 +1143,32 @@ public final class Util { ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } + public static void showDetailsDialog(Context context, @StringRes int title, List<Integer> headers, List<String> details) { + List<String> headerStrings = new ArrayList<>(); + for(@StringRes Integer res: headers) { + headerStrings.add(context.getResources().getString(res)); + } + showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); + } + public static void showDetailsDialog(Context context, String title, List<String> headers, 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); + + new AlertDialog.Builder(context) + // .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(title) + .setView(listView) + .setPositiveButton(R.string.common_close, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + } + public static void sleepQuietly(long millis) { try { Thread.sleep(millis); diff --git a/app/src/main/res/layout/details_item.xml b/app/src/main/res/layout/details_item.xml new file mode 100644 index 00000000..4ef5fef0 --- /dev/null +++ b/app/src/main/res/layout/details_item.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="8dp"> + + <TextView + android:id="@+id/detail_name" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorPrimary" + android:paddingLeft="14dp" + android:layout_gravity="center_vertical"/> + + <TextView + android:id="@+id/detail_value" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:textColorSecondary" + android:paddingLeft="14dp" + android:layout_gravity="center_vertical"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/menu/select_album_context.xml b/app/src/main/res/menu/select_album_context.xml index 5b2529e7..388fd1f5 100644 --- a/app/src/main/res/menu/select_album_context.xml +++ b/app/src/main/res/menu/select_album_context.xml @@ -1,48 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - +<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/album_menu_info" android:title="@string/common.info"/> - <item - android:id="@+id/album_menu_play_now" - android:title="@string/common.play_now" - /> - - <item - android:id="@+id/album_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> - - <group android:id="@+id/hide_play_next"> - <item - android:id="@+id/album_menu_play_next" - android:title="@string/common.play_next"/> - </group> - - <group android:id="@+id/hide_play_last"> - <item - android:id="@+id/album_menu_play_last" - android:title="@string/common.play_last"/> - </group> - - <item - android:id="@+id/album_menu_download" - android:title="@string/common.download" - /> - - <item - android:id="@+id/album_menu_pin" - android:title="@string/common.pin" - /> - - <item - android:id="@+id/album_menu_delete" - android:title="@string/menu.delete_cache"/> - <item android:id="@+id/album_menu_show_artist" android:title="@string/menu.show_artist"/> diff --git a/app/src/main/res/menu/select_album_context_offline.xml b/app/src/main/res/menu/select_album_context_offline.xml index a1805f5b..60858d91 100644 --- a/app/src/main/res/menu/select_album_context_offline.xml +++ b/app/src/main/res/menu/select_album_context_offline.xml @@ -1,31 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - <item - android:id="@+id/album_menu_play_now" - android:title="@string/common.play_now" - /> - - <item - android:id="@+id/album_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> - - <group android:id="@+id/hide_play_next"> - <item - android:id="@+id/album_menu_play_next" - android:title="@string/common.play_next"/> - </group> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> - <group android:id="@+id/hide_play_last"> - <item - android:id="@+id/album_menu_play_last" - android:title="@string/common.play_last"/> - </group> - - <item - android:id="@+id/album_menu_delete" - android:title="@string/menu.delete_cache"/> + <item + android:id="@+id/album_menu_info" + android:title="@string/common.info"/> <item android:id="@+id/album_menu_star" diff --git a/app/src/main/res/menu/select_artist_context.xml b/app/src/main/res/menu/select_artist_context.xml index debc07c6..b8bce32d 100644 --- a/app/src/main/res/menu/select_artist_context.xml +++ b/app/src/main/res/menu/select_artist_context.xml @@ -1,43 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/artist_menu_play_now" - android:title="@string/common.play_now" - /> - - <item - android:id="@+id/artist_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> - - <group android:id="@+id/hide_play_next"> - <item - android:id="@+id/artist_menu_play_next" - android:title="@string/common.play_next"/> - </group> - - <group android:id="@+id/hide_play_last"> - <item - android:id="@+id/artist_menu_play_last" - android:title="@string/common.play_last"/> - </group> - - <item - android:id="@+id/artist_menu_download" - android:title="@string/common.download" - /> - - <item - android:id="@+id/artist_menu_pin" - android:title="@string/common.pin" - /> - - <item - android:id="@+id/artist_menu_delete" - android:title="@string/menu.delete_cache"/> - +<menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:id="@+id/server_1_10"> <group android:id="@+id/hide_star"> <item diff --git a/app/src/main/res/menu/select_artist_context_offline.xml b/app/src/main/res/menu/select_artist_context_offline.xml deleted file mode 100644 index 17ee97e0..00000000 --- a/app/src/main/res/menu/select_artist_context_offline.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/artist_menu_play_now" - android:title="@string/common.play_now" - /> - - <item - android:id="@+id/artist_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> - - <group android:id="@+id/hide_play_next"> - <item - android:id="@+id/artist_menu_play_next" - android:title="@string/common.play_next"/> - </group> - - <group android:id="@+id/hide_play_last"> - <item - android:id="@+id/artist_menu_play_last" - android:title="@string/common.play_last"/> - </group> - - <item - android:id="@+id/artist_menu_delete" - android:title="@string/menu.delete_cache"/> -</menu> diff --git a/app/src/main/res/menu/select_bookmark_context.xml b/app/src/main/res/menu/select_bookmark_context.xml index 2b1b83fd..d52db105 100644 --- a/app/src/main/res/menu/select_bookmark_context.xml +++ b/app/src/main/res/menu/select_bookmark_context.xml @@ -15,18 +15,6 @@ android:title="@string/menu.show_artist"/> <item - android:id="@+id/song_menu_download" - android:title="@string/common.download"/> - - <item - android:id="@+id/song_menu_pin" - android:title="@string/common.pin"/> - - <item - android:id="@+id/song_menu_delete" - android:title="@string/menu.delete_cache"/> - - <item android:id="@+id/bookmark_menu_delete" android:title="@string/bookmark.delete"/> </menu> diff --git a/app/src/main/res/menu/select_playlist_context.xml b/app/src/main/res/menu/select_playlist_context.xml index 47033d9c..4941e94b 100644 --- a/app/src/main/res/menu/select_playlist_context.xml +++ b/app/src/main/res/menu/select_playlist_context.xml @@ -7,21 +7,6 @@ android:title="@string/common.info" /> - <item - android:id="@+id/playlist_menu_play_now" - android:title="@string/common.play_now" - /> - - <item - android:id="@+id/playlist_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> - - <item - android:id="@+id/playlist_menu_download" - android:title="@string/common.download" - /> - <item android:id="@+id/playlist_menu_sync" android:title="@string/menu.keep_synced"/> diff --git a/app/src/main/res/menu/select_playlist_context_offline.xml b/app/src/main/res/menu/select_playlist_context_offline.xml index d63aec17..6745d850 100644 --- a/app/src/main/res/menu/select_playlist_context_offline.xml +++ b/app/src/main/res/menu/select_playlist_context_offline.xml @@ -1,13 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - <item - android:id="@+id/playlist_menu_play_now" - android:title="@string/common.play_now" +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/playlist_info" + android:title="@string/common.info" /> - - <item - android:id="@+id/playlist_menu_play_shuffled" - android:title="@string/common.play_shuffled" - /> </menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/select_podcast_episode_context.xml b/app/src/main/res/menu/select_podcast_episode_context.xml index bacccda3..31f2a051 100644 --- a/app/src/main/res/menu/select_podcast_episode_context.xml +++ b/app/src/main/res/menu/select_podcast_episode_context.xml @@ -7,7 +7,7 @@ android:title="@string/common.info" /> - <item + <item android:id="@+id/song_menu_play_now" android:title="@string/common.play_now" /> @@ -23,16 +23,16 @@ android:id="@+id/song_menu_play_last" android:title="@string/common.play_last"/> </group> - - <item + + <item android:id="@+id/song_menu_download" android:title="@string/common.download" /> - - <item + + <item android:id="@+id/song_menu_delete" android:title="@string/menu.delete_cache"/> - + <group android:id="@+id/server_1.9"> <item android:id="@+id/bookmark_menu_delete" diff --git a/app/src/main/res/menu/select_podcast_episode_context_offline.xml b/app/src/main/res/menu/select_podcast_episode_context_offline.xml index 587d01f7..301746f4 100644 --- a/app/src/main/res/menu/select_podcast_episode_context_offline.xml +++ b/app/src/main/res/menu/select_podcast_episode_context_offline.xml @@ -6,8 +6,8 @@ android:id="@+id/song_menu_info" android:title="@string/common.info" /> - - <item + + <item android:id="@+id/song_menu_play_now" android:title="@string/common.play_now" /> @@ -23,8 +23,8 @@ android:id="@+id/song_menu_play_last" android:title="@string/common.play_last"/> </group> - - <item + + <item android:id="@+id/song_menu_delete" android:title="@string/menu.delete_cache"/> </menu> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cd387b65..d4a239a6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,13 +42,7 @@ <string name="main.welcome_text">Willkommen zu DSub! Die App ist aktuell für den Subsonic-Demo-Server konfiguriert. Nachdem Sie Ihren eigenen Server aufgesetzt haben (verfügbar unter <b>subsonic.org</b>) könne Sie diesen unter <b>Einstellungen</b> konfigurieren.</string> <string name="main.about_title">Über DSub</string> - <string name="main.about_text">Autor: Scott Jackson - \nEmail: dsub.android@gmail.com - \nVersion: %1$s - \nLokal gespeicherte Titel: %2$s - \nGenutzter Speicher: %3$s von %4$s - \nVerfügbarer Speicher: %5$s von %6$s</string> - <string name="main.faq_title">FAQ</string> + <string name="main.faq_title">FAQ</string> <string name="main.faq_text"> <![CDATA[ <font color="red">Cache vs Permanenter Cache</font>: diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3c2593e2..e4d80211 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -41,13 +41,7 @@ <string name="main.welcome_title">Bienvenido!</string> <string name="main.welcome_text">Bienvenido a DSub! Ahora la aplicación está configurada para usar el servidor de demostración de Subsonic. Cuando configures tu servidor personal (disponible en <b>subsonic.org</b>), accede a <b>Preferencias</b> y cambia la configuración para conectarte.</string> <string name="main.about_title">Acerca de DSub</string> - <string name="main.about_text">Autor: Scott Jackson - \nEmail: dsub.android@gmail.com - \nVersión: %1$s - \nArchivos en caché: %2$s - \nEspacio usado: %3$s of %4$s - \nEspacio disponible: %5$s of %6$s</string> - <string name="main.select_server">Seleccionar servidor</string> + <string name="main.select_server">Seleccionar servidor</string> <string name="main.shuffle">Reproducción aleatoria</string> <string name="main.offline">Modo Offline</string> <string name="main.online">Modo Online</string> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cea3c77d..84e3dd75 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -41,12 +41,6 @@ <string name="main.welcome_title">Bienvenue !</string> <string name="main.welcome_text">Bienvenue dans DSub ! L\'application est actuellement configurée pour se connecter au serveur de démo Subsonic (<b>demo.subsonic.org</b>). Vous pouvez configurer votre propre serveur dans les paramètres. Choisir <b>Paramètres</b> et mettre à jour la configuration pour vous y connecter.</string> <string name="main.about_title">A propos de DSub</string> - <string name="main.about_text">Auteur : Scott Jackson - \nEmail : dsub.android@gmail.com - \nVersion : %1$s - \nFichiers en cache : %2$s - \nEspace utilisé : %3$s de %4$s - \nEspace dispo. : %5$s de %6$s</string> <string name="main.faq_title">FAQ</string> <string name="main.faq_text"> <![CDATA[ diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 816e605a..481a4c99 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -42,12 +42,6 @@ <string name="main.welcome_text">Üdvözli a DSub! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját (elérhető: <b>subsonic.org</b>), húzza balról jobbra az oldalsávot, lépjen be a <b>Beállítások</b> menüpontba és adja meg a kapcsolódási adatokat!</string> <string name="main.about_title">DSub információk</string> - <string name="main.about_text">Fejlesztő: Scott Jackson - \nEmail: dsub.android@gmail.com - \nVerzió: %1$s - \nGyorsítótárazott fájlok: %2$s - \nFelhasznált tároló: %3$s/%4$s - \nFelhasználható tároló: %5$s/%6$s</string> <string name="main.faq_title">GYIK</string> <string name="main.faq_text"> <![CDATA[ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0f904d95..15c79a69 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -32,12 +32,6 @@ <string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string> <string name="main.about_title">О программе DSub</string> - <string name="main.about_text">Автор: Scott Jackson - \nEmail: dsub.android@gmail.com - \nВерсия: %1$s - \nFiles Cached: %2$s - \nИспользовано места: %3$s из %4$s - \nДоступно места: %5$s из %6$s</string> <string name="main.select_server">Выбрать сервер</string> <string name="main.shuffle">Случайное воспроизведение</string> <string name="main.offline">Отключиться</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7c91da3..74d69e5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ <string name="common.confirm_message_cache">cache</string> <string name="common.empty">No data</string> <string name="common.warning">Warning</string> + <string name="common.close">Close</string> <string name="button_bar.home">Home</string> <string name="button_bar.browse">Library</string> @@ -42,12 +43,6 @@ <string name="main.welcome_text">Welcome to DSub! The app is currently configured to use the Subsonic demo server. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please go to <b>Settings</b> and change the configuration to connect to it.</string> <string name="main.about_title">About DSub</string> - <string name="main.about_text">Author: Scott Jackson - \nEmail: dsub.android@gmail.com - \nVersion: %1$s - \nFiles Cached: %2$s - \nUsed Space: %3$s of %4$s - \nAvailable Space: %5$s of %6$s</string> <string name="main.faq_title">FAQ</string> <string name="main.faq_text"> <![CDATA[ @@ -589,6 +584,43 @@ <string name="tasker.edit_server_offline">Toggle offline: </string> <string name="tasker.edit_do_nothing">Do Nothing</string> + <string name="details.title.song">Song Details</string> + <string name="details.title.album">Album Details</string> + <string name="details.title.podcast">Podcast Details</string> + <string name="details.title.playlist">Playlist Details</string> + <string name="details.podcast">Podcast</string> + <string name="details.status">Status</string> + <string name="details.artist">Artist</string> + <string name="details.album">Album</string> + <string name="details.track">Track</string> + <string name="details.genre">Genre</string> + <string name="details.year">Year</string> + <string name="details.server_format">Server Format</string> + <string name="details.server_bitrate">Server Bitrate</string> + <string name="details.cached_format">Cached Format</string> + <string name="details.cached_bitrate">Cached Bitrate</string> + <string name="details.size">Size</string> + <string name="details.length">Length</string> + <string name="details.bookmark_position">Bookmark Position</string> + <string name="details.rating">Rating</string> + <string name="details.description">Description</string> + <string name="details.owner">Owner</string> + <string name="details.comments">Comments</string> + <string name="details.song_count">Song Count</string> + <string name="details.public">Public</string> + <string name="details.created">Created</string> + <string name="details.title">Title</string> + <string name="details.url">URL</string> + <string name="details.error">Error Message</string> + <string name="details.author">Author</string> + <string name="details.email">Email</string> + <string name="details.version">Version</string> + <string name="details.files_cached">Files Cached</string> + <string name="details.files_permanent">Permanent Cached</string> + <string name="details.used_space">Used Space</string> + <string name="details.available_space">Available Space</string> + <string name="details.of">%1$s of %2$s</string> + <plurals name="select_album_n_songs"> <item quantity="zero">No songs</item> <item quantity="one">One song</item> |