Merge pull request #1 from adrianvic/feature-offline-cache

Add support for downloading to internal storage and fix project build.
This commit is contained in:
Tenkuma 2026-01-21 16:55:58 -03:00 committed by GitHub
commit 9eba857973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 100 additions and 36 deletions

View file

@ -89,7 +89,7 @@ dependencies {
implementation 'com.afollestad:material-cab:2.0.1' implementation 'com.afollestad:material-cab:2.0.1'
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
implementation 'com.github.QuadFlask:colorpicker:ef73ced217' implementation 'com.github.QuadFlask:colorpicker:0.0.15'
implementation 'com.github.codekidX:storage-chooser:2.0.4.4' implementation 'com.github.codekidX:storage-chooser:2.0.4.4'
implementation 'me.relex:circleindicator:2.1.6' implementation 'me.relex:circleindicator:2.1.6'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0' implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'

View file

@ -1,12 +1,19 @@
package com.dkanada.gramophone.activities; package com.dkanada.gramophone.activities;
import android.Manifest;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -20,6 +27,8 @@ import com.dkanada.gramophone.dialogs.preferences.NowPlayingPreferenceDialog;
import com.dkanada.gramophone.activities.base.AbsBaseActivity; import com.dkanada.gramophone.activities.base.AbsBaseActivity;
import com.dkanada.gramophone.util.PreferenceUtil; import com.dkanada.gramophone.util.PreferenceUtil;
import java.io.File;
public class SettingsActivity extends AbsBaseActivity { public class SettingsActivity extends AbsBaseActivity {
private ActivitySettingsBinding binding; private ActivitySettingsBinding binding;
@ -43,6 +52,30 @@ public class SettingsActivity extends AbsBaseActivity {
} }
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private ActivityResultLauncher<Intent> dirPickerLauncher;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dirPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
Intent data = result.getData();
if (data != null) {
Uri uri = data.getData();
requireContext().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(requireContext()).edit();
editor.putString(PreferenceUtil.LOCATION_DOWNLOAD, uri.toString());
editor.apply();
invalidateSettings();
}
}
});
}
@Override @Override
public void onCreatePreferences(Bundle bundle, String s) { public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.pref_library); addPreferencesFromResource(R.xml.pref_library);
@ -93,14 +126,6 @@ public class SettingsActivity extends AbsBaseActivity {
blurAlbumCoverPreference.setEnabled(false); blurAlbumCoverPreference.setEnabled(false);
} }
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
downloadLocationPreference.setEnabled(false);
// stock Android 11 removed the album cover on lock screens
// supported on LineageOS so we might want to add a check at some point
showAlbumCoverPreference.setEnabled(false);
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
// custom notification layouts were removed entirely in Android 12 // custom notification layouts were removed entirely in Android 12
classicNotification.setEnabled(false); classicNotification.setEnabled(false);
@ -117,13 +142,32 @@ public class SettingsActivity extends AbsBaseActivity {
return false; return false;
}); });
// use this to set default state for playback screen and notification style downloadLocationPreference.setOnPreferenceClickListener(preference -> {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); openDirectoryPicker();
return true;
});
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
String downloadLocation = preferences.getString(PreferenceUtil.LOCATION_DOWNLOAD, null);
if (downloadLocation != null) {
Uri uri = Uri.parse(downloadLocation);
File file = new File(uri.getPath());
downloadLocationPreference.setSummary(file.getPath());
} else {
downloadLocationPreference.setSummary(R.string.pref_title_download_location);
}
// use this to set default state for playback screen and notification style
onSharedPreferenceChanged(preferences, PreferenceUtil.NOW_PLAYING_SCREEN); onSharedPreferenceChanged(preferences, PreferenceUtil.NOW_PLAYING_SCREEN);
onSharedPreferenceChanged(preferences, PreferenceUtil.CLASSIC_NOTIFICATION); onSharedPreferenceChanged(preferences, PreferenceUtil.CLASSIC_NOTIFICATION);
} }
private void openDirectoryPicker() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
dirPickerLauncher.launch(intent);
}
@Override @Override
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {

View file

@ -2,7 +2,9 @@ package com.dkanada.gramophone.service;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import androidx.documentfile.provider.DocumentFile;
import com.dkanada.gramophone.App; import com.dkanada.gramophone.App;
import com.dkanada.gramophone.BuildConfig; import com.dkanada.gramophone.BuildConfig;
@ -13,8 +15,6 @@ import com.dkanada.gramophone.util.MusicUtil;
import com.dkanada.gramophone.util.PreferenceUtil; import com.dkanada.gramophone.util.PreferenceUtil;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URL; import java.net.URL;
@ -76,17 +76,28 @@ public class DownloadService extends Service {
URL url = new URL(MusicUtil.getDownloadUri(song)); URL url = new URL(MusicUtil.getDownloadUri(song));
URLConnection connection = url.openConnection(); URLConnection connection = url.openConnection();
String cache = PreferenceUtil.getInstance(App.getInstance()).getLocationCache(); String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
File download = new File(cache, "download/" + song.id); DocumentFile root;
File audio = new File(MusicUtil.getFileUri(song)); if (location.equals(getApplicationContext().getCacheDir().toString())) {
root = DocumentFile.fromFile(new File(location));
} else {
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
}
download.getParentFile().mkdirs(); DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
download.createNewFile(); if (artist == null) {
audio.getParentFile().mkdirs(); artist = root.createDirectory(MusicUtil.ascii(song.artistName));
audio.createNewFile(); }
DocumentFile album = artist.findFile(MusicUtil.ascii(song.albumName));
if (album == null) {
album = artist.createDirectory(MusicUtil.ascii(song.albumName));
}
String fileName = song.discNumber + "." + song.trackNumber + " - " + MusicUtil.ascii(song.title) + "." + song.container;
DocumentFile audio = album.createFile("audio/" + song.container, fileName);
InputStream input = connection.getInputStream(); InputStream input = connection.getInputStream();
OutputStream output = new FileOutputStream(download); OutputStream output = getContentResolver().openOutputStream(audio.getUri());
connection.connect(); connection.connect();
@ -102,17 +113,6 @@ public class DownloadService extends Service {
input.close(); input.close();
output.close(); output.close();
input = new FileInputStream(download);
output = new FileOutputStream(audio);
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
input.close();
output.close();
download.delete();
App.getDatabase().cacheDao().insertCache(new Cache(song)); App.getDatabase().cacheDao().insertCache(new Cache(song));
notification.stop(song); notification.stop(song);
} catch (Exception e) { } catch (Exception e) {

View file

@ -2,6 +2,7 @@ package com.dkanada.gramophone.util;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@ -78,7 +79,12 @@ public class MusicUtil {
} }
public static String getFileUri(Song song) { public static String getFileUri(Song song) {
File root = new File(PreferenceUtil.getInstance(App.getInstance()).getLocationDownload(), "music"); String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
File root = new File(location, "music");
if (!location.equals(App.getInstance().getCacheDir().toString())) {
Uri uri = Uri.parse(location);
return new File(uri.getPath()).getAbsolutePath();
}
String path = "/" + ascii(song.artistName) + "/" + ascii(song.albumName); String path = "/" + ascii(song.artistName) + "/" + ascii(song.albumName);
String name = "/" + song.discNumber + "." + song.trackNumber + " - " + ascii(song.title) + "." + song.container; String name = "/" + song.discNumber + "." + song.trackNumber + " - " + ascii(song.title) + "." + song.container;

View file

@ -84,7 +84,6 @@ public final class PreferenceUtil {
public static final String GAIN_OFFSET = "gain_offset"; public static final String GAIN_OFFSET = "gain_offset";
public static final String LOCATION_DOWNLOAD = "location_download"; public static final String LOCATION_DOWNLOAD = "location_download";
public static final String LOCATION_CACHE = "location_cache";
public static final String IMAGE_CACHE_SIZE = "image_cache_size"; public static final String IMAGE_CACHE_SIZE = "image_cache_size";
public static final String MEDIA_CACHE_SIZE = "media_cache_size"; public static final String MEDIA_CACHE_SIZE = "media_cache_size";
@ -97,7 +96,7 @@ public final class PreferenceUtil {
preferences.edit().putString(GENERAL_THEME, Theme.valueOf(theme.toUpperCase()).toString()).commit(); preferences.edit().putString(GENERAL_THEME, Theme.valueOf(theme.toUpperCase()).toString()).commit();
preferences.edit().putString(IMAGE_CACHE_SIZE, imageSize.substring(0, imageSize.length() - 6)).commit(); preferences.edit().putString(IMAGE_CACHE_SIZE, imageSize.substring(0, imageSize.length() - 6)).commit();
preferences.edit().putString(MEDIA_CACHE_SIZE, mediaSize.substring(0, imageSize.length() - 6)).commit(); preferences.edit().putString(MEDIA_CACHE_SIZE, mediaSize.substring(0, mediaSize.length() - 6)).commit();
} }
}; };
@ -424,7 +423,7 @@ public final class PreferenceUtil {
} }
public final String getLocationCache() { public final String getLocationCache() {
return mPreferences.getString(LOCATION_CACHE, mContext.getCacheDir().toString()); return mContext.getCacheDir().toString();
} }
public final long getImageCacheSize() { public final long getImageCacheSize() {

View file

@ -107,6 +107,7 @@
<string name="pref_header_library">Library</string> <string name="pref_header_library">Library</string>
<string name="pref_header_lock_screen">Lock Screen</string> <string name="pref_header_lock_screen">Lock Screen</string>
<string name="pref_header_notification">Notification</string> <string name="pref_header_notification">Notification</string>
<string name="pref_header_storage">Storage</string>
<string name="pref_title_direct_play_codecs">Direct Play Codecs</string> <string name="pref_title_direct_play_codecs">Direct Play Codecs</string>
<string name="pref_title_categories">Categories</string> <string name="pref_title_categories">Categories</string>
@ -143,6 +144,9 @@
<string name="pref_summary_categories">Configure visibility and order of display categories.</string> <string name="pref_summary_categories">Configure visibility and order of display categories.</string>
<string name="pref_summary_direct_play_codecs">Disable direct play codecs to force transcoding.</string> <string name="pref_summary_direct_play_codecs">Disable direct play codecs to force transcoding.</string>
<string name="pref_summary_gain_adjustment">Adjust the gain of the music player\'s output</string> <string name="pref_summary_gain_adjustment">Adjust the gain of the music player\'s output</string>
<string name="save_lyrics">Save Lyrics</string>
<string name="cached_songs_location">Cached Songs Location</string>
<string name="cached_songs_location_summary">Select where to store cached songs</string>
<string name="delete_action">Delete</string> <string name="delete_action">Delete</string>
<string name="remove_action">Remove</string> <string name="remove_action">Remove</string>

View file

@ -4,7 +4,7 @@
<com.dkanada.gramophone.views.settings.JellyPreferenceCategory android:title="@string/pref_header_cache"> <com.dkanada.gramophone.views.settings.JellyPreferenceCategory android:title="@string/pref_header_cache">
<com.dkanada.gramophone.views.settings.FilePreference <Preference
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:key="location_download" android:key="location_download"
android:title="@string/pref_title_download_location" /> android:title="@string/pref_title_download_location" />

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
app:key="save_lyrics"
app:title="@string/save_lyrics"
app:defaultValue="false" />
<Preference
app:key="cached_songs_location"
app:title="@string/cached_songs_location"
app:summary="@string/cached_songs_location_summary" />
</androidx.preference.PreferenceScreen>