mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +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")
|
||||
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")
|
||||
Download getOne(String id);
|
||||
|
||||
|
|
@ -30,6 +33,9 @@ public interface DownloadDao {
|
|||
@Query("DELETE FROM download WHERE id = :id")
|
||||
void delete(String id);
|
||||
|
||||
@Query("DELETE FROM download WHERE id IN (:ids)")
|
||||
void deleteByIds(List<String> ids);
|
||||
|
||||
@Query("DELETE FROM download")
|
||||
void deleteAll();
|
||||
}
|
||||
|
|
@ -18,6 +18,20 @@ public class DownloadRepository {
|
|||
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) {
|
||||
Download download = null;
|
||||
|
||||
|
|
@ -35,6 +49,24 @@ public class DownloadRepository {
|
|||
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 final DownloadDao downloadDao;
|
||||
private final String id;
|
||||
|
|
@ -143,6 +175,12 @@ public class DownloadRepository {
|
|||
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 final DownloadDao downloadDao;
|
||||
private final String id;
|
||||
|
|
@ -157,4 +195,19 @@ public class DownloadRepository {
|
|||
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.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||
}
|
||||
|
||||
private void finishDownloadView(List<Child> songs) {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,16 @@ public class ExternalAudioReader {
|
|||
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) {
|
||||
ensureCache();
|
||||
if (cachedDirUri == null) return false;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import androidx.core.app.NotificationCompat;
|
|||
import androidx.documentfile.provider.DocumentFile;
|
||||
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.ui.activity.MainActivity;
|
||||
|
||||
|
|
@ -156,6 +158,7 @@ public class ExternalAudioWriter {
|
|||
}
|
||||
if (matches) {
|
||||
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
||||
recordDownload(child, existingFile.getUri());
|
||||
ExternalAudioReader.refreshCache();
|
||||
notifyExists(context, fileName);
|
||||
return;
|
||||
|
|
@ -204,6 +207,7 @@ public class ExternalAudioWriter {
|
|||
}
|
||||
|
||||
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
||||
recordDownload(child, targetUri);
|
||||
notifySuccess(context, fileName, child, targetUri);
|
||||
ExternalAudioReader.refreshCache();
|
||||
}
|
||||
|
|
@ -265,6 +269,20 @@ public class ExternalAudioWriter {
|
|||
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) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
|
@ -8,10 +9,13 @@ import androidx.lifecycle.AndroidViewModel;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
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.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
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<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Integer> refreshResult = new MutableLiveData<>();
|
||||
|
||||
public DownloadViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
|
@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel {
|
|||
return viewStack;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getRefreshResult() {
|
||||
return refreshResult;
|
||||
}
|
||||
|
||||
public void initViewStack(DownloadStack level) {
|
||||
ArrayList<DownloadStack> stack = new ArrayList<>();
|
||||
stack.add(level);
|
||||
|
|
@ -60,4 +69,59 @@ public class DownloadViewModel extends AndroidViewModel {
|
|||
stack.remove(stack.size() - 1);
|
||||
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_height="wrap_content"
|
||||
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_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
|
@ -94,6 +94,19 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
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
|
||||
android:id="@+id/downloaded_go_back_image_view"
|
||||
android:layout_width="24dp"
|
||||
|
|
@ -103,6 +116,7 @@
|
|||
android:background="@drawable/ic_arrow_back"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
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" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
|||
|
|
@ -81,6 +81,13 @@
|
|||
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
||||
<string name="download_storage_directory_dialog_neutral_button">Directory</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_play_next">Play next</string>
|
||||
<string name="downloaded_bottom_sheet_remove">Remove</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue