mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
Implemented offline mode functionality
This commit is contained in:
parent
658e69dcb9
commit
e0569c3901
21 changed files with 635 additions and 23 deletions
|
|
@ -25,5 +25,13 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".service.MusicService" android:enabled="true"/>
|
||||
|
||||
<service android:name=".service.PlayDownloadService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -21,6 +21,7 @@ import com.cappielloantonio.play.model.Song;
|
|||
import com.cappielloantonio.play.repository.QueueRepository;
|
||||
import com.cappielloantonio.play.repository.SongRepository;
|
||||
import com.cappielloantonio.play.ui.activities.MainActivity;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
|
@ -21,7 +20,6 @@ import com.cappielloantonio.play.model.Song;
|
|||
import com.cappielloantonio.play.repository.QueueRepository;
|
||||
import com.cappielloantonio.play.ui.activities.MainActivity;
|
||||
import com.cappielloantonio.play.util.MusicUtil;
|
||||
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -60,6 +58,12 @@ public class SongResultSearchAdapter extends RecyclerView.Adapter<SongResultSear
|
|||
holder.songArtist.setText(song.getArtistName());
|
||||
holder.songDuration.setText(MusicUtil.getReadableDurationString(song.getDuration()));
|
||||
|
||||
if (song.isOffline()) {
|
||||
holder.downloadIndicator.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.downloadIndicator.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(context, song.getPrimary(), song.getBlurHash(), CustomGlideRequest.PRIMARY, CustomGlideRequest.TOP_QUALITY, CustomGlideRequest.SONG_PIC)
|
||||
.build()
|
||||
|
|
@ -75,6 +79,7 @@ public class SongResultSearchAdapter extends RecyclerView.Adapter<SongResultSear
|
|||
TextView songTitle;
|
||||
TextView songArtist;
|
||||
TextView songDuration;
|
||||
View downloadIndicator;
|
||||
ImageView more;
|
||||
ImageView cover;
|
||||
|
||||
|
|
@ -84,6 +89,7 @@ public class SongResultSearchAdapter extends RecyclerView.Adapter<SongResultSear
|
|||
songTitle = itemView.findViewById(R.id.search_result_song_title_text_view);
|
||||
songArtist = itemView.findViewById(R.id.search_result_song_artist_text_view);
|
||||
songDuration = itemView.findViewById(R.id.search_result_song_duration_text_view);
|
||||
downloadIndicator = itemView.findViewById(R.id.search_result_dowanload_indicator_image_view);
|
||||
more = itemView.findViewById(R.id.search_result_song_more_button);
|
||||
cover = itemView.findViewById(R.id.song_cover_image_view);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import com.cappielloantonio.play.model.Song;
|
|||
import com.cappielloantonio.play.model.SongArtistCross;
|
||||
import com.cappielloantonio.play.model.SongGenreCross;
|
||||
|
||||
@Database(entities = {Album.class, Artist.class, Genre.class, Playlist.class, Song.class, RecentSearch.class, SongGenreCross.class, Queue.class, AlbumArtistCross.class, SongArtistCross.class, PlaylistSongCross.class}, version = 11, exportSchema = false)
|
||||
@Database(entities = {Album.class, Artist.class, Genre.class, Playlist.class, Song.class, RecentSearch.class, SongGenreCross.class, Queue.class, AlbumArtistCross.class, SongArtistCross.class, PlaylistSongCross.class}, version = 12, exportSchema = false)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
private static final String TAG = "AppDatabase";
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ public interface SongDao {
|
|||
@Update
|
||||
void update(Song song);
|
||||
|
||||
@Query("UPDATE song SET offline = 0 WHERE offline == 1")
|
||||
void updateAllOffline();
|
||||
|
||||
@Query("SELECT * FROM song WHERE id IN (:ids)")
|
||||
List<Song> getSongsByID(List<String> ids);
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,10 @@ public class Song implements Parcelable {
|
|||
@ColumnInfo(name = "last_play")
|
||||
private long lastPlay;
|
||||
|
||||
public Song(@NonNull String id, String title, int trackNumber, int discNumber, int year, long duration, String albumId, String albumName, String artistId, String artistName, String primary, String blurHash, boolean favorite, String path, long size, String container, String codec, int sampleRate, int bitRate, int bitDepth, int channels, long added, int playCount, long lastPlay) {
|
||||
@ColumnInfo(name = "offline")
|
||||
private boolean offline;
|
||||
|
||||
public Song(@NonNull String id, String title, int trackNumber, int discNumber, int year, long duration, String albumId, String albumName, String artistId, String artistName, String primary, String blurHash, boolean favorite, String path, long size, String container, String codec, int sampleRate, int bitRate, int bitDepth, int channels, long added, int playCount, long lastPlay, boolean offline) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.trackNumber = trackNumber;
|
||||
|
|
@ -160,6 +163,7 @@ public class Song implements Parcelable {
|
|||
this.added = added;
|
||||
this.playCount = playCount;
|
||||
this.lastPlay = lastPlay;
|
||||
this.offline = offline;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
|
|
@ -228,6 +232,7 @@ public class Song implements Parcelable {
|
|||
this.added = Instant.now().toEpochMilli();
|
||||
this.playCount = 0;
|
||||
this.lastPlay = 0;
|
||||
this.offline = false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -327,6 +332,10 @@ public class Song implements Parcelable {
|
|||
return lastPlay;
|
||||
}
|
||||
|
||||
public boolean isOffline() {
|
||||
return offline;
|
||||
}
|
||||
|
||||
public void setId(@NonNull String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
|
@ -423,6 +432,8 @@ public class Song implements Parcelable {
|
|||
this.playCount = playCount;
|
||||
}
|
||||
|
||||
public void setOffline(boolean offline) { this.offline = offline; }
|
||||
|
||||
/*
|
||||
Log.i(TAG, "increasePlayCount: " + isIncreased);
|
||||
* Incremento il numero di ascolti solo se ho ascoltato la canzone da più tempo di:
|
||||
|
|
@ -493,6 +504,7 @@ public class Song implements Parcelable {
|
|||
dest.writeLong(this.added);
|
||||
dest.writeInt(this.playCount);
|
||||
dest.writeLong(this.lastPlay);
|
||||
dest.writeBoolean(this.offline);
|
||||
}
|
||||
|
||||
protected Song(Parcel in) {
|
||||
|
|
@ -520,6 +532,7 @@ public class Song implements Parcelable {
|
|||
this.added = in.readLong();
|
||||
this.playCount = in.readInt();
|
||||
this.lastPlay = in.readLong();
|
||||
this.offline = in.readBoolean();
|
||||
}
|
||||
|
||||
public static final Creator<Song> CREATOR = new Creator<Song>() {
|
||||
|
|
|
|||
|
|
@ -353,6 +353,12 @@ public class SongRepository {
|
|||
thread.start();
|
||||
}
|
||||
|
||||
public void setOfflineStatus(Song song) {
|
||||
UpdateThreadSafe update = new UpdateThreadSafe(songDao, song);
|
||||
Thread thread = new Thread(update);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static class UpdateThreadSafe implements Runnable {
|
||||
private SongDao songDao;
|
||||
private Song song;
|
||||
|
|
@ -368,6 +374,25 @@ public class SongRepository {
|
|||
}
|
||||
}
|
||||
|
||||
public void setAllOffline() {
|
||||
SetAllOfflineThreadSafe update = new SetAllOfflineThreadSafe(songDao);
|
||||
Thread thread = new Thread(update);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static class SetAllOfflineThreadSafe implements Runnable {
|
||||
private SongDao songDao;
|
||||
|
||||
public SetAllOfflineThreadSafe(SongDao songDao) {
|
||||
this.songDao = songDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
songDao.updateAllOffline();
|
||||
}
|
||||
}
|
||||
|
||||
public void insertSongPerGenre(ArrayList<SongGenreCross> songGenreCrosses) {
|
||||
InsertPerGenreThreadSafe insertPerGenre = new InsertPerGenreThreadSafe(songGenreCrossDao, songGenreCrosses);
|
||||
Thread thread = new Thread(insertPerGenre);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.cappielloantonio.play.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.MediaStore;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.cappielloantonio.play.App;
|
||||
import com.cappielloantonio.play.model.Song;
|
||||
import com.cappielloantonio.play.repository.SongRepository;
|
||||
import com.cappielloantonio.play.util.MusicUtil;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.OfflineLicenseHelper;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper.LiveContentUnsupportedException;
|
||||
import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
/**
|
||||
* Tracks media that has been downloaded.
|
||||
*/
|
||||
public class DownloadTracker {
|
||||
|
||||
private static final String TAG = "DownloadTracker";
|
||||
private final Context context;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, Download> downloads;
|
||||
private final DownloadIndex downloadIndex;
|
||||
private final DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
|
||||
public DownloadTracker(Context context,HttpDataSource.Factory httpDataSourceFactory,DownloadManager downloadManager) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
downloads = new HashMap<>();
|
||||
downloadIndex = downloadManager.getDownloadIndex();
|
||||
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
|
||||
downloadManager.addListener(new DownloadManagerListener());
|
||||
loadDownloads();
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
checkNotNull(listener);
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean isDownloaded(Song song) {
|
||||
MediaItem mediaItem = MusicUtil.getMediaItemFromSong(song);
|
||||
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||
return download != null && download.state != Download.STATE_FAILED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
return new DownloadRequest.Builder(uri.toString(), uri).build();
|
||||
}
|
||||
|
||||
public void toggleDownload(List<Song> songs) {
|
||||
SongRepository songRepository = new SongRepository(App.getInstance());
|
||||
|
||||
for(Song song: songs) {
|
||||
MediaItem mediaItem = MusicUtil.getMediaItemFromSong(song);
|
||||
|
||||
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||
|
||||
if (download != null && download.state != Download.STATE_FAILED) {
|
||||
song.setOffline(false);
|
||||
DownloadService.sendRemoveDownload(context, PlayDownloadService.class, download.request.id, false);
|
||||
} else {
|
||||
song.setOffline(true);
|
||||
DownloadService.sendAddDownload(context, PlayDownloadService.class, getDownloadRequest(mediaItem.playbackProperties.uri),false);
|
||||
}
|
||||
|
||||
songRepository.setOfflineStatus(song);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAllDownloads() {
|
||||
SongRepository songRepository = new SongRepository(App.getInstance());
|
||||
songRepository.setAllOffline();
|
||||
DownloadService.sendRemoveAllDownloads(context, PlayDownloadService.class, false);
|
||||
}
|
||||
|
||||
private void loadDownloads() {
|
||||
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
|
||||
while (loadedDownloads.moveToNext()) {
|
||||
Download download = loadedDownloads.getDownload();
|
||||
downloads.put(download.request.uri, download);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to query downloads", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface Listener {
|
||||
void onDownloadsChanged();
|
||||
}
|
||||
|
||||
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(
|
||||
@NonNull DownloadManager downloadManager,
|
||||
@NonNull Download download,
|
||||
@Nullable Exception finalException) {
|
||||
downloads.put(download.request.uri, download);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadRemoved(
|
||||
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||
downloads.remove(download.request.uri);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import android.widget.Toast;
|
|||
import com.cappielloantonio.play.R;
|
||||
import com.cappielloantonio.play.model.Song;
|
||||
import com.cappielloantonio.play.service.playback.Playback;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.cappielloantonio.play.util.MusicUtil;
|
||||
import com.cappielloantonio.play.util.PreferenceUtil;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
|
@ -16,6 +17,7 @@ import com.google.android.exoplayer2.Player;
|
|||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
|
@ -77,8 +79,17 @@ public class MultiPlayer implements Playback {
|
|||
public MultiPlayer(Context context) {
|
||||
this.context = context;
|
||||
|
||||
MediaSourceFactory mediaSourceFactory = new UnknownMediaSourceFactory(buildDataSourceFactory());
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
|
||||
// Create a read-only cache data source factory using the download cache.
|
||||
DataSource.Factory cacheDataSourceFactory =
|
||||
new CacheDataSource.Factory()
|
||||
.setCache(DownloadUtil.getDownloadCache(context))
|
||||
.setUpstreamDataSourceFactory(DownloadUtil.getHttpDataSourceFactory(context))
|
||||
.setCacheWriteDataSinkFactory(null); // Disable writing.
|
||||
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context)
|
||||
.setMediaSourceFactory(new DefaultMediaSourceFactory(cacheDataSourceFactory))
|
||||
.build();
|
||||
|
||||
// TODO: “Player is accessed on the wrong thread” suppressed
|
||||
// exoPlayer.setThrowsWhenUsingWrongThread(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.cappielloantonio.play.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.cappielloantonio.play.R;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A service for downloading media.
|
||||
*/
|
||||
public class PlayDownloadService extends DownloadService {
|
||||
|
||||
private static final int JOB_ID = 1;
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||
|
||||
public PlayDownloadService() {
|
||||
super(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||
R.string.exo_download_notification_channel_name,
|
||||
0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Scheduler getScheduler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected DownloadManager getDownloadManager() {
|
||||
|
||||
DownloadManager downloadManager = DownloadUtil.getDownloadManager(/* context= */ this);
|
||||
DownloadNotificationHelper downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this);
|
||||
downloadManager.addListener(
|
||||
new TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
|
||||
return DownloadUtil.getDownloadNotificationHelper(/* context= */ this)
|
||||
.buildProgressNotification(
|
||||
this,
|
||||
R.drawable.ic_downloading,
|
||||
null,
|
||||
null,
|
||||
downloads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and displays notifications for downloads when they complete or fail.
|
||||
*
|
||||
* <p>This helper will outlive the lifespan of a single instance of DemoDownloadService.
|
||||
* It is static to avoid leaking the first DemoDownloadService instance.
|
||||
*/
|
||||
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
|
||||
|
||||
private final Context context;
|
||||
private final DownloadNotificationHelper notificationHelper;
|
||||
|
||||
private int nextNotificationId;
|
||||
|
||||
public TerminalStateNotificationHelper(
|
||||
Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.notificationHelper = notificationHelper;
|
||||
nextNotificationId = firstNotificationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(
|
||||
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
|
||||
Notification notification;
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.drawable.ic_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.drawable.ic_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
NotificationUtil.setNotification(context, nextNotificationId++, notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import android.os.IBinder;
|
|||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
|
@ -20,7 +21,11 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import com.cappielloantonio.play.R;
|
||||
import com.cappielloantonio.play.helper.MusicPlayerRemote;
|
||||
import com.cappielloantonio.play.interfaces.MusicServiceEventListener;
|
||||
import com.cappielloantonio.play.service.DownloadTracker;
|
||||
import com.cappielloantonio.play.service.MusicService;
|
||||
import com.cappielloantonio.play.service.PlayDownloadService;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -29,7 +34,7 @@ import java.util.List;
|
|||
import pub.devrel.easypermissions.AppSettingsDialog;
|
||||
import pub.devrel.easypermissions.EasyPermissions;
|
||||
|
||||
public class BaseActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks, MusicServiceEventListener {
|
||||
public class BaseActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks, MusicServiceEventListener, DownloadTracker.Listener {
|
||||
private static final String TAG = "BaseActivity";
|
||||
public static final int REQUEST_PERM_ACCESS = 1;
|
||||
|
||||
|
|
@ -38,6 +43,8 @@ public class BaseActivity extends AppCompatActivity implements EasyPermissions.P
|
|||
private MusicPlayerRemote.ServiceToken serviceToken;
|
||||
private MusicStateReceiver musicStateReceiver;
|
||||
|
||||
private DownloadTracker downloadTracker;
|
||||
|
||||
private boolean receiverRegistered;
|
||||
|
||||
@Override
|
||||
|
|
@ -58,6 +65,23 @@ public class BaseActivity extends AppCompatActivity implements EasyPermissions.P
|
|||
BaseActivity.this.onServiceDisconnected();
|
||||
}
|
||||
});
|
||||
|
||||
downloadTracker = DownloadUtil.getDownloadTracker(this);
|
||||
// Start the download service if it should be running but it's not currently.
|
||||
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||
// action. Starting it in the background throws an exception if the app is in the background too
|
||||
// (e.g. if device screen is locked).
|
||||
try {
|
||||
DownloadService.start(this, PlayDownloadService.class);
|
||||
} catch (IllegalStateException e) {
|
||||
DownloadService.startForeground(this, PlayDownloadService.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
downloadTracker.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -67,6 +91,12 @@ public class BaseActivity extends AppCompatActivity implements EasyPermissions.P
|
|||
checkBatteryOptimization();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
downloadTracker.removeListener(this);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
|
@ -216,6 +246,13 @@ public class BaseActivity extends AppCompatActivity implements EasyPermissions.P
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadsChanged() {
|
||||
// TODO Notificare all'item scaricato che lo stato di download è cambiato
|
||||
// sampleAdapter.notifyDataSetChanged();
|
||||
Toast.makeText(this, "Download changed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private static final class MusicStateReceiver extends BroadcastReceiver {
|
||||
|
||||
private final WeakReference<BaseActivity> reference;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import com.cappielloantonio.play.model.Song;
|
|||
import com.cappielloantonio.play.repository.QueueRepository;
|
||||
import com.cappielloantonio.play.repository.SongRepository;
|
||||
import com.cappielloantonio.play.ui.activities.MainActivity;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.cappielloantonio.play.util.MusicUtil;
|
||||
import com.cappielloantonio.play.util.PreferenceUtil;
|
||||
import com.cappielloantonio.play.util.SyncUtil;
|
||||
import com.cappielloantonio.play.viewmodel.AlbumBottomSheetViewModel;
|
||||
|
|
@ -135,7 +137,8 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
|
||||
Download = view.findViewById(R.id.download_text_view);
|
||||
Download.setOnClickListener(v -> {
|
||||
Toast.makeText(requireContext(), "Download", Toast.LENGTH_SHORT).show();
|
||||
List<Song> songs = songRepository.getAlbumListSong(album.getId(), false);
|
||||
DownloadUtil.getDownloadTracker(requireContext()).toggleDownload(songs);
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,19 +21,18 @@ import com.cappielloantonio.play.helper.MusicPlayerRemote;
|
|||
import com.cappielloantonio.play.interfaces.MediaCallback;
|
||||
import com.cappielloantonio.play.model.Album;
|
||||
import com.cappielloantonio.play.model.Artist;
|
||||
import com.cappielloantonio.play.model.PlaylistSongCross;
|
||||
import com.cappielloantonio.play.model.Song;
|
||||
import com.cappielloantonio.play.repository.QueueRepository;
|
||||
import com.cappielloantonio.play.ui.activities.MainActivity;
|
||||
import com.cappielloantonio.play.util.DownloadUtil;
|
||||
import com.cappielloantonio.play.util.PreferenceUtil;
|
||||
import com.cappielloantonio.play.util.SyncUtil;
|
||||
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.cappielloantonio.play.viewmodel.SongBottomSheetViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||
private static final String TAG = "SongBottomSheetDialog";
|
||||
|
|
@ -44,12 +43,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
private ImageView coverSong;
|
||||
private TextView titleSong;
|
||||
private TextView artistSong;
|
||||
private ToggleButton thumbToggle;
|
||||
private ToggleButton favoriteToggle;
|
||||
private ImageView downloadIndicator;
|
||||
|
||||
private TextView playRadio;
|
||||
private TextView playNext;
|
||||
private TextView addToQueue;
|
||||
private TextView Download;
|
||||
private TextView download;
|
||||
private TextView addToPlaylist;
|
||||
private TextView goToAlbum;
|
||||
private TextView goToArtist;
|
||||
|
|
@ -65,6 +65,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
songBottomSheetViewModel.setSong(song);
|
||||
|
||||
init(view);
|
||||
initDownloadedUI();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
|
@ -83,13 +84,15 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
artistSong = view.findViewById(R.id.song_artist_text_view);
|
||||
artistSong.setText(songBottomSheetViewModel.getSong().getArtistName());
|
||||
|
||||
thumbToggle = view.findViewById(R.id.button_favorite);
|
||||
thumbToggle.setChecked(songBottomSheetViewModel.getSong().isFavorite());
|
||||
thumbToggle.setOnClickListener(v -> {
|
||||
favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().isFavorite());
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
songBottomSheetViewModel.setFavorite();
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
downloadIndicator = view.findViewById(R.id.bottom_sheet_song_dowanload_indicator_image_view);
|
||||
|
||||
playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
playRadio.setOnClickListener(v -> {
|
||||
SyncUtil.getInstantMix(requireContext(), new MediaCallback() {
|
||||
|
|
@ -129,9 +132,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
Download = view.findViewById(R.id.download_text_view);
|
||||
Download.setOnClickListener(v -> {
|
||||
Toast.makeText(requireContext(), "Download", Toast.LENGTH_SHORT).show();
|
||||
download = view.findViewById(R.id.download_text_view);
|
||||
download.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).toggleDownload(Arrays.asList(song));
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
|
|
@ -177,4 +180,14 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
private void dismissBottomSheet() {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void initDownloadedUI() {
|
||||
if (song.isOffline()) {
|
||||
downloadIndicator.setVisibility(View.VISIBLE);
|
||||
download.setText("Remove");
|
||||
} else {
|
||||
downloadIndicator.setVisibility(View.GONE);
|
||||
download.setText("Download");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.cappielloantonio.play.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.cappielloantonio.play.service.DownloadTracker;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class DownloadUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
private static final String TAG = "DemoUtil";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
private static HttpDataSource.Factory httpDataSourceFactory;
|
||||
private static DatabaseProvider databaseProvider;
|
||||
private static File downloadDirectory;
|
||||
private static Cache downloadCache;
|
||||
private static DownloadManager downloadManager;
|
||||
private static DownloadTracker downloadTracker;
|
||||
private static DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
httpDataSourceFactory = new DefaultHttpDataSourceFactory();
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
public static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context));
|
||||
upgradeActionFile(
|
||||
context, DOWNLOAD_ACTION_FILE, downloadIndex, false);
|
||||
upgradeActionFile(
|
||||
context,
|
||||
DOWNLOAD_TRACKER_ACTION_FILE,
|
||||
downloadIndex,
|
||||
true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(6));
|
||||
downloadTracker =
|
||||
new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void upgradeActionFile(
|
||||
Context context,
|
||||
String fileName,
|
||||
DefaultDownloadIndex downloadIndex,
|
||||
boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(context), fileName),
|
||||
null,
|
||||
downloadIndex,
|
||||
true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.cappielloantonio.play.R;
|
|||
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.play.model.DirectPlayCodec;
|
||||
import com.cappielloantonio.play.model.Song;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
|
||||
|
|
@ -91,6 +92,22 @@ public class MusicUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<MediaItem> getMediaItemsFromSongs(List<Song> songs) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for(Song song: songs) {
|
||||
mediaItems.add(getMediaItemFromSong(song));
|
||||
}
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
public static MediaItem getMediaItemFromSong(Song song) {
|
||||
String uri = MusicUtil.getSongFileUri(song);
|
||||
MediaItem mediaItem = MediaItem.fromUri(uri);
|
||||
return mediaItem;
|
||||
}
|
||||
|
||||
public static List<Integer> getRandomSongNumber(Context context, int numberOfNumbers, int refreshAfterXHours) {
|
||||
List<Integer> list = new ArrayList<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ public class SyncUtil {
|
|||
newSong.setAdded(oldSong.getAdded());
|
||||
newSong.setLastPlay(oldSong.getLastPlay());
|
||||
newSong.setPlayCount(oldSong.getPlayCount());
|
||||
newSong.setOffline(oldSong.isOffline());
|
||||
}
|
||||
|
||||
return newSong;
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_check_circle.xml
Normal file
9
app/src/main/res/drawable/ic_check_circle.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/titleTextColor"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z"/>
|
||||
</vector>
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
android:textColor="@color/titleTextColor"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_favorite"
|
||||
app:layout_constraintEnd_toStartOf="@id/bottom_sheet_song_dowanload_indicator_image_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/song_cover_image_view"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
|
@ -73,10 +73,22 @@
|
|||
android:text="@string/label_placeholder"
|
||||
android:textColor="@color/subtitleTextColor"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_favorite"
|
||||
app:layout_constraintEnd_toStartOf="@id/bottom_sheet_song_dowanload_indicator_image_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/song_cover_image_view"
|
||||
app:layout_constraintTop_toBottomOf="@+id/song_title_text_view" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_song_dowanload_indicator_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/ic_check_circle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_favorite"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:text="@string/label_placeholder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_song_more_button"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_dowanload_indicator_image_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/song_cover_image_view"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_song_more_button"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_dowanload_indicator_image_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/song_cover_image_view"
|
||||
app:layout_constraintTop_toBottomOf="@+id/search_result_song_title_text_view">
|
||||
|
||||
|
|
@ -74,6 +74,18 @@
|
|||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/search_result_dowanload_indicator_image_view"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/ic_check_circle"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_song_more_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/search_result_song_more_button"
|
||||
android:layout_width="18dp"
|
||||
|
|
|
|||
|
|
@ -36,4 +36,6 @@
|
|||
<color name="chipUnelectedBackgroundColor">#FFFFFF</color>
|
||||
|
||||
<color name="white">#FFFFFF</color>
|
||||
|
||||
<color name="downloadIconColor">#77DD77</color>
|
||||
</resources>
|
||||
|
|
@ -49,4 +49,6 @@
|
|||
|
||||
<string name="playing_notification_description">The playing notification provides actions for play/pause etc.</string>
|
||||
<string name="playing_notification_name">Playing Notification</string>
|
||||
|
||||
<string name="exo_download_notification_channel_name">Downloads</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue