Implemented offline mode functionality

This commit is contained in:
CappielloAntonio 2021-04-26 19:17:42 +02:00
parent 658e69dcb9
commit e0569c3901
21 changed files with 635 additions and 23 deletions

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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";

View file

@ -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);

View file

@ -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>() {

View file

@ -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);

View file

@ -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();
}
}
}
}

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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();
});

View file

@ -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");
}
}
}

View file

@ -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;
}
}

View file

@ -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<>();

View file

@ -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;

View 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>

View file

@ -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

View file

@ -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"

View file

@ -36,4 +36,6 @@
<color name="chipUnelectedBackgroundColor">#FFFFFF</color>
<color name="white">#FFFFFF</color>
<color name="downloadIconColor">#77DD77</color>
</resources>

View file

@ -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>