From a4612a127d3e09b02135f4667aa88aace0b62df0 Mon Sep 17 00:00:00 2001 From: aliveoutside <53598473+aliveoutside@users.noreply.github.com> Date: Fri, 5 Aug 2022 19:39:21 +0300 Subject: [PATCH] implement download folder picker --- .../lite/ui/fragments/MediaFragment.java | 191 +++++++++++------- .../ru/vtosters/lite/utils/FileUriUtils.java | 122 +++++++++++ .../ru/vtosters/lite/utils/Preferences.java | 9 + smali/res/xml/preferences_media.xml | 6 + smali/smali_classes2/b/h/g/m/FileUtils.smali | 16 +- 5 files changed, 260 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/ru/vtosters/lite/utils/FileUriUtils.java diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java index eb2087769..1c8055e78 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java @@ -13,8 +13,10 @@ import static ru.vtosters.lite.utils.ThemesUtils.getAlertStyle; import static ru.vtosters.lite.utils.ThemesUtils.getSTextAttr; import static ru.vtosters.lite.utils.ThemesUtils.getTextAttr; +import android.app.Activity; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.res.ColorStateList; import android.os.Bundle; import android.util.Log; @@ -40,41 +42,93 @@ import okhttp3.Request; import ru.vtosters.lite.downloaders.VideoDownloader; import ru.vtosters.lite.music.Scrobbler; import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter; -import ru.vtosters.lite.ui.dialogs.RoundingSeekbarDialog; +import ru.vtosters.lite.utils.FileUriUtils; +import ru.vtosters.lite.utils.Preferences; public class MediaFragment extends MaterialPreferenceToolbarFragment { - public static void download(Context ctx) { - AlertDialog.Builder alertDialog = new AlertDialog.Builder(ctx, getAlertStyle()); - alertDialog.setTitle("Введите ссылку на видео"); + private final int REQUEST_CODE_SET_DOWNLOAD_DIRECTORY = 665; - final EditText input = new EditText(ctx); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT); - input.setLayoutParams(lp); - input.setTextColor(getTextAttr()); - input.setBackgroundTintList(ColorStateList.valueOf(getAccentColor())); - alertDialog.setView(input); - alertDialog.setPositiveButton("Скачать", (dialog, which) -> VideoDownloader.parseVideoLink(input.getText().toString(), ctx)); - var alert = alertDialog.create(); - alert.show(); - alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + addPreferencesFromResource(getIdentifier("preferences_media", "xml")); + prefs(); } - private void logout(Context ctx) { - VkAlertDialog.Builder alertDialog = new VkAlertDialog.Builder(ctx); - alertDialog.setTitle("Вы уверены?"); - alertDialog.setMessage("Вы действительно хотите выйти из аккаунта?"); - alertDialog.setPositiveButton("Да", (dialog, which) -> { - Scrobbler.logout(); + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_SET_DOWNLOAD_DIRECTORY && resultCode == Activity.RESULT_OK) { + var path = data.getData(); + if (path != null) { + var prefs = getPreferences(); + var editor = prefs.edit(); + var actualPath = FileUriUtils.getFullPathFromTreeUri(path, getContext()); + editor.putString("downloads_directory", actualPath); + editor.apply(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + prefs(); + } + + private void prefs() { + findPreference("download_video").setOnPreferenceClickListener(new MediaFragment.download()); + findPreference("clearvideohistory").setOnPreferenceClickListener(preference -> { + deleteVideoHistory(); + return true; }); - alertDialog.setNeutralButton("Нет", (dialog, which) -> { - dialog.cancel(); + findPreference("dateformat").setOnPreferenceChangeListener(new MediaFragment.restart()); + findPreference("lastfm_auth").setOnPreferenceClickListener(preference -> { + if (isLoggedIn()) { + logout(getContext()); + } else { + lastfmAuth(getContext()); + } + return true; + }); + + findPreference("downloads_directory").setSummary( + "Текущая папка: " + Preferences.getDownloadsDir() + ); + findPreference("downloads_directory").setOnPreferenceClickListener(preference -> { + var intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addCategory(Intent.CATEGORY_DEFAULT); + startActivityForResult(intent, REQUEST_CODE_SET_DOWNLOAD_DIRECTORY); + return true; + }); + + if (isLoggedIn()) { + findPreference("lastfm_auth").setSummary("Вы авторизованы как " + Scrobbler.getUserName()); + } else { + findPreference("lastfm_enabled").setEnabled(false); + } + + findPreference("select_photo_search_engine").setOnPreferenceClickListener(preference -> { + var items = Arrays.asList( + new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("yandex", "drawable"), "Yandex"), + new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("google", "drawable"), "Google"), + new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("microsoft", "drawable"), "Bing") + ); + + var alert = new VkAlertDialog.Builder(getActivity()) + .create(); + + var listView = (ListView) LayoutInflater.from(getContext()).inflate(com.vtosters.lite.R.layout.abc_select_dialog_material, null, false); + var adapter = new ImagineArrayAdapter(getContext(), items, i -> { + getPreferences().edit().putInt("search_engine", i).apply(); + alert.dismiss(); + }); + adapter.setSelected(getPreferences().getInt("search_engine", 0)); + listView.setAdapter(adapter); + alert.setView(listView); + alert.show(); + + return true; }); - var alert = alertDialog.create(); - alert.show(); - alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); - alert.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(getResources().getColor(com.vtosters.lite.R.color.red)); } private void lastfmAuth(Context ctx) { @@ -116,6 +170,22 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); } + private void logout(Context ctx) { + VkAlertDialog.Builder alertDialog = new VkAlertDialog.Builder(ctx); + alertDialog.setTitle("Вы уверены?"); + alertDialog.setMessage("Вы действительно хотите выйти из аккаунта?"); + alertDialog.setPositiveButton("Да", (dialog, which) -> { + Scrobbler.logout(); + }); + alertDialog.setNeutralButton("Нет", (dialog, which) -> { + dialog.cancel(); + }); + var alert = alertDialog.create(); + alert.show(); + alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); + alert.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(getResources().getColor(com.vtosters.lite.R.color.red)); + } + public static void deleteVideoHistory() { Thread thread = new Thread(() -> { try { @@ -140,57 +210,22 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { sendToast("История просмотра видео очищена"); } - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - addPreferencesFromResource(getIdentifier("preferences_media", "xml")); - prefs(); - } + public static void download(Context ctx) { + AlertDialog.Builder alertDialog = new AlertDialog.Builder(ctx, getAlertStyle()); + alertDialog.setTitle("Введите ссылку на видео"); - private void prefs() { - findPreference("download_video").setOnPreferenceClickListener(new MediaFragment.download()); - findPreference("clearvideohistory").setOnPreferenceClickListener(preference -> { - deleteVideoHistory(); - return true; - }); - findPreference("dateformat").setOnPreferenceChangeListener(new MediaFragment.restart()); - findPreference("lastfm_auth").setOnPreferenceClickListener(preference -> { - if (isLoggedIn()) { - logout(getContext()); - } else { - lastfmAuth(getContext()); - } - return true; - }); - - if (isLoggedIn()) { - findPreference("lastfm_auth").setSummary("Вы авторизованы как " + Scrobbler.getUserName()); - } else { - findPreference("lastfm_enabled").setEnabled(false); - } - - findPreference("select_photo_search_engine").setOnPreferenceClickListener(preference -> { - var items = Arrays.asList( - new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("yandex", "drawable"), "Yandex"), - new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("google", "drawable"), "Google"), - new ImagineArrayAdapter.ImagineArrayAdapterItem(getIdentifier("microsoft", "drawable"), "Bing") - ); - - var alert = new VkAlertDialog.Builder(getActivity()) - .create(); - - var listView = (ListView) LayoutInflater.from(getContext()).inflate(com.vtosters.lite.R.layout.abc_select_dialog_material, null, false); - var adapter = new ImagineArrayAdapter(getContext(), items, i -> { - getPreferences().edit().putInt("search_engine", i).apply(); - alert.dismiss(); - }); - adapter.setSelected(getPreferences().getInt("search_engine", 0)); - listView.setAdapter(adapter); - alert.setView(listView); - alert.show(); - - return true; - }); + final EditText input = new EditText(ctx); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + input.setLayoutParams(lp); + input.setTextColor(getTextAttr()); + input.setBackgroundTintList(ColorStateList.valueOf(getAccentColor())); + alertDialog.setView(input); + alertDialog.setPositiveButton("Скачать", (dialog, which) -> VideoDownloader.parseVideoLink(input.getText().toString(), ctx)); + var alert = alertDialog.create(); + alert.show(); + alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); } private class download implements Preference.OnPreferenceClickListener { diff --git a/app/src/main/java/ru/vtosters/lite/utils/FileUriUtils.java b/app/src/main/java/ru/vtosters/lite/utils/FileUriUtils.java new file mode 100644 index 000000000..f808ffcc9 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/utils/FileUriUtils.java @@ -0,0 +1,122 @@ +package ru.vtosters.lite.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; +import android.provider.DocumentsContract; + +import androidx.annotation.Nullable; + +import java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.List; + +// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691 + +public final class FileUriUtils { + + private static final String PRIMARY_VOLUME_NAME = "primary"; + + @Nullable + public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { + if (treeUri == null) return null; + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con); + if (volumePath == null) return File.separator; + if (volumePath.endsWith(File.separator)) + volumePath = volumePath.substring(0, volumePath.length() - 1); + + String documentPath = getDocumentPathFromTreeUri(treeUri); + if (documentPath.endsWith(File.separator)) + documentPath = documentPath.substring(0, documentPath.length() - 1); + + if (documentPath.length() > 0) { + if (documentPath.startsWith(File.separator)) + return volumePath + documentPath; + else + return volumePath + File.separator + documentPath; + } + else return volumePath; + } + + + private static String getVolumePath(final String volumeId, Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + return getVolumePathForAndroid11AndAbove(volumeId, context); + else + return getVolumePathBeforeAndroid11(volumeId, context); + } + + + private static String getVolumePathBeforeAndroid11(final String volumeId, Context context){ + try { + StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + Class storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); + Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); + Method getUuid = storageVolumeClazz.getMethod("getUuid"); + Method getPath = storageVolumeClazz.getMethod("getPath"); + Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); + Object result = getVolumeList.invoke(mStorageManager); + + final int length = Array.getLength(result); + for (int i = 0; i < length; i++) { + Object storageVolumeElement = Array.get(result, i); + String uuid = (String) getUuid.invoke(storageVolumeElement); + Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); + + if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) // primary volume? + return (String) getPath.invoke(storageVolumeElement); + + if (uuid != null && uuid.equals(volumeId)) // other volumes? + return (String) getPath.invoke(storageVolumeElement); + } + // not found. + return null; + } catch (Exception ex) { + return null; + } + } + + @TargetApi(Build.VERSION_CODES.R) + private static String getVolumePathForAndroid11AndAbove(final String volumeId, Context context) { + try { + StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + List storageVolumes = mStorageManager.getStorageVolumes(); + for (StorageVolume storageVolume : storageVolumes) { + // primary volume? + if (storageVolume.isPrimary() && PRIMARY_VOLUME_NAME.equals(volumeId)) + return storageVolume.getDirectory().getPath(); + + // other volumes? + String uuid = storageVolume.getUuid(); + if (uuid != null && uuid.equals(volumeId)) + return storageVolume.getDirectory().getPath(); + + } + // not found. + return null; + } catch (Exception ex) { + return null; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getVolumeIdFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if (split.length > 0) return split[0]; + else return null; + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getDocumentPathFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if ((split.length >= 2) && (split[1] != null)) return split[1]; + else return File.separator; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/utils/Preferences.java b/app/src/main/java/ru/vtosters/lite/utils/Preferences.java index f535b2cf3..8b02a202f 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/Preferences.java +++ b/app/src/main/java/ru/vtosters/lite/utils/Preferences.java @@ -3,6 +3,7 @@ package ru.vtosters.lite.utils; import static java.lang.Long.MAX_VALUE; import static ru.vtosters.lite.utils.AndroidUtils.edit; import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext; +import static ru.vtosters.lite.utils.AndroidUtils.getPreferences; import static ru.vtosters.lite.utils.AndroidUtils.getPrefsValue; import static ru.vtosters.lite.utils.CacheUtils.getInstance; import static ru.vtosters.lite.utils.DeletedMessagesHandler.reloadMessagesList; @@ -17,11 +18,13 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.Environment; import android.preference.PreferenceManager; import com.vtosters.lite.data.Users; import com.vtosters.lite.fragments.SettingsListFragment; +import java.io.File; import java.security.NoSuchAlgorithmException; import ru.vtosters.lite.ui.fragments.VTSettings; @@ -383,4 +386,10 @@ public class Preferences { } return origquality; } + + public static File getDownloadsDir() { + return new File(getPreferences().getString( + "downloads_directory", + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath())); + } } diff --git a/smali/res/xml/preferences_media.xml b/smali/res/xml/preferences_media.xml index 4260ce02a..2bcf4056a 100644 --- a/smali/res/xml/preferences_media.xml +++ b/smali/res/xml/preferences_media.xml @@ -24,6 +24,12 @@ android:summary="Показывать результаты в опросах без участия в них" android:title="Результаты опросов" /> + + + getVKDir()Ljava/io/File; +# invoke-static {}, Lb/h/g/m/FileUtils;->getVKDir()Ljava/io/File; +# +# move-result-object v0 +# +# const-string v1, "Downloads" +# +# invoke-static {v0, v1}, Lb/h/g/m/FileUtils;->a(Ljava/io/File;Ljava/lang/String;)Ljava/io/File; +# +# move-result-object v0 - move-result-object v0 - - const-string v1, "Downloads" - - invoke-static {v0, v1}, Lb/h/g/m/FileUtils;->a(Ljava/io/File;Ljava/lang/String;)Ljava/io/File; + invoke-static {}, Lru/vtosters/lite/utils/Preferences;->getDownloadsDir()Ljava/io/File; move-result-object v0