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