diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java index 628e9dd6..a2d49f6b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java @@ -15,6 +15,9 @@ public interface DownloadDao { @Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC") LiveData> getAll(); + @Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC") + List 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 ids); + @Query("DELETE FROM download") void deleteAll(); } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java index c71d2f8c..1d8c935a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java @@ -18,6 +18,20 @@ public class DownloadRepository { return downloadDao.getAll(); } + public List 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 downloads; + + public GetAllDownloadsThreadSafe(DownloadDao downloadDao) { + this.downloadDao = downloadDao; + } + + @Override + public void run() { + downloads = downloadDao.getAllSync(); + } + + public List 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 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 ids; + + public DeleteMultipleThreadSafe(DownloadDao downloadDao, List ids) { + this.downloadDao = downloadDao; + this.ids = ids; + } + + @Override + public void run() { + downloadDao.deleteByIds(ids); + } + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java index 7d4a076a..be2b3eb0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -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 songs) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java index 42792e31..afabcc82 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java @@ -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; diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java index 84708546..df35e169 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java @@ -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) diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java index 3059eb09..6f69cee1 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java @@ -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> downloadedTrackSample = new MutableLiveData<>(null); private final MutableLiveData> viewStack = new MutableLiveData<>(null); + private final MutableLiveData refreshResult = new MutableLiveData<>(); public DownloadViewModel(@NonNull Application application) { super(application); @@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel { return viewStack; } + public LiveData getRefreshResult() { + return refreshResult; + } + public void initViewStack(DownloadStack level) { ArrayList 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 downloads = downloadRepository.getAllDownloads(); + if (downloads == null || downloads.isEmpty()) { + refreshResult.postValue(0); + return; + } + + ArrayList 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 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(); + } } diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..f3dcb53a --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_download.xml b/app/src/main/res/layout/fragment_download.xml index 0a454508..68d3f84b 100644 --- a/app/src/main/res/layout/fragment_download.xml +++ b/app/src/main/res/layout/fragment_download.xml @@ -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"/> + + Internal Directory Downloads + Set a download folder to refresh your downloads. + No missing downloads found. + + Removed %d missing download. + Removed %d missing downloads. + + Refresh downloaded items Add to queue Play next Remove