diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d2aeff9..fa059796 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,5 +25,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java index 6d5e9e7b..18b886cb 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java @@ -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; diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/SongResultSearchAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/SongResultSearchAdapter.java index c26f58ed..2c7fdeae 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/SongResultSearchAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/SongResultSearchAdapter.java @@ -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 getSongsByID(List ids); diff --git a/app/src/main/java/com/cappielloantonio/play/model/Song.java b/app/src/main/java/com/cappielloantonio/play/model/Song.java index 76dd1345..1a695670 100644 --- a/app/src/main/java/com/cappielloantonio/play/model/Song.java +++ b/app/src/main/java/com/cappielloantonio/play/model/Song.java @@ -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 CREATOR = new Creator() { diff --git a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java index 9b4b26a3..82d9da81 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java @@ -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 songGenreCrosses) { InsertPerGenreThreadSafe insertPerGenre = new InsertPerGenreThreadSafe(songGenreCrossDao, songGenreCrosses); Thread thread = new Thread(insertPerGenre); diff --git a/app/src/main/java/com/cappielloantonio/play/service/DownloadTracker.java b/app/src/main/java/com/cappielloantonio/play/service/DownloadTracker.java new file mode 100644 index 00000000..8f00595c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/DownloadTracker.java @@ -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 listeners; + private final HashMap 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 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(); + } + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java b/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java index 4961a0e4..1bb3c878 100644 --- a/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java +++ b/app/src/main/java/com/cappielloantonio/play/service/MultiPlayer.java @@ -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); diff --git a/app/src/main/java/com/cappielloantonio/play/service/PlayDownloadService.java b/app/src/main/java/com/cappielloantonio/play/service/PlayDownloadService.java new file mode 100644 index 00000000..96616961 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/PlayDownloadService.java @@ -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 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. + * + *

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); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java index d67362fd..b7c3dad4 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java @@ -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 reference; diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 0309ddc8..6d7e1a93 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -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 songs = songRepository.getAlbumListSong(album.getId(), false); + DownloadUtil.getDownloadTracker(requireContext()).toggleDownload(songs); dismissBottomSheet(); }); diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index fd5f7ff1..98825ab0 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -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"); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java new file mode 100644 index 00000000..2260b000 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java @@ -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; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java index d10f6fe5..d5745637 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/MusicUtil.java @@ -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 getMediaItemsFromSongs(List songs) { + List 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 getRandomSongNumber(Context context, int numberOfNumbers, int refreshAfterXHours) { List list = new ArrayList<>(); diff --git a/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java index 0073613d..66a9a329 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java @@ -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; diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 00000000..53a6ad40 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/bottom_sheet_song_dialog.xml b/app/src/main/res/layout/bottom_sheet_song_dialog.xml index dca42a20..1a63e5c2 100644 --- a/app/src/main/res/layout/bottom_sheet_song_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_song_dialog.xml @@ -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" /> + + @@ -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" /> + + #FFFFFF #FFFFFF + + #77DD77 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20dbecfc..a302ac4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,4 +49,6 @@ The playing notification provides actions for play/pause etc. Playing Notification + + Downloads \ No newline at end of file