Merge pull request #413 from tzugen/download
Migrate DownloadFile to Kotlin, add percentage display
This commit is contained in:
commit
4f7da06e26
|
@ -320,28 +320,6 @@
|
||||||
column="25"/>
|
column="25"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="MissingDefaultResource"
|
|
||||||
message="The layout "download" in layout-land has no declaration in the base `layout` folder; this can lead to crashes when the resource is queried in a configuration that does not match this qualifier"
|
|
||||||
errorLine1="<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android""
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout-land/download.xml"
|
|
||||||
line="2"
|
|
||||||
column="2"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="MissingDefaultResource"
|
|
||||||
message="The layout "download" in layout-port has no declaration in the base `layout` folder; this can lead to crashes when the resource is queried in a configuration that does not match this qualifier"
|
|
||||||
errorLine1="<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android""
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout-port/download.xml"
|
|
||||||
line="2"
|
|
||||||
column="2"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SdCardPath"
|
id="SdCardPath"
|
||||||
message="Do not hardcode "/sdcard/"; use `Environment.getExternalStorageDirectory().getPath()` instead"
|
message="Do not hardcode "/sdcard/"; use `Environment.getExternalStorageDirectory().getPath()` instead"
|
||||||
|
@ -2430,61 +2408,6 @@
|
||||||
column="4"/>
|
column="4"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1="<ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/media_buttons.xml"
|
|
||||||
line="12"
|
|
||||||
column="2"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/media_buttons.xml"
|
|
||||||
line="32"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/media_buttons.xml"
|
|
||||||
line="42"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/media_buttons.xml"
|
|
||||||
line="52"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/media_buttons.xml"
|
|
||||||
line="72"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="ContentDescription"
|
id="ContentDescription"
|
||||||
message="Missing `contentDescription` attribute on image"
|
message="Missing `contentDescription` attribute on image"
|
||||||
|
|
|
@ -145,7 +145,7 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.download, container, false);
|
return inflater.inflate(R.layout.current_playing, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -168,27 +168,27 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||||
swipeVelocity = swipeDistance;
|
swipeVelocity = swipeDistance;
|
||||||
gestureScanner = new GestureDetector(getContext(), this);
|
gestureScanner = new GestureDetector(getContext(), this);
|
||||||
|
|
||||||
playlistFlipper = view.findViewById(R.id.download_playlist_flipper);
|
playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper);
|
||||||
emptyTextView = view.findViewById(R.id.download_empty);
|
emptyTextView = view.findViewById(R.id.playlist_empty);
|
||||||
songTitleTextView = view.findViewById(R.id.download_song_title);
|
songTitleTextView = view.findViewById(R.id.current_playing_song);
|
||||||
albumTextView = view.findViewById(R.id.download_album);
|
albumTextView = view.findViewById(R.id.current_playing_album);
|
||||||
artistTextView = view.findViewById(R.id.download_artist);
|
artistTextView = view.findViewById(R.id.current_playing_artist);
|
||||||
albumArtImageView = view.findViewById(R.id.download_album_art_image);
|
albumArtImageView = view.findViewById(R.id.current_playing_album_art_image);
|
||||||
positionTextView = view.findViewById(R.id.download_position);
|
positionTextView = view.findViewById(R.id.current_playing_position);
|
||||||
downloadTrackTextView = view.findViewById(R.id.download_track);
|
downloadTrackTextView = view.findViewById(R.id.current_playing_track);
|
||||||
downloadTotalDurationTextView = view.findViewById(R.id.download_total_duration);
|
downloadTotalDurationTextView = view.findViewById(R.id.current_total_duration);
|
||||||
durationTextView = view.findViewById(R.id.download_duration);
|
durationTextView = view.findViewById(R.id.current_playing_duration);
|
||||||
progressBar = view.findViewById(R.id.download_progress_bar);
|
progressBar = view.findViewById(R.id.current_playing_progress_bar);
|
||||||
playlistView = view.findViewById(R.id.download_list);
|
playlistView = view.findViewById(R.id.playlist_view);
|
||||||
final AutoRepeatButton previousButton = view.findViewById(R.id.download_previous);
|
final AutoRepeatButton previousButton = view.findViewById(R.id.button_previous);
|
||||||
final AutoRepeatButton nextButton = view.findViewById(R.id.download_next);
|
final AutoRepeatButton nextButton = view.findViewById(R.id.button_next);
|
||||||
pauseButton = view.findViewById(R.id.download_pause);
|
pauseButton = view.findViewById(R.id.button_pause);
|
||||||
stopButton = view.findViewById(R.id.download_stop);
|
stopButton = view.findViewById(R.id.button_stop);
|
||||||
startButton = view.findViewById(R.id.download_start);
|
startButton = view.findViewById(R.id.button_start);
|
||||||
final View shuffleButton = view.findViewById(R.id.download_shuffle);
|
final View shuffleButton = view.findViewById(R.id.button_shuffle);
|
||||||
repeatButton = view.findViewById(R.id.download_repeat);
|
repeatButton = view.findViewById(R.id.button_repeat);
|
||||||
|
|
||||||
visualizerViewLayout = view.findViewById(R.id.download_visualizer_view_layout);
|
visualizerViewLayout = view.findViewById(R.id.current_playing_visualizer_layout);
|
||||||
|
|
||||||
LinearLayout ratingLinearLayout = view.findViewById(R.id.song_rating);
|
LinearLayout ratingLinearLayout = view.findViewById(R.id.song_rating);
|
||||||
fiveStar1ImageView = view.findViewById(R.id.song_five_star_1);
|
fiveStar1ImageView = view.findViewById(R.id.song_five_star_1);
|
||||||
|
@ -1375,13 +1375,10 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||||
switch (playerState)
|
switch (playerState)
|
||||||
{
|
{
|
||||||
case DOWNLOADING:
|
case DOWNLOADING:
|
||||||
final long bytes = currentPlaying != null ? currentPlaying.getPartialFile().length() : 0;
|
String downloadStatus = getResources().getString(R.string.download_playerstate_downloading, Util.formatPercentage(currentPlaying.getProgress().getValue()));
|
||||||
String downloadStatus = getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, getContext()));
|
|
||||||
Timber.d("Player set title");
|
|
||||||
FragmentTitle.Companion.setTitle(PlayerFragment.this, downloadStatus);
|
FragmentTitle.Companion.setTitle(PlayerFragment.this, downloadStatus);
|
||||||
break;
|
break;
|
||||||
case PREPARING:
|
case PREPARING:
|
||||||
Timber.d("Player set title");
|
|
||||||
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_buffering);
|
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_buffering);
|
||||||
break;
|
break;
|
||||||
case STARTED:
|
case STARTED:
|
||||||
|
@ -1389,17 +1386,14 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||||
|
|
||||||
if (mediaPlayerController != null && mediaPlayerController.isShufflePlayEnabled())
|
if (mediaPlayerController != null && mediaPlayerController.isShufflePlayEnabled())
|
||||||
{
|
{
|
||||||
Timber.d("Player set title");
|
|
||||||
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_playing_shuffle);
|
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.download_playerstate_playing_shuffle);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Timber.d("Player set title");
|
|
||||||
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname);
|
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Timber.d("Player set title");
|
|
||||||
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname);
|
FragmentTitle.Companion.setTitle(PlayerFragment.this, R.string.common_appname);
|
||||||
break;
|
break;
|
||||||
case IDLE:
|
case IDLE:
|
||||||
|
|
|
@ -321,9 +321,9 @@ public class CachedMusicService implements MusicService
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<InputStream, Boolean> getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception
|
public Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception
|
||||||
{
|
{
|
||||||
return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
|
return musicService.getDownloadInputStream(song, offset, maxBitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,521 +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.service;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.util.CacheCleaner;
|
|
||||||
import org.moire.ultrasonic.util.CancellableTask;
|
|
||||||
import org.moire.ultrasonic.util.FileUtil;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
import kotlin.Pair;
|
|
||||||
|
|
||||||
import static android.content.Context.POWER_SERVICE;
|
|
||||||
import static android.os.PowerManager.ON_AFTER_RELEASE;
|
|
||||||
import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK;
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class DownloadFile
|
|
||||||
{
|
|
||||||
private final Context context;
|
|
||||||
private final MusicDirectory.Entry song;
|
|
||||||
private final File partialFile;
|
|
||||||
private final File completeFile;
|
|
||||||
private final File saveFile;
|
|
||||||
|
|
||||||
private final MediaStoreService mediaStoreService;
|
|
||||||
private CancellableTask downloadTask;
|
|
||||||
private final boolean save;
|
|
||||||
private boolean failed;
|
|
||||||
private int bitRate;
|
|
||||||
private volatile boolean isPlaying;
|
|
||||||
private volatile boolean saveWhenDone;
|
|
||||||
private volatile boolean completeWhenDone;
|
|
||||||
|
|
||||||
private final Lazy<Downloader> downloader = inject(Downloader.class);
|
|
||||||
|
|
||||||
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.context = context;
|
|
||||||
this.song = song;
|
|
||||||
this.save = save;
|
|
||||||
|
|
||||||
saveFile = FileUtil.getSongFile(context, song);
|
|
||||||
bitRate = Util.getMaxBitRate(context);
|
|
||||||
partialFile = new File(saveFile.getParent(), String.format("%s.partial.%s", FileUtil.getBaseName(saveFile.getName()), FileUtil.getExtension(saveFile.getName())));
|
|
||||||
completeFile = new File(saveFile.getParent(), String.format("%s.complete.%s", FileUtil.getBaseName(saveFile.getName()), FileUtil.getExtension(saveFile.getName())));
|
|
||||||
mediaStoreService = new MediaStoreService(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MusicDirectory.Entry getSong()
|
|
||||||
{
|
|
||||||
return song;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the effective bit rate.
|
|
||||||
*/
|
|
||||||
public int getBitRate()
|
|
||||||
{
|
|
||||||
if (!partialFile.exists())
|
|
||||||
{
|
|
||||||
bitRate = Util.getMaxBitRate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitRate > 0)
|
|
||||||
{
|
|
||||||
return bitRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return song.getBitRate() == null ? 160 : song.getBitRate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void download()
|
|
||||||
{
|
|
||||||
FileUtil.createDirectoryForParent(saveFile);
|
|
||||||
failed = false;
|
|
||||||
|
|
||||||
if (!partialFile.exists())
|
|
||||||
{
|
|
||||||
bitRate = Util.getMaxBitRate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadTask = new DownloadTask();
|
|
||||||
downloadTask.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void cancelDownload()
|
|
||||||
{
|
|
||||||
if (downloadTask != null)
|
|
||||||
{
|
|
||||||
downloadTask.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getCompleteFile()
|
|
||||||
{
|
|
||||||
if (saveFile.exists())
|
|
||||||
{
|
|
||||||
return saveFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completeFile.exists())
|
|
||||||
{
|
|
||||||
return completeFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
return saveFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getCompleteOrPartialFile() {
|
|
||||||
if (isCompleteFileAvailable()) {
|
|
||||||
return getCompleteFile();
|
|
||||||
} else {
|
|
||||||
return getPartialFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getPartialFile()
|
|
||||||
{
|
|
||||||
return partialFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSaved()
|
|
||||||
{
|
|
||||||
return saveFile.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isCompleteFileAvailable()
|
|
||||||
{
|
|
||||||
return saveFile.exists() || completeFile.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isWorkDone()
|
|
||||||
{
|
|
||||||
return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isDownloading()
|
|
||||||
{
|
|
||||||
return downloadTask != null && downloadTask.isRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isDownloadCancelled()
|
|
||||||
{
|
|
||||||
return downloadTask != null && downloadTask.isCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldSave()
|
|
||||||
{
|
|
||||||
return save;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFailed()
|
|
||||||
{
|
|
||||||
return failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete()
|
|
||||||
{
|
|
||||||
cancelDownload();
|
|
||||||
Util.delete(partialFile);
|
|
||||||
Util.delete(completeFile);
|
|
||||||
Util.delete(saveFile);
|
|
||||||
mediaStoreService.deleteFromMediaStore(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unpin()
|
|
||||||
{
|
|
||||||
if (saveFile.exists())
|
|
||||||
{
|
|
||||||
if (!saveFile.renameTo(completeFile)){
|
|
||||||
Timber.w("Renaming file failed. Original file: %s; Rename to: %s", saveFile.getName(), completeFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean cleanup()
|
|
||||||
{
|
|
||||||
boolean ok = true;
|
|
||||||
|
|
||||||
if (completeFile.exists() || saveFile.exists())
|
|
||||||
{
|
|
||||||
ok = Util.delete(partialFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveFile.exists())
|
|
||||||
{
|
|
||||||
ok &= Util.delete(completeFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In support of LRU caching.
|
|
||||||
public void updateModificationDate()
|
|
||||||
{
|
|
||||||
updateModificationDate(saveFile);
|
|
||||||
updateModificationDate(partialFile);
|
|
||||||
updateModificationDate(completeFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateModificationDate(File file)
|
|
||||||
{
|
|
||||||
if (file.exists())
|
|
||||||
{
|
|
||||||
boolean ok = file.setLastModified(System.currentTimeMillis());
|
|
||||||
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
Timber.i("Failed to set last-modified date on %s, trying alternate method", file);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Try alternate method to update last modified date to current time
|
|
||||||
// Found at https://code.google.com/p/android/issues/detail?id=18624
|
|
||||||
RandomAccessFile raf = new RandomAccessFile(file, "rw");
|
|
||||||
long length = raf.length();
|
|
||||||
raf.setLength(length + 1);
|
|
||||||
raf.setLength(length);
|
|
||||||
raf.close();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Timber.w("Failed to set last-modified date on %s", file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPlaying(boolean isPlaying)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (saveWhenDone && !isPlaying)
|
|
||||||
{
|
|
||||||
Util.renameFile(completeFile, saveFile);
|
|
||||||
saveWhenDone = false;
|
|
||||||
}
|
|
||||||
else if (completeWhenDone && !isPlaying)
|
|
||||||
{
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
Util.renameFile(partialFile, saveFile);
|
|
||||||
mediaStoreService.saveInMediaStore(DownloadFile.this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Util.renameFile(partialFile, completeFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
completeWhenDone = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
Timber.w("Failed to rename file %s to %s", completeFile, saveFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isPlaying = isPlaying;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return String.format("DownloadFile (%s)", song);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadTask extends CancellableTask
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
InputStream in = null;
|
|
||||||
FileOutputStream out = null;
|
|
||||||
PowerManager.WakeLock wakeLock = null;
|
|
||||||
WifiManager.WifiLock wifiLock = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Util.isScreenLitOnDownload(context))
|
|
||||||
{
|
|
||||||
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
|
|
||||||
wakeLock = pm.newWakeLock(SCREEN_DIM_WAKE_LOCK | ON_AFTER_RELEASE, toString());
|
|
||||||
wakeLock.acquire(10*60*1000L /*10 minutes*/);
|
|
||||||
Timber.i("Acquired wake lock %s", wakeLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
wifiLock = Util.createWifiLock(context, toString());
|
|
||||||
wifiLock.acquire();
|
|
||||||
|
|
||||||
if (saveFile.exists())
|
|
||||||
{
|
|
||||||
Timber.i("%s already exists. Skipping.", saveFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (completeFile.exists())
|
|
||||||
{
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
if (isPlaying)
|
|
||||||
{
|
|
||||||
saveWhenDone = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Util.renameFile(completeFile, saveFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Timber.i("%s already exists. Skipping.", completeFile);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MusicService musicService = MusicServiceFactory.getMusicService(context);
|
|
||||||
|
|
||||||
// Some devices seem to throw error on partial file which doesn't exist
|
|
||||||
boolean compare;
|
|
||||||
|
|
||||||
Integer duration = song.getDuration();
|
|
||||||
long fileLength = 0;
|
|
||||||
|
|
||||||
if (!partialFile.exists())
|
|
||||||
{
|
|
||||||
fileLength = partialFile.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
compare = (bitRate == 0) || (duration == null || duration == 0) || (fileLength == 0);
|
|
||||||
//(bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
compare = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compare)
|
|
||||||
{
|
|
||||||
// Attempt partial HTTP GET, appending to the file if it exists.
|
|
||||||
Pair<InputStream, Boolean> response = musicService
|
|
||||||
.getDownloadInputStream(context, song, partialFile.length(), bitRate,
|
|
||||||
DownloadTask.this);
|
|
||||||
|
|
||||||
if (response.getSecond())
|
|
||||||
{
|
|
||||||
Timber.i("Executed partial HTTP GET, skipping %d bytes", partialFile.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
out = new FileOutputStream(partialFile, response.getSecond());
|
|
||||||
long n = copy(response.getFirst(), out);
|
|
||||||
Timber.i("Downloaded %d bytes to %s", n, partialFile);
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
if (isCancelled())
|
|
||||||
{
|
|
||||||
throw new Exception(String.format("Download of '%s' was cancelled", song));
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadAndSaveCoverArt(musicService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying)
|
|
||||||
{
|
|
||||||
completeWhenDone = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
Util.renameFile(partialFile, saveFile);
|
|
||||||
mediaStoreService.saveInMediaStore(DownloadFile.this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Util.renameFile(partialFile, completeFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
Util.close(out);
|
|
||||||
Util.delete(completeFile);
|
|
||||||
Util.delete(saveFile);
|
|
||||||
|
|
||||||
if (!isCancelled())
|
|
||||||
{
|
|
||||||
failed = true;
|
|
||||||
Timber.w(x, "Failed to download '%s'.", song);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Util.close(in);
|
|
||||||
Util.close(out);
|
|
||||||
if (wakeLock != null)
|
|
||||||
{
|
|
||||||
wakeLock.release();
|
|
||||||
Timber.i("Released wake lock %s", wakeLock);
|
|
||||||
}
|
|
||||||
if (wifiLock != null)
|
|
||||||
{
|
|
||||||
wifiLock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
new CacheCleaner(context).cleanSpace();
|
|
||||||
|
|
||||||
downloader.getValue().checkDownloads();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return String.format("DownloadTask (%s)", song);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadAndSaveCoverArt(MusicService musicService)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!TextUtils.isEmpty(song.getCoverArt())) {
|
|
||||||
int size = Util.getMinDisplayMetric(context);
|
|
||||||
musicService.getCoverArt(context, song, size, true, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
Timber.e(x, "Failed to get cover art.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private long copy(final InputStream in, OutputStream out) throws IOException
|
|
||||||
{
|
|
||||||
// Start a thread that will close the input stream if the task is
|
|
||||||
// cancelled, thus causing the copy() method to return.
|
|
||||||
new Thread()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
Util.sleepQuietly(3000L);
|
|
||||||
|
|
||||||
if (isCancelled())
|
|
||||||
{
|
|
||||||
Util.close(in);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRunning())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
byte[] buffer = new byte[1024 * 16];
|
|
||||||
long count = 0;
|
|
||||||
int n;
|
|
||||||
long lastLog = System.currentTimeMillis();
|
|
||||||
|
|
||||||
while (!isCancelled() && (n = in.read(buffer)) != -1)
|
|
||||||
{
|
|
||||||
out.write(buffer, 0, n);
|
|
||||||
count += n;
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (now - lastLog > 3000L)
|
|
||||||
{ // Only every so often.
|
|
||||||
Timber.i("Downloaded %s of %s", Util.formatBytes(count), song);
|
|
||||||
lastLog = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -108,7 +108,7 @@ public interface MusicService
|
||||||
* Return response {@link InputStream} and a {@link Boolean} that indicates if this response is
|
* Return response {@link InputStream} and a {@link Boolean} that indicates if this response is
|
||||||
* partial.
|
* partial.
|
||||||
*/
|
*/
|
||||||
Pair<InputStream, Boolean> getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
|
Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception;
|
||||||
|
|
||||||
// TODO: Refactor and remove this call (see RestMusicService implementation)
|
// TODO: Refactor and remove this call (see RestMusicService implementation)
|
||||||
String getVideoUrl(Context context, String id, boolean useFlash) throws Exception;
|
String getVideoUrl(Context context, String id, boolean useFlash) throws Exception;
|
||||||
|
|
|
@ -893,7 +893,7 @@ public class OfflineMusicService implements MusicService
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<InputStream, Boolean> getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) {
|
public Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) {
|
||||||
Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available");
|
Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,7 +229,7 @@ public class CacheCleaner
|
||||||
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
|
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
|
||||||
{
|
{
|
||||||
filesToNotDelete.add(downloadFile.getPartialFile());
|
filesToNotDelete.add(downloadFile.getPartialFile());
|
||||||
filesToNotDelete.add(downloadFile.getCompleteFile());
|
filesToNotDelete.add(downloadFile.getCompleteOrSaveFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
filesToNotDelete.add(FileUtil.getMusicDirectory(context));
|
filesToNotDelete.add(FileUtil.getMusicDirectory(context));
|
||||||
|
|
|
@ -549,6 +549,28 @@ public class FileUtil
|
||||||
return index == -1 ? name : name.substring(0, index);
|
return index == -1 ? name : name.substring(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file name of a .partial file of the given file.
|
||||||
|
*
|
||||||
|
* @param name The filename in question.
|
||||||
|
* @return The .partial file name
|
||||||
|
*/
|
||||||
|
public static String getPartialFile(String name)
|
||||||
|
{
|
||||||
|
return String.format("%s.partial.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file name of a .complete file of the given file.
|
||||||
|
*
|
||||||
|
* @param name The filename in question.
|
||||||
|
* @return The .complete file name
|
||||||
|
*/
|
||||||
|
public static String getCompleteFile(String name)
|
||||||
|
{
|
||||||
|
return String.format("%s.complete.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
|
||||||
|
}
|
||||||
|
|
||||||
public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName)
|
public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName)
|
||||||
{
|
{
|
||||||
File file = new File(context.getCacheDir(), fileName);
|
File file = new File(context.getCacheDir(), fileName);
|
||||||
|
|
|
@ -194,7 +194,7 @@ public class StreamProxy implements Runnable
|
||||||
while (isRunning && !client.isClosed())
|
while (isRunning && !client.isClosed())
|
||||||
{
|
{
|
||||||
// See if there's more to send
|
// See if there's more to send
|
||||||
File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
|
File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||||
int cbSentThisBatch = 0;
|
int cbSentThisBatch = 0;
|
||||||
|
|
||||||
if (file.exists())
|
if (file.exists())
|
||||||
|
|
|
@ -343,6 +343,23 @@ public class Util
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an Int to a percentage string
|
||||||
|
* For instance:
|
||||||
|
* <ul>
|
||||||
|
* <li><code>format(99)</code> returns <em>"99 %"</em>.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param percent The percent as a range from 0 - 100
|
||||||
|
* @return The formatted string.
|
||||||
|
*/
|
||||||
|
public static synchronized String formatPercentage(int percent)
|
||||||
|
{
|
||||||
|
return Math.min(Math.max(percent,0),100) + " %";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a byte-count to a formatted string suitable for display to the user.
|
* Converts a byte-count to a formatted string suitable for display to the user.
|
||||||
* For instance:
|
* For instance:
|
||||||
|
|
|
@ -0,0 +1,377 @@
|
||||||
|
/*
|
||||||
|
* DownloadFile.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.wifi.WifiManager.WifiLock
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.os.PowerManager.WakeLock
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
|
import org.moire.ultrasonic.util.CancellableTask
|
||||||
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a singe Song or Video that can be downloaded.
|
||||||
|
*
|
||||||
|
* @author Sindre Mehus
|
||||||
|
* @version $Id$
|
||||||
|
*/
|
||||||
|
class DownloadFile(
|
||||||
|
private val context: Context,
|
||||||
|
val song: MusicDirectory.Entry,
|
||||||
|
private val save: Boolean
|
||||||
|
) {
|
||||||
|
val partialFile: File
|
||||||
|
val completeFile: File
|
||||||
|
private val saveFile: File = FileUtil.getSongFile(context, song)
|
||||||
|
private val mediaStoreService: MediaStoreService
|
||||||
|
private var downloadTask: CancellableTask? = null
|
||||||
|
var isFailed = false
|
||||||
|
|
||||||
|
private val desiredBitRate: Int = Util.getMaxBitRate(context)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isPlaying = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var saveWhenDone = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var completeWhenDone = false
|
||||||
|
|
||||||
|
private val downloader = inject(Downloader::class.java)
|
||||||
|
|
||||||
|
val progress: MutableLiveData<Int> = MutableLiveData(0)
|
||||||
|
|
||||||
|
init {
|
||||||
|
partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name))
|
||||||
|
completeFile = File(saveFile.parent, FileUtil.getCompleteFile(saveFile.name))
|
||||||
|
mediaStoreService = MediaStoreService(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the effective bit rate.
|
||||||
|
*/
|
||||||
|
fun getBitRate(): Int {
|
||||||
|
return if (song.bitRate == null) desiredBitRate else song.bitRate!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun download() {
|
||||||
|
FileUtil.createDirectoryForParent(saveFile)
|
||||||
|
isFailed = false
|
||||||
|
downloadTask = DownloadTask()
|
||||||
|
downloadTask!!.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun cancelDownload() {
|
||||||
|
if (downloadTask != null) {
|
||||||
|
downloadTask!!.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val completeOrSaveFile: File
|
||||||
|
get() = if (saveFile.exists()) {
|
||||||
|
saveFile
|
||||||
|
} else {
|
||||||
|
completeFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val completeOrPartialFile: File
|
||||||
|
get() = if (isCompleteFileAvailable) {
|
||||||
|
completeOrSaveFile
|
||||||
|
} else {
|
||||||
|
partialFile
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSaved: Boolean
|
||||||
|
get() = saveFile.exists()
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val isCompleteFileAvailable: Boolean
|
||||||
|
get() = saveFile.exists() || completeFile.exists()
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val isWorkDone: Boolean
|
||||||
|
get() = saveFile.exists() || completeFile.exists() && !save ||
|
||||||
|
saveWhenDone || completeWhenDone
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val isDownloading: Boolean
|
||||||
|
get() = downloadTask != null && downloadTask!!.isRunning
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val isDownloadCancelled: Boolean
|
||||||
|
get() = downloadTask != null && downloadTask!!.isCancelled
|
||||||
|
|
||||||
|
fun shouldSave(): Boolean {
|
||||||
|
return save
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete() {
|
||||||
|
cancelDownload()
|
||||||
|
Util.delete(partialFile)
|
||||||
|
Util.delete(completeFile)
|
||||||
|
Util.delete(saveFile)
|
||||||
|
mediaStoreService.deleteFromMediaStore(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpin() {
|
||||||
|
if (saveFile.exists()) {
|
||||||
|
if (!saveFile.renameTo(completeFile)) {
|
||||||
|
Timber.w(
|
||||||
|
"Renaming file failed. Original file: %s; Rename to: %s",
|
||||||
|
saveFile.name, completeFile.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup(): Boolean {
|
||||||
|
var ok = true
|
||||||
|
if (completeFile.exists() || saveFile.exists()) {
|
||||||
|
ok = Util.delete(partialFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveFile.exists()) {
|
||||||
|
ok = ok and Util.delete(completeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// In support of LRU caching.
|
||||||
|
fun updateModificationDate() {
|
||||||
|
updateModificationDate(saveFile)
|
||||||
|
updateModificationDate(partialFile)
|
||||||
|
updateModificationDate(completeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPlaying(isPlaying: Boolean) {
|
||||||
|
if (!isPlaying) doPendingRename()
|
||||||
|
this.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a pending rename after the song has stopped playing
|
||||||
|
private fun doPendingRename() {
|
||||||
|
try {
|
||||||
|
if (saveWhenDone) {
|
||||||
|
Util.renameFile(completeFile, saveFile)
|
||||||
|
saveWhenDone = false
|
||||||
|
} else if (completeWhenDone) {
|
||||||
|
if (save) {
|
||||||
|
Util.renameFile(partialFile, saveFile)
|
||||||
|
mediaStoreService.saveInMediaStore(this@DownloadFile)
|
||||||
|
} else {
|
||||||
|
Util.renameFile(partialFile, completeFile)
|
||||||
|
}
|
||||||
|
completeWhenDone = false
|
||||||
|
}
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Timber.w("Failed to rename file %s to %s", completeFile, saveFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return String.format("DownloadFile (%s)", song)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DownloadTask : CancellableTask() {
|
||||||
|
override fun execute() {
|
||||||
|
var inputStream: InputStream? = null
|
||||||
|
var outputStream: FileOutputStream? = null
|
||||||
|
var wakeLock: WakeLock? = null
|
||||||
|
var wifiLock: WifiLock? = null
|
||||||
|
try {
|
||||||
|
wakeLock = acquireWakeLock(wakeLock)
|
||||||
|
wifiLock = Util.createWifiLock(context, toString())
|
||||||
|
wifiLock.acquire()
|
||||||
|
|
||||||
|
if (saveFile.exists()) {
|
||||||
|
Timber.i("%s already exists. Skipping.", saveFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completeFile.exists()) {
|
||||||
|
if (save) {
|
||||||
|
if (isPlaying) {
|
||||||
|
saveWhenDone = true
|
||||||
|
} else {
|
||||||
|
Util.renameFile(completeFile, saveFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.i("%s already exists. Skipping.", completeFile)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val musicService = getMusicService(context)
|
||||||
|
|
||||||
|
// Some devices seem to throw error on partial file which doesn't exist
|
||||||
|
val needsDownloading: Boolean
|
||||||
|
val duration = song.duration
|
||||||
|
var fileLength: Long = 0
|
||||||
|
|
||||||
|
if (!partialFile.exists()) {
|
||||||
|
fileLength = partialFile.length()
|
||||||
|
}
|
||||||
|
|
||||||
|
needsDownloading = (
|
||||||
|
desiredBitRate == 0 || duration == null ||
|
||||||
|
duration == 0 || fileLength == 0L
|
||||||
|
)
|
||||||
|
|
||||||
|
if (needsDownloading) {
|
||||||
|
// Attempt partial HTTP GET, appending to the file if it exists.
|
||||||
|
val (inStream, partial) = musicService
|
||||||
|
.getDownloadInputStream(song, partialFile.length(), desiredBitRate)
|
||||||
|
|
||||||
|
inputStream = inStream
|
||||||
|
|
||||||
|
if (partial) {
|
||||||
|
Timber.i(
|
||||||
|
"Executed partial HTTP GET, skipping %d bytes",
|
||||||
|
partialFile.length()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream = FileOutputStream(partialFile, partial)
|
||||||
|
|
||||||
|
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
|
||||||
|
setProgress(totalBytesCopied)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.i("Downloaded %d bytes to %s", len, partialFile)
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
if (isCancelled) {
|
||||||
|
throw Exception(String.format("Download of '%s' was cancelled", song))
|
||||||
|
}
|
||||||
|
downloadAndSaveCoverArt(musicService)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
completeWhenDone = true
|
||||||
|
} else {
|
||||||
|
if (save) {
|
||||||
|
Util.renameFile(partialFile, saveFile)
|
||||||
|
mediaStoreService.saveInMediaStore(this@DownloadFile)
|
||||||
|
} else {
|
||||||
|
Util.renameFile(partialFile, completeFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (x: Exception) {
|
||||||
|
Util.close(outputStream)
|
||||||
|
Util.delete(completeFile)
|
||||||
|
Util.delete(saveFile)
|
||||||
|
if (!isCancelled) {
|
||||||
|
isFailed = true
|
||||||
|
Timber.w(x, "Failed to download '%s'.", song)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Util.close(inputStream)
|
||||||
|
Util.close(outputStream)
|
||||||
|
if (wakeLock != null) {
|
||||||
|
wakeLock.release()
|
||||||
|
Timber.i("Released wake lock %s", wakeLock)
|
||||||
|
}
|
||||||
|
wifiLock?.release()
|
||||||
|
CacheCleaner(context).cleanSpace()
|
||||||
|
downloader.value.checkDownloads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acquireWakeLock(wakeLock: WakeLock?): WakeLock? {
|
||||||
|
var wakeLock1 = wakeLock
|
||||||
|
if (Util.isScreenLitOnDownload(context)) {
|
||||||
|
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
val flags = PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ON_AFTER_RELEASE
|
||||||
|
wakeLock1 = pm.newWakeLock(flags, toString())
|
||||||
|
wakeLock1.acquire(10 * 60 * 1000L /*10 minutes*/)
|
||||||
|
Timber.i("Acquired wake lock %s", wakeLock1)
|
||||||
|
}
|
||||||
|
return wakeLock1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return String.format("DownloadTask (%s)", song)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadAndSaveCoverArt(musicService: MusicService) {
|
||||||
|
try {
|
||||||
|
if (!TextUtils.isEmpty(song.coverArt)) {
|
||||||
|
val size = Util.getMinDisplayMetric(context)
|
||||||
|
musicService.getCoverArt(context, song, size, true, true)
|
||||||
|
}
|
||||||
|
} catch (x: Exception) {
|
||||||
|
Timber.e(x, "Failed to get cover art.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long) -> Any): Long {
|
||||||
|
var bytesCopied: Long = 0
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
var bytes = read(buffer)
|
||||||
|
while (!isCancelled && bytes >= 0) {
|
||||||
|
out.write(buffer, 0, bytes)
|
||||||
|
bytesCopied += bytes
|
||||||
|
onCopy(bytesCopied)
|
||||||
|
bytes = read(buffer)
|
||||||
|
}
|
||||||
|
return bytesCopied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setProgress(totalBytesCopied: Long) {
|
||||||
|
if (song.size != null) {
|
||||||
|
progress.postValue((totalBytesCopied * 100 / song.size!!).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateModificationDate(file: File) {
|
||||||
|
if (file.exists()) {
|
||||||
|
val ok = file.setLastModified(System.currentTimeMillis())
|
||||||
|
if (!ok) {
|
||||||
|
Timber.i(
|
||||||
|
"Failed to set last-modified date on %s, trying alternate method",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
// Try alternate method to update last modified date to current time
|
||||||
|
// Found at https://code.google.com/p/android/issues/detail?id=18624
|
||||||
|
// According to the bug, this was fixed in Android 8.0 (API 26)
|
||||||
|
val raf = RandomAccessFile(file, "rw")
|
||||||
|
val length = raf.length()
|
||||||
|
raf.setLength(length + 1)
|
||||||
|
raf.setLength(length)
|
||||||
|
raf.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w("Failed to set last-modified date on %s", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* LocalMediaPlayer.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
@ -755,7 +762,7 @@ class LocalMediaPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
|
// Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
|
||||||
val bitRate = downloadFile.bitRate
|
val bitRate = downloadFile.getBitRate()
|
||||||
val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength)
|
val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength)
|
||||||
|
|
||||||
// Find out how large the file should grow before resuming playback.
|
// Find out how large the file should grow before resuming playback.
|
||||||
|
|
|
@ -59,7 +59,6 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList
|
||||||
import org.moire.ultrasonic.domain.toDomainEntity
|
import org.moire.ultrasonic.domain.toDomainEntity
|
||||||
import org.moire.ultrasonic.domain.toDomainEntityList
|
import org.moire.ultrasonic.domain.toDomainEntityList
|
||||||
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
||||||
import org.moire.ultrasonic.util.CancellableTask
|
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -611,11 +610,9 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getDownloadInputStream(
|
override fun getDownloadInputStream(
|
||||||
context: Context,
|
|
||||||
song: MusicDirectory.Entry,
|
song: MusicDirectory.Entry,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
maxBitrate: Int,
|
maxBitrate: Int
|
||||||
task: CancellableTask
|
|
||||||
): Pair<InputStream, Boolean> {
|
): Pair<InputStream, Boolean> {
|
||||||
val songOffset = if (offset < 0) 0 else offset
|
val songOffset = if (offset < 0) 0 else offset
|
||||||
|
|
||||||
|
|
|
@ -225,59 +225,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
|
|
||||||
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
|
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
|
||||||
|
|
||||||
val partialFile = downloadFile!!.partialFile
|
updateDownloadStatus(downloadFile!!)
|
||||||
|
|
||||||
if (downloadFile!!.isWorkDone) {
|
|
||||||
val newLeftImageType =
|
|
||||||
if (downloadFile!!.isSaved) ImageType.Pin else ImageType.Downloaded
|
|
||||||
|
|
||||||
if (leftImageType != newLeftImageType) {
|
|
||||||
leftImage = if (downloadFile!!.isSaved) pinImage else downloadedImage
|
|
||||||
leftImageType = newLeftImageType
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
leftImageType = ImageType.None
|
|
||||||
leftImage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
val rightImageType: ImageType
|
|
||||||
val rightImage: Drawable?
|
|
||||||
|
|
||||||
if (
|
|
||||||
downloadFile!!.isDownloading &&
|
|
||||||
!downloadFile!!.isDownloadCancelled &&
|
|
||||||
partialFile.exists()
|
|
||||||
) {
|
|
||||||
viewHolder?.status?.text = Util.formatLocalizedBytes(
|
|
||||||
partialFile.length(), this.context
|
|
||||||
)
|
|
||||||
|
|
||||||
rightImageType = ImageType.Downloading
|
|
||||||
rightImage = downloadingImage
|
|
||||||
} else {
|
|
||||||
rightImageType = ImageType.None
|
|
||||||
rightImage = null
|
|
||||||
|
|
||||||
val statusText = viewHolder?.status?.text
|
|
||||||
if (!statusText.isNullOrEmpty()) viewHolder?.status?.text = null
|
|
||||||
}
|
|
||||||
if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
|
|
||||||
previousLeftImageType = leftImageType
|
|
||||||
previousRightImageType = rightImageType
|
|
||||||
|
|
||||||
if (viewHolder?.status != null) {
|
|
||||||
viewHolder?.status?.setCompoundDrawablesWithIntrinsicBounds(
|
|
||||||
leftImage, null, rightImage, null
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rightImage === downloadingImage) {
|
|
||||||
val frameAnimation = rightImage as AnimationDrawable?
|
|
||||||
|
|
||||||
frameAnimation!!.setVisible(true, true)
|
|
||||||
frameAnimation.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry?.starred != true) {
|
if (entry?.starred != true) {
|
||||||
if (viewHolder?.star?.drawable !== starHollowDrawable) {
|
if (viewHolder?.star?.drawable !== starHollowDrawable) {
|
||||||
|
@ -325,6 +273,56 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateDownloadStatus(downloadFile: DownloadFile) {
|
||||||
|
|
||||||
|
if (downloadFile.isWorkDone) {
|
||||||
|
val newLeftImageType =
|
||||||
|
if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded
|
||||||
|
|
||||||
|
if (leftImageType != newLeftImageType) {
|
||||||
|
leftImage = if (downloadFile.isSaved) pinImage else downloadedImage
|
||||||
|
leftImageType = newLeftImageType
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
leftImageType = ImageType.None
|
||||||
|
leftImage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val rightImageType: ImageType
|
||||||
|
val rightImage: Drawable?
|
||||||
|
|
||||||
|
if (downloadFile.isDownloading && !downloadFile.isDownloadCancelled) {
|
||||||
|
viewHolder?.status?.text = Util.formatPercentage(downloadFile.progress.value!!)
|
||||||
|
|
||||||
|
rightImageType = ImageType.Downloading
|
||||||
|
rightImage = downloadingImage
|
||||||
|
} else {
|
||||||
|
rightImageType = ImageType.None
|
||||||
|
rightImage = null
|
||||||
|
|
||||||
|
val statusText = viewHolder?.status?.text
|
||||||
|
if (!statusText.isNullOrEmpty()) viewHolder?.status?.text = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
|
||||||
|
previousLeftImageType = leftImageType
|
||||||
|
previousRightImageType = rightImageType
|
||||||
|
|
||||||
|
if (viewHolder?.status != null) {
|
||||||
|
viewHolder?.status?.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
leftImage, null, rightImage, null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rightImage === downloadingImage) {
|
||||||
|
val frameAnimation = rightImage as AnimationDrawable?
|
||||||
|
|
||||||
|
frameAnimation!!.setVisible(true, true)
|
||||||
|
frameAnimation.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun setChecked(b: Boolean) {
|
override fun setChecked(b: Boolean) {
|
||||||
viewHolder?.check?.isChecked = b
|
viewHolder?.check?.isChecked = b
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
a:orientation="horizontal">
|
a:orientation="horizontal">
|
||||||
|
|
||||||
<org.moire.ultrasonic.util.MyViewFlipper
|
<org.moire.ultrasonic.util.MyViewFlipper
|
||||||
a:id="@+id/download_playlist_flipper"
|
a:id="@+id/current_playing_playlist_flipper"
|
||||||
a:layout_width="0dp"
|
a:layout_width="0dp"
|
||||||
a:layout_height="fill_parent"
|
a:layout_height="fill_parent"
|
||||||
a:layout_weight="1">
|
a:layout_weight="1">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
a:id="@+id/download_album_art_layout"
|
a:id="@+id/current_playing_album_art_layout"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="fill_parent"
|
a:layout_height="fill_parent"
|
||||||
a:layout_weight="1"
|
a:layout_weight="1"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
a:orientation="horizontal">
|
a:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_album_art_image"
|
a:id="@+id/current_playing_album_art_image"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:alpha="0.2"
|
a:alpha="0.2"
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
a:id="@+id/download_visualizer_view_layout"
|
a:id="@+id/current_playing_visualizer_layout"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="60dip"
|
a:layout_height="60dip"
|
||||||
a:layout_gravity="bottom|center_horizontal"
|
a:layout_gravity="bottom|center_horizontal"
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<include layout="@layout/download_playlist"/>
|
<include layout="@layout/current_playlist"/>
|
||||||
</org.moire.ultrasonic.util.MyViewFlipper>
|
</org.moire.ultrasonic.util.MyViewFlipper>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -5,13 +5,13 @@
|
||||||
a:orientation="vertical" >
|
a:orientation="vertical" >
|
||||||
|
|
||||||
<org.moire.ultrasonic.util.MyViewFlipper
|
<org.moire.ultrasonic.util.MyViewFlipper
|
||||||
a:id="@+id/download_playlist_flipper"
|
a:id="@+id/current_playing_playlist_flipper"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="0dip"
|
a:layout_height="0dip"
|
||||||
a:layout_weight="1" >
|
a:layout_weight="1" >
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
a:id="@+id/download_album_art_layout"
|
a:id="@+id/current_playing_album_art_layout"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="fill_parent"
|
a:layout_height="fill_parent"
|
||||||
a:layout_weight="1"
|
a:layout_weight="1"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
a:orientation="vertical" >
|
a:orientation="vertical" >
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_album_art_image"
|
a:id="@+id/current_playing_album_art_image"
|
||||||
a:layout_width="match_parent"
|
a:layout_width="match_parent"
|
||||||
a:layout_height="match_parent"
|
a:layout_height="match_parent"
|
||||||
a:scaleType="centerCrop"
|
a:scaleType="centerCrop"
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
a:id="@+id/download_visualizer_view_layout"
|
a:id="@+id/current_playing_visualizer_layout"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="60dip"
|
a:layout_height="60dip"
|
||||||
a:layout_gravity="center"
|
a:layout_gravity="center"
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<include layout="@layout/download_playlist" />
|
<include layout="@layout/current_playlist" />
|
||||||
</org.moire.ultrasonic.util.MyViewFlipper>
|
</org.moire.ultrasonic.util.MyViewFlipper>
|
||||||
|
|
||||||
<include layout="@layout/player_media_info" />
|
<include layout="@layout/player_media_info" />
|
|
@ -9,14 +9,14 @@
|
||||||
a:layout_weight="1">
|
a:layout_weight="1">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_empty"
|
a:id="@+id/playlist_empty"
|
||||||
a:text="@string/download.empty"
|
a:text="@string/download.empty"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:padding="10dip"/>
|
a:padding="10dip"/>
|
||||||
|
|
||||||
<com.mobeta.android.dslv.DragSortListView
|
<com.mobeta.android.dslv.DragSortListView
|
||||||
a:id="@+id/download_list"
|
a:id="@+id/playlist_view"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="0dip"
|
a:layout_height="0dip"
|
||||||
a:layout_weight="1"
|
a:layout_weight="1"
|
|
@ -8,7 +8,7 @@
|
||||||
a:layout_marginRight="12dp" >
|
a:layout_marginRight="12dp" >
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_shuffle"
|
a:id="@+id/button_shuffle"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="26dp"
|
a:layout_height="26dp"
|
||||||
a:layout_alignParentLeft="true"
|
a:layout_alignParentLeft="true"
|
||||||
|
@ -17,10 +17,11 @@
|
||||||
a:adjustViewBounds="true"
|
a:adjustViewBounds="true"
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_shuffle" />
|
a:src="?attr/media_shuffle"
|
||||||
|
a:contentDescription="@string/buttons.shuffle" />
|
||||||
|
|
||||||
<org.moire.ultrasonic.view.AutoRepeatButton
|
<org.moire.ultrasonic.view.AutoRepeatButton
|
||||||
a:id="@+id/download_previous"
|
a:id="@+id/button_previous"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="42dp"
|
a:layout_height="42dp"
|
||||||
a:layout_gravity="center"
|
a:layout_gravity="center"
|
||||||
|
@ -28,10 +29,11 @@
|
||||||
a:adjustViewBounds="true"
|
a:adjustViewBounds="true"
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_previous" />
|
a:src="?attr/media_previous"
|
||||||
|
a:contentDescription="@string/buttons.previous" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_start"
|
a:id="@+id/button_start"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="74dp"
|
a:layout_height="74dp"
|
||||||
a:layout_weight="2"
|
a:layout_weight="2"
|
||||||
|
@ -39,20 +41,22 @@
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_play"
|
a:src="?attr/media_play"
|
||||||
tools:visibility="gone" />
|
tools:visibility="gone"
|
||||||
|
a:contentDescription="@string/buttons.play" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_pause"
|
a:id="@+id/button_pause"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="74dp"
|
a:layout_height="74dp"
|
||||||
a:layout_weight="2"
|
a:layout_weight="2"
|
||||||
a:adjustViewBounds="true"
|
a:adjustViewBounds="true"
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_pause" />
|
a:src="?attr/media_pause"
|
||||||
|
a:contentDescription="@string/buttons.pause" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_stop"
|
a:id="@+id/button_stop"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="74dp"
|
a:layout_height="74dp"
|
||||||
a:layout_weight="2"
|
a:layout_weight="2"
|
||||||
|
@ -60,10 +64,11 @@
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_stop"
|
a:src="?attr/media_stop"
|
||||||
tools:visibility="gone" />
|
tools:visibility="gone"
|
||||||
|
a:contentDescription="@string/buttons.stop" />
|
||||||
|
|
||||||
<org.moire.ultrasonic.view.AutoRepeatButton
|
<org.moire.ultrasonic.view.AutoRepeatButton
|
||||||
a:id="@+id/download_next"
|
a:id="@+id/button_next"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="42dp"
|
a:layout_height="42dp"
|
||||||
a:layout_gravity="center"
|
a:layout_gravity="center"
|
||||||
|
@ -71,10 +76,11 @@
|
||||||
a:adjustViewBounds="true"
|
a:adjustViewBounds="true"
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_next" />
|
a:src="?attr/media_next"
|
||||||
|
a:contentDescription="@string/buttons.next"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_repeat"
|
a:id="@+id/button_repeat"
|
||||||
a:layout_width="0dip"
|
a:layout_width="0dip"
|
||||||
a:layout_height="26dp"
|
a:layout_height="26dp"
|
||||||
a:layout_gravity="center"
|
a:layout_gravity="center"
|
||||||
|
@ -82,6 +88,7 @@
|
||||||
a:adjustViewBounds="true"
|
a:adjustViewBounds="true"
|
||||||
a:focusable="true"
|
a:focusable="true"
|
||||||
a:scaleType="fitCenter"
|
a:scaleType="fitCenter"
|
||||||
a:src="?attr/media_repeat_off" />
|
a:src="?attr/media_repeat_off"
|
||||||
|
a:contentDescription="@string/buttons.repeat" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -15,7 +15,7 @@
|
||||||
a:orientation="vertical">
|
a:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_song_title"
|
a:id="@+id/current_playing_song"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:ellipsize="start"
|
a:ellipsize="start"
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
tools:text="Title" />
|
tools:text="Title" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_artist"
|
a:id="@+id/current_playing_artist"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:ellipsize="start"
|
a:ellipsize="start"
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
tools:text="Artist" />
|
tools:text="Artist" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_album"
|
a:id="@+id/current_playing_album"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:ellipsize="start"
|
a:ellipsize="start"
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
a:orientation="vertical">
|
a:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_track"
|
a:id="@+id/current_playing_track"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:ellipsize="start"
|
a:ellipsize="start"
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
a:textAppearance="?android:attr/textAppearanceSmall" />
|
a:textAppearance="?android:attr/textAppearanceSmall" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_total_duration"
|
a:id="@+id/current_total_duration"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:ellipsize="start"
|
a:ellipsize="start"
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
a:layout_marginRight="12dp" >
|
a:layout_marginRight="12dp" >
|
||||||
|
|
||||||
<SeekBar
|
<SeekBar
|
||||||
a:id="@+id/download_progress_bar"
|
a:id="@+id/current_playing_progress_bar"
|
||||||
a:layout_width="fill_parent"
|
a:layout_width="fill_parent"
|
||||||
a:layout_height="32dp"
|
a:layout_height="32dp"
|
||||||
a:indeterminate="false" />
|
a:indeterminate="false" />
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
a:layout_height="match_parent">
|
a:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_position"
|
a:id="@+id/current_playing_position"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:layout_alignParentLeft="true"
|
a:layout_alignParentLeft="true"
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
a:textAppearance="?android:attr/textAppearanceSmall" />
|
a:textAppearance="?android:attr/textAppearanceSmall" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
a:id="@+id/download_duration"
|
a:id="@+id/current_playing_duration"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="wrap_content"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
a:layout_alignParentRight="true"
|
a:layout_alignParentRight="true"
|
||||||
|
|
|
@ -15,6 +15,13 @@
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
<string name="button_bar.home">Ultrasonic Main</string>
|
<string name="button_bar.home">Ultrasonic Main</string>
|
||||||
<string name="button_bar.now_playing">Now Playing</string>
|
<string name="button_bar.now_playing">Now Playing</string>
|
||||||
|
<string name="buttons.play">Play</string>
|
||||||
|
<string name="buttons.pause">Pause</string>
|
||||||
|
<string name="buttons.repeat">Repeat</string>
|
||||||
|
<string name="buttons.shuffle">Shuffle</string>
|
||||||
|
<string name="buttons.stop">Stop</string>
|
||||||
|
<string name="buttons.next">Next</string>
|
||||||
|
<string name="buttons.previous">Previous</string>
|
||||||
<string name="podcasts.label">Podcast</string>
|
<string name="podcasts.label">Podcast</string>
|
||||||
<string name="podcasts_channels.empty">No podcasts channels registered</string>
|
<string name="podcasts_channels.empty">No podcasts channels registered</string>
|
||||||
<string name="button_bar.podcasts">Podcast</string>
|
<string name="button_bar.podcasts">Podcast</string>
|
||||||
|
|
Loading…
Reference in New Issue