mirror of
https://github.com/antebudimir/tempus.git
synced 2026-01-01 18:03:33 +00:00
feat: Integrate external downloads into downloaded songs view
This commit is contained in:
parent
682f63ef38
commit
1357c5c062
9 changed files with 201 additions and 1 deletions
|
|
@ -15,6 +15,9 @@ public interface DownloadDao {
|
||||||
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
LiveData<List<Download>> getAll();
|
LiveData<List<Download>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
|
List<Download> getAllSync();
|
||||||
|
|
||||||
@Query("SELECT * FROM download WHERE id = :id")
|
@Query("SELECT * FROM download WHERE id = :id")
|
||||||
Download getOne(String id);
|
Download getOne(String id);
|
||||||
|
|
||||||
|
|
@ -30,6 +33,9 @@ public interface DownloadDao {
|
||||||
@Query("DELETE FROM download WHERE id = :id")
|
@Query("DELETE FROM download WHERE id = :id")
|
||||||
void delete(String id);
|
void delete(String id);
|
||||||
|
|
||||||
|
@Query("DELETE FROM download WHERE id IN (:ids)")
|
||||||
|
void deleteByIds(List<String> ids);
|
||||||
|
|
||||||
@Query("DELETE FROM download")
|
@Query("DELETE FROM download")
|
||||||
void deleteAll();
|
void deleteAll();
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,20 @@ public class DownloadRepository {
|
||||||
return downloadDao.getAll();
|
return downloadDao.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Download> getAllDownloads() {
|
||||||
|
GetAllDownloadsThreadSafe getDownloads = new GetAllDownloadsThreadSafe(downloadDao);
|
||||||
|
Thread thread = new Thread(getDownloads);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
thread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDownloads.getDownloads();
|
||||||
|
}
|
||||||
|
|
||||||
public Download getDownload(String id) {
|
public Download getDownload(String id) {
|
||||||
Download download = null;
|
Download download = null;
|
||||||
|
|
||||||
|
|
@ -35,6 +49,24 @@ public class DownloadRepository {
|
||||||
return download;
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GetAllDownloadsThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private List<Download> downloads;
|
||||||
|
|
||||||
|
public GetAllDownloadsThreadSafe(DownloadDao downloadDao) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloads = downloadDao.getAllSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Download> getDownloads() {
|
||||||
|
return downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class GetDownloadThreadSafe implements Runnable {
|
private static class GetDownloadThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
@ -143,6 +175,12 @@ public class DownloadRepository {
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids);
|
||||||
|
Thread thread = new Thread(delete);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class DeleteThreadSafe implements Runnable {
|
private static class DeleteThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
@ -157,4 +195,19 @@ public class DownloadRepository {
|
||||||
downloadDao.delete(id);
|
downloadDao.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class DeleteMultipleThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private final List<String> ids;
|
||||||
|
|
||||||
|
public DeleteMultipleThreadSafe(DownloadDao downloadDao, List<String> ids) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloadDao.deleteByIds(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> {
|
||||||
|
if (count == null || bind == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == -1) {
|
||||||
|
Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (count == 0) {
|
||||||
|
Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
getResources().getQuantityString(R.plurals.download_refresh_removed, count, count),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
||||||
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||||
|
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishDownloadView(List<Child> songs) {
|
private void finishDownloadView(List<Child> songs) {
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,16 @@ public class ExternalAudioReader {
|
||||||
return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum());
|
return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized void removeMetadata(Child media) {
|
||||||
|
if (media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
cache.remove(key);
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean delete(Child media) {
|
public static boolean delete(Child media) {
|
||||||
ensureCache();
|
ensureCache();
|
||||||
if (cachedDirUri == null) return false;
|
if (cachedDirUri == null) return false;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import androidx.core.app.NotificationCompat;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
|
|
@ -156,6 +158,7 @@ public class ExternalAudioWriter {
|
||||||
}
|
}
|
||||||
if (matches) {
|
if (matches) {
|
||||||
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
||||||
|
recordDownload(child, existingFile.getUri());
|
||||||
ExternalAudioReader.refreshCache();
|
ExternalAudioReader.refreshCache();
|
||||||
notifyExists(context, fileName);
|
notifyExists(context, fileName);
|
||||||
return;
|
return;
|
||||||
|
|
@ -204,6 +207,7 @@ public class ExternalAudioWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
||||||
|
recordDownload(child, targetUri);
|
||||||
notifySuccess(context, fileName, child, targetUri);
|
notifySuccess(context, fileName, child, targetUri);
|
||||||
ExternalAudioReader.refreshCache();
|
ExternalAudioReader.refreshCache();
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +269,20 @@ public class ExternalAudioWriter {
|
||||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void recordDownload(Child child, Uri fileUri) {
|
||||||
|
if (child == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Download download = new Download(child);
|
||||||
|
download.setDownloadState(1);
|
||||||
|
if (fileUri != null) {
|
||||||
|
download.setDownloadUri(fileUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
new DownloadRepository().insert(download);
|
||||||
|
}
|
||||||
|
|
||||||
private static void notifyExists(Context context, String name) {
|
private static void notifyExists(Context context, String name) {
|
||||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
@ -8,10 +9,13 @@ import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.DownloadStack;
|
import com.cappielloantonio.tempo.model.DownloadStack;
|
||||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -25,6 +29,7 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Integer> refreshResult = new MutableLiveData<>();
|
||||||
|
|
||||||
public DownloadViewModel(@NonNull Application application) {
|
public DownloadViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
return viewStack;
|
return viewStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Integer> getRefreshResult() {
|
||||||
|
return refreshResult;
|
||||||
|
}
|
||||||
|
|
||||||
public void initViewStack(DownloadStack level) {
|
public void initViewStack(DownloadStack level) {
|
||||||
ArrayList<DownloadStack> stack = new ArrayList<>();
|
ArrayList<DownloadStack> stack = new ArrayList<>();
|
||||||
stack.add(level);
|
stack.add(level);
|
||||||
|
|
@ -60,4 +69,59 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
stack.remove(stack.size() - 1);
|
stack.remove(stack.size() - 1);
|
||||||
viewStack.setValue(stack);
|
viewStack.setValue(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refreshExternalDownloads() {
|
||||||
|
new Thread(() -> {
|
||||||
|
String directoryUri = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (directoryUri == null) {
|
||||||
|
refreshResult.postValue(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Download> downloads = downloadRepository.getAllDownloads();
|
||||||
|
if (downloads == null || downloads.isEmpty()) {
|
||||||
|
refreshResult.postValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Download> toRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Download download : downloads) {
|
||||||
|
String uriString = download.getDownloadUri();
|
||||||
|
if (uriString == null || uriString.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(uriString);
|
||||||
|
if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("content")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile file;
|
||||||
|
try {
|
||||||
|
file = DocumentFile.fromSingleUri(getApplication(), uri);
|
||||||
|
} catch (SecurityException exception) {
|
||||||
|
file = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null || !file.exists()) {
|
||||||
|
toRemove.add(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toRemove.isEmpty()) {
|
||||||
|
ArrayList<String> ids = new ArrayList<>();
|
||||||
|
for (Download download : toRemove) {
|
||||||
|
ids.add(download.getId());
|
||||||
|
ExternalAudioReader.removeMetadata(download);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadRepository.delete(ids);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
refreshResult.postValue(ids.size());
|
||||||
|
} else {
|
||||||
|
refreshResult.postValue(0);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -8,3.58 -8,8h2c0,-3.31 2.69,-6 6,-6 1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35zM19,12c0,3.31 -2.69,6 -6,6 -1.66,0 -3.14,-0.69 -4.22,-1.78L11,13H4v7l2.35,-2.35C7.8,19.1 9.79,20 12,20c4.42,0 8,-3.58 8,-8h-2z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/download_title_section"
|
android:text="@string/download_title_section"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/downloaded_go_back_image_view"
|
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
@ -94,6 +94,19 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/downloaded_refresh_image_view"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/ic_refresh"
|
||||||
|
android:contentDescription="@string/download_refresh_button_content_description"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/downloaded_go_back_image_view"
|
android:id="@+id/downloaded_go_back_image_view"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
|
|
@ -103,6 +116,7 @@
|
||||||
android:background="@drawable/ic_arrow_back"
|
android:background="@drawable/ic_arrow_back"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,13 @@
|
||||||
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
||||||
<string name="download_storage_directory_dialog_neutral_button">Directory</string>
|
<string name="download_storage_directory_dialog_neutral_button">Directory</string>
|
||||||
<string name="download_title_section">Downloads</string>
|
<string name="download_title_section">Downloads</string>
|
||||||
|
<string name="download_refresh_no_directory">Set a download folder to refresh your downloads.</string>
|
||||||
|
<string name="download_refresh_no_changes">No missing downloads found.</string>
|
||||||
|
<plurals name="download_refresh_removed">
|
||||||
|
<item quantity="one">Removed %d missing download.</item>
|
||||||
|
<item quantity="other">Removed %d missing downloads.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="download_refresh_button_content_description">Refresh downloaded items</string>
|
||||||
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
||||||
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
||||||
<string name="downloaded_bottom_sheet_remove">Remove</string>
|
<string name="downloaded_bottom_sheet_remove">Remove</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue