diff options
22 files changed, 851 insertions, 55 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13339b1b..690f1026 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.SET_WALLPAPER" /> <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" /> - <uses-permission android:name="android.permission.VIBRATE"/> + <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> @@ -24,7 +24,13 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:replace="android:label"> - <activity android:name=".Activity.ViewRedditGalleryActivity" + <service + android:name=".Service.DownloadImageService" + android:enabled="true" + android:exported="false"></service> + + <activity + android:name=".Activity.ViewRedditGalleryActivity" android:configChanges="orientation|screenSize|layoutDirection" android:parentActivityName=".Activity.MainActivity" android:theme="@style/AppTheme.Draggable" /> diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/API/DownloadVideo.java b/app/src/main/java/ml/docilealligator/infinityforreddit/API/DownloadFile.java index e3d9d238..10a8dda4 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/API/DownloadVideo.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/API/DownloadFile.java @@ -6,7 +6,7 @@ import retrofit2.http.GET; import retrofit2.http.Streaming; import retrofit2.http.Url; -public interface DownloadVideo { +public interface DownloadFile { @Streaming @GET() Call<ResponseBody> downloadFile(@Url String fileUrl); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java index fe1a0e9c..471a4ea1 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java @@ -1,6 +1,5 @@ package ml.docilealligator.infinityforreddit.Activity; -import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -22,8 +21,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import com.bumptech.glide.Glide; @@ -52,8 +49,8 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; +import ml.docilealligator.infinityforreddit.AsyncTask.SaveBitmapImageToFileAsyncTask; import ml.docilealligator.infinityforreddit.AsyncTask.SaveGIFToFileAsyncTask; -import ml.docilealligator.infinityforreddit.AsyncTask.SaveImageToFileAsyncTask; import ml.docilealligator.infinityforreddit.BottomSheetFragment.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.Font.ContentFontFamily; @@ -66,6 +63,7 @@ import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.MediaDownloader; import ml.docilealligator.infinityforreddit.MediaDownloaderImpl; import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.Service.DownloadImageService; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.WallpaperSetter; @@ -279,29 +277,16 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa finish(); return true; case R.id.action_download_view_image_or_gif_activity: - if (isDownloading) { - return false; - } - - isDownloading = true; - - if (Build.VERSION.SDK_INT >= 23) { - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - // Permission is not granted - // No explanation needed; request the permission - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE); - } else { - // Permission has already been granted - mediaDownloader.download(mImageUrl, mImageFileName, this); - } + Intent intent = new Intent(this, DownloadImageService.class); + intent.putExtra(DownloadImageService.EXTRA_URL, mImageUrl); + intent.putExtra(DownloadImageService.EXTRA_IS_GIF, isGif); + intent.putExtra(DownloadImageService.EXTRA_FILE_NAME, mImageFileName); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); } else { - mediaDownloader.download(mImageUrl, mImageFileName, this); + startService(intent); } + Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); return true; case R.id.action_share_view_image_or_gif_activity: @@ -343,8 +328,8 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { if (getExternalCacheDir() != null) { Toast.makeText(ViewImageOrGifActivity.this, R.string.save_image_first, Toast.LENGTH_SHORT).show(); - new SaveImageToFileAsyncTask(resource, getExternalCacheDir().getPath(), mImageFileName, - new SaveImageToFileAsyncTask.SaveImageToFileAsyncTaskListener() { + new SaveBitmapImageToFileAsyncTask(resource, getExternalCacheDir().getPath(), mImageFileName, + new SaveBitmapImageToFileAsyncTask.SaveBitmapImageToFileAsyncTaskListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(ViewImageOrGifActivity.this, diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java index 35705736..0d3e6aec 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java @@ -13,6 +13,7 @@ import ml.docilealligator.infinityforreddit.Activity.EditCommentActivity; import ml.docilealligator.infinityforreddit.Activity.EditMultiRedditActivity; import ml.docilealligator.infinityforreddit.Activity.EditPostActivity; import ml.docilealligator.infinityforreddit.Activity.FilteredThingActivity; +import ml.docilealligator.infinityforreddit.Activity.InboxActivity; import ml.docilealligator.infinityforreddit.Activity.LinkResolverActivity; import ml.docilealligator.infinityforreddit.Activity.LoginActivity; import ml.docilealligator.infinityforreddit.Activity.MainActivity; @@ -34,7 +35,6 @@ import ml.docilealligator.infinityforreddit.Activity.SubscribedThingListingActiv import ml.docilealligator.infinityforreddit.Activity.ThemePreviewActivity; import ml.docilealligator.infinityforreddit.Activity.ViewImageOrGifActivity; import ml.docilealligator.infinityforreddit.Activity.ViewImgurMediaActivity; -import ml.docilealligator.infinityforreddit.Activity.InboxActivity; import ml.docilealligator.infinityforreddit.Activity.ViewMultiRedditDetailActivity; import ml.docilealligator.infinityforreddit.Activity.ViewPostDetailActivity; import ml.docilealligator.infinityforreddit.Activity.ViewPrivateMessagesActivity; @@ -42,8 +42,8 @@ import ml.docilealligator.infinityforreddit.Activity.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.Activity.ViewSubredditDetailActivity; import ml.docilealligator.infinityforreddit.Activity.ViewUserDetailActivity; import ml.docilealligator.infinityforreddit.Activity.ViewVideoActivity; -import ml.docilealligator.infinityforreddit.Fragment.CommentsListingFragment; import ml.docilealligator.infinityforreddit.BottomSheetFragment.FlairBottomSheetFragment; +import ml.docilealligator.infinityforreddit.Fragment.CommentsListingFragment; import ml.docilealligator.infinityforreddit.Fragment.FollowedUsersListingFragment; import ml.docilealligator.infinityforreddit.Fragment.InboxFragment; import ml.docilealligator.infinityforreddit.Fragment.MultiRedditListingFragment; @@ -54,10 +54,12 @@ import ml.docilealligator.infinityforreddit.Fragment.SubscribedSubredditsListing import ml.docilealligator.infinityforreddit.Fragment.UserListingFragment; import ml.docilealligator.infinityforreddit.Fragment.ViewImgurVideoFragment; import ml.docilealligator.infinityforreddit.Fragment.ViewRedditGalleryVideoFragment; +import ml.docilealligator.infinityforreddit.Service.DownloadImageService; import ml.docilealligator.infinityforreddit.Service.DownloadVideoService; import ml.docilealligator.infinityforreddit.Service.SubmitPostService; import ml.docilealligator.infinityforreddit.Settings.AdvancedPreferenceFragment; import ml.docilealligator.infinityforreddit.Settings.CustomizeMainPageTabsFragment; +import ml.docilealligator.infinityforreddit.Settings.DownloadLocationPreferenceFragment; import ml.docilealligator.infinityforreddit.Settings.GesturesAndButtonsPreferenceFragment; import ml.docilealligator.infinityforreddit.Settings.MainPreferenceFragment; import ml.docilealligator.infinityforreddit.Settings.NotificationPreferenceFragment; @@ -188,4 +190,8 @@ public interface AppComponent { void inject(ViewRedditGalleryVideoFragment viewRedditGalleryVideoFragment); void inject(CustomizeMainPageTabsFragment customizeMainPageTabsFragment); + + void inject(DownloadImageService downloadImageService); + + void inject(DownloadLocationPreferenceFragment downloadLocationPreferenceFragment); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java index 44c86977..a1dd3517 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AppModule.java @@ -91,7 +91,7 @@ class AppModule { } @Provides - @Named("download_reddit_video") + @Named("download_media") @Singleton Retrofit provideDownloadRedditVideoRetrofit() { return new Retrofit.Builder() diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AsyncTask/SaveImageToFileAsyncTask.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AsyncTask/SaveBitmapImageToFileAsyncTask.java index 65c1c199..f70a87be 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AsyncTask/SaveImageToFileAsyncTask.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AsyncTask/SaveBitmapImageToFileAsyncTask.java @@ -8,23 +8,23 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -public class SaveImageToFileAsyncTask extends AsyncTask<Void, Void, Void> { +public class SaveBitmapImageToFileAsyncTask extends AsyncTask<Void, Void, Void> { private Bitmap resource; private String cacheDirPath; private String fileName; - private SaveImageToFileAsyncTaskListener saveImageToFileAsyncTaskListener; + private SaveBitmapImageToFileAsyncTaskListener saveBitmapImageToFileAsyncTaskListener; private boolean saveSuccess = true; private File imageFile; - public SaveImageToFileAsyncTask(Bitmap resource, String cacheDirPath, String fileName, - SaveImageToFileAsyncTaskListener saveImageToFileAsyncTaskListener) { + public SaveBitmapImageToFileAsyncTask(Bitmap resource, String cacheDirPath, String fileName, + SaveBitmapImageToFileAsyncTaskListener saveBitmapImageToFileAsyncTaskListener) { this.resource = resource; this.cacheDirPath = cacheDirPath; this.fileName = fileName; - this.saveImageToFileAsyncTaskListener = saveImageToFileAsyncTaskListener; + this.saveBitmapImageToFileAsyncTaskListener = saveBitmapImageToFileAsyncTaskListener; } - public interface SaveImageToFileAsyncTaskListener { + public interface SaveBitmapImageToFileAsyncTaskListener { void saveSuccess(File imageFile); void saveFailed(); } @@ -47,9 +47,9 @@ public class SaveImageToFileAsyncTask extends AsyncTask<Void, Void, Void> { protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (saveSuccess) { - saveImageToFileAsyncTaskListener.saveSuccess(imageFile); + saveBitmapImageToFileAsyncTaskListener.saveSuccess(imageFile); } else { - saveImageToFileAsyncTaskListener.saveFailed(); + saveBitmapImageToFileAsyncTaskListener.saveFailed(); } } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java new file mode 100644 index 00000000..61e87c1f --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java @@ -0,0 +1,9 @@ +package ml.docilealligator.infinityforreddit.Event; + +public class DownloadImageOrGifEvent { + public boolean isSuccessful; + + public DownloadImageOrGifEvent(boolean isSuccessful) { + this.isSuccessful = isSuccessful; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java index b29ccf2d..91d25aed 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java @@ -41,7 +41,7 @@ import java.io.File; import butterknife.BindView; import butterknife.ButterKnife; import ml.docilealligator.infinityforreddit.Activity.ViewImgurMediaActivity; -import ml.docilealligator.infinityforreddit.AsyncTask.SaveImageToFileAsyncTask; +import ml.docilealligator.infinityforreddit.AsyncTask.SaveBitmapImageToFileAsyncTask; import ml.docilealligator.infinityforreddit.BottomSheetFragment.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.ImgurMedia; @@ -183,9 +183,9 @@ public class ViewImgurImageFragment extends Fragment { public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { if (activity.getExternalCacheDir() != null) { Toast.makeText(activity, R.string.save_image_first, Toast.LENGTH_SHORT).show(); - new SaveImageToFileAsyncTask(resource, activity.getExternalCacheDir().getPath(), + new SaveBitmapImageToFileAsyncTask(resource, activity.getExternalCacheDir().getPath(), imgurMedia.getFileName(), - new SaveImageToFileAsyncTask.SaveImageToFileAsyncTaskListener() { + new SaveBitmapImageToFileAsyncTask.SaveBitmapImageToFileAsyncTaskListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(activity, diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java index a3674051..e8954f41 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java @@ -47,7 +47,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import ml.docilealligator.infinityforreddit.Activity.ViewRedditGalleryActivity; import ml.docilealligator.infinityforreddit.AsyncTask.SaveGIFToFileAsyncTask; -import ml.docilealligator.infinityforreddit.AsyncTask.SaveImageToFileAsyncTask; +import ml.docilealligator.infinityforreddit.AsyncTask.SaveBitmapImageToFileAsyncTask; import ml.docilealligator.infinityforreddit.BottomSheetFragment.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.MediaDownloader; @@ -277,9 +277,9 @@ public class ViewRedditGalleryImageOrGifFragment extends Fragment { public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { if (activity.getExternalCacheDir() != null) { Toast.makeText(activity, R.string.save_image_first, Toast.LENGTH_SHORT).show(); - new SaveImageToFileAsyncTask(resource, activity.getExternalCacheDir().getPath(), + new SaveBitmapImageToFileAsyncTask(resource, activity.getExternalCacheDir().getPath(), media.fileName, - new SaveImageToFileAsyncTask.SaveImageToFileAsyncTaskListener() { + new SaveBitmapImageToFileAsyncTask.SaveBitmapImageToFileAsyncTaskListener() { @Override public void saveSuccess(File imageFile) { Uri uri = FileProvider.getUriForFile(activity, diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MediaDownloaderImpl.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MediaDownloaderImpl.java index fd802284..892f370f 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/MediaDownloaderImpl.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MediaDownloaderImpl.java @@ -7,6 +7,8 @@ import android.os.Build; import android.os.Environment; import android.widget.Toast; +import androidx.documentfile.provider.DocumentFile; + import java.io.File; public class MediaDownloaderImpl implements MediaDownloader { @@ -21,7 +23,10 @@ public class MediaDownloaderImpl implements MediaDownloader { //Android Q support if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, fileName); + //ctx.getContentResolver().takePersistableUriPermission(Uri.parse("content://com.android.providers.downloads.documents/tree/downloads"), Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + request.setDestinationInExternalPublicDir(DocumentFile.fromTreeUri(ctx, Uri.parse("content://com.android.providers.downloads.documents/tree/downloads")).toString(), fileName); + //request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, fileName); + //request.setDestinationUri(Uri.parse(Paths.get("content://com.android.providers.downloads.documents/tree/downloads", fileName).toString())); } else { String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString(); File directory = new File(path + "/Infinity/"); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/NotificationUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/NotificationUtils.java index 8fe4b33e..c0935772 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/NotificationUtils.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/NotificationUtils.java @@ -15,9 +15,15 @@ public class NotificationUtils { public static final String CHANNEL_DOWNLOAD_REDDIT_VIDEO = "Download Reddit Video"; public static final String CHANNEL_ID_DOWNLOAD_VIDEO = "download_video"; public static final String CHANNEL_DOWNLOAD_VIDEO = "Download Video"; + public static final String CHANNEL_ID_DOWNLOAD_IMAGE = "download_image"; + public static final String CHANNEL_DOWNLOAD_IMAGE = "Download Image"; + public static final String CHANNEL_ID_DOWNLOAD_GIF = "download_gif"; + public static final String CHANNEL_DOWNLOAD_GIF = "Download Gif"; public static final int SUBMIT_POST_SERVICE_NOTIFICATION_ID = 10000; public static final int DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID = 20000; public static final int DOWNLOAD_VIDEO_NOTIFICATION_ID = 30000; + public static final int DOWNLOAD_IMAGE_NOTIFICATION_ID = 40000; + public static final int DOWNLOAD_GIF_NOTIFICATION_ID = 50000; private static final int SUMMARY_BASE_ID_UNREAD_MESSAGE = 0; private static final int NOTIFICATION_BASE_ID_UNREAD_MESSAGE = 1; diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java new file mode 100644 index 00000000..06d7449e --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java @@ -0,0 +1,345 @@ +package ml.docilealligator.infinityforreddit.Service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.os.IBinder; +import android.provider.MediaStore; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.documentfile.provider.DocumentFile; + +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.inject.Inject; +import javax.inject.Named; + +import ml.docilealligator.infinityforreddit.API.DownloadFile; +import ml.docilealligator.infinityforreddit.CustomTheme.CustomThemeWrapper; +import ml.docilealligator.infinityforreddit.Event.DownloadImageOrGifEvent; +import ml.docilealligator.infinityforreddit.Infinity; +import ml.docilealligator.infinityforreddit.NotificationUtils; +import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; + +import static android.os.Environment.getExternalStoragePublicDirectory; + +public class DownloadImageService extends Service { + public static final String EXTRA_URL = "EU"; + public static final String EXTRA_FILE_NAME = "EFN"; + public static final String EXTRA_IS_GIF = "EIG"; + + private static final int NO_ERROR = -1; + private static final int ERROR_CANNOT_GET_DESTINATION_DIRECTORY = 0; + private static final int ERROR_FILE_CANNOT_DOWNLOAD = 1; + private static final int ERROR_FILE_CANNOT_SAVE = 2; + @Inject + @Named("download_media") + Retrofit retrofit; + @Inject + @Named("default") + SharedPreferences mSharedPreferences; + @Inject + CustomThemeWrapper mCustomThemeWrapper; + private boolean isGif; + + public DownloadImageService() { + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + ((Infinity) getApplication()).getAppComponent().inject(this); + + String fileUrl = intent.getStringExtra(EXTRA_URL); + String fileName; + fileName = intent.getStringExtra(EXTRA_FILE_NAME); + isGif = intent.getBooleanExtra(EXTRA_IS_GIF, false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel; + serviceChannel = new NotificationChannel( + isGif ? NotificationUtils.CHANNEL_ID_DOWNLOAD_GIF : NotificationUtils.CHANNEL_ID_DOWNLOAD_IMAGE, + isGif ? NotificationUtils.CHANNEL_DOWNLOAD_GIF : NotificationUtils.CHANNEL_DOWNLOAD_IMAGE, + NotificationManager.IMPORTANCE_LOW + ); + + NotificationManagerCompat manager = NotificationManagerCompat.from(this); + manager.createNotificationChannel(serviceChannel); + } + + startForeground( + isGif ? NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID : NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID, + createNotification(isGif ? R.string.downloading_gif : R.string.downloading_image, fileName, null) + ); + + retrofit.create(DownloadFile.class).downloadFile(fileUrl).enqueue(new Callback<ResponseBody>() { + @Override + public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) { + if (response.isSuccessful() && response.body() != null) { + String destinationFileDirectory = isGif ? mSharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, "") : + mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); + String destinationFileUriString; + boolean isDefaultDestination; + if (destinationFileDirectory.equals("")) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + File directory = getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + if (directory != null) { + String directoryPath = directory.getAbsolutePath() + "/Infinity/"; + File infinityDir = new File(directoryPath); + if (!infinityDir.exists() && !infinityDir.mkdir()) { + downloadFinished(null, fileName, ERROR_CANNOT_GET_DESTINATION_DIRECTORY); + return; + } + destinationFileUriString = directoryPath + fileName; + } else { + downloadFinished(null, fileName, ERROR_CANNOT_GET_DESTINATION_DIRECTORY); + return; + } + } else { + destinationFileUriString = Environment.DIRECTORY_PICTURES + "/Infinity/"; + } + isDefaultDestination = true; + } else { + isDefaultDestination = false; + DocumentFile picFile = DocumentFile.fromTreeUri(DownloadImageService.this, Uri.parse(destinationFileDirectory)).createFile("image/*", fileName); + if (picFile == null) { + downloadFinished(null, fileName, ERROR_CANNOT_GET_DESTINATION_DIRECTORY); + return; + } + destinationFileUriString = picFile.getUri().toString(); + } + + new SaveImageOrGifAndCopyToExternalStorageAsyncTask(response.body(), isGif, + isDefaultDestination, fileName, destinationFileUriString, getContentResolver(), + new SaveImageOrGifAndCopyToExternalStorageAsyncTask.SaveImageOrGifAndCopyToExternalStorageAsyncTaskListener() { + @Override + public void finished(Uri destinationFileUri, int errorCode) { + downloadFinished(destinationFileUri, fileName, errorCode); + } + + @Override + public void updateProgressNotification(int stringResId) { + updateNotification(stringResId, fileName, null); + } + }).execute(); + } + } + + @Override + public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) { + downloadFinished(null, fileName, ERROR_FILE_CANNOT_DOWNLOAD); + } + }); + return super.onStartCommand(intent, flags, startId); + } + + private Notification createNotification(int stringResId, String fileName, PendingIntent pendingIntent) { + NotificationCompat.Builder builder; + builder = new NotificationCompat.Builder(this, isGif ? NotificationUtils.CHANNEL_ID_DOWNLOAD_GIF : NotificationUtils.CHANNEL_ID_DOWNLOAD_IMAGE); + builder.setContentTitle(fileName).setContentText(getString(stringResId)); + if (pendingIntent != null) { + builder.setContentIntent(pendingIntent); + } + return builder.setSmallIcon(R.drawable.ic_notification) + .setColor(mCustomThemeWrapper.getColorPrimaryLightTheme()) + .build(); + } + + private void updateNotification(int stringResId, String fileName, PendingIntent pendingIntent) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.notify(isGif ? NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID : NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID, + createNotification(stringResId, fileName, pendingIntent)); + } + } + + private void downloadFinished(Uri destinationFileUri, String fileName, int errorCode) { + if (errorCode != NO_ERROR) { + switch (errorCode) { + case ERROR_CANNOT_GET_DESTINATION_DIRECTORY: + updateNotification(R.string.downloading_image_or_gif_failed_cannot_get_destination_directory, fileName, null); + break; + case ERROR_FILE_CANNOT_DOWNLOAD: + updateNotification(isGif ? R.string.downloading_gif_failed_cannot_download_gif : R.string.downloading_image_failed_cannot_download_image, fileName, null); + break; + case ERROR_FILE_CANNOT_SAVE: + updateNotification(isGif ? R.string.downloading_gif_failed_cannot_save_gif : R.string.downloading_image_failed_cannot_save_image, fileName, null); + break; + } + EventBus.getDefault().post(new DownloadImageOrGifEvent(false)); + } else { + MediaScannerConnection.scanFile( + this, new String[]{destinationFileUri.toString()}, null, + (path, uri) -> { + Intent intent = new Intent(); + intent.setAction(android.content.Intent.ACTION_VIEW); + intent.setDataAndType(destinationFileUri, "image/*"); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + updateNotification(isGif ? R.string.downloading_gif_finished : R.string.downloading_image_finished, fileName, pendingIntent); + EventBus.getDefault().post(new DownloadImageOrGifEvent(true)); + } + ); + } + stopForeground(false); + } + + private static class SaveImageOrGifAndCopyToExternalStorageAsyncTask extends AsyncTask<Void, Integer, Void> { + + private ResponseBody response; + private boolean isGif; + private boolean isDefaultDestination; + private String destinationFileName; + @NonNull + private String destinationFileUriString; + private ContentResolver contentResolver; + private SaveImageOrGifAndCopyToExternalStorageAsyncTaskListener saveImageOrGifAndCopyToExternalStorageAsyncTaskListener; + private int errorCode = NO_ERROR; + + interface SaveImageOrGifAndCopyToExternalStorageAsyncTaskListener { + void finished(Uri destinationFileUri, int errorCode); + void updateProgressNotification(int stringResId); + } + + public SaveImageOrGifAndCopyToExternalStorageAsyncTask(ResponseBody response, boolean isGif, + boolean isDefaultDestination, + String destinationFileName, + @NonNull String destinationFileUriString, + ContentResolver contentResolver, + SaveImageOrGifAndCopyToExternalStorageAsyncTaskListener saveImageOrGifAndCopyToExternalStorageAsyncTaskListener) { + this.response = response; + this.isGif = isGif; + this.isDefaultDestination = isDefaultDestination; + this.destinationFileName = destinationFileName; + this.destinationFileUriString = destinationFileUriString; + this.contentResolver = contentResolver; + this.saveImageOrGifAndCopyToExternalStorageAsyncTaskListener = saveImageOrGifAndCopyToExternalStorageAsyncTaskListener; + } + + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + saveImageOrGifAndCopyToExternalStorageAsyncTaskListener.updateProgressNotification(values[0]); + } + + @Override + protected Void doInBackground(Void... voids) { + publishProgress(isGif ? R.string.downloading_gif_save_gif : R.string.downloading_image_save_image); + try { + writeResponseBodyToDisk(response); + } catch (IOException e) { + errorCode = ERROR_FILE_CANNOT_SAVE; + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + saveImageOrGifAndCopyToExternalStorageAsyncTaskListener.finished(Uri.parse(destinationFileUriString), errorCode); + } + + private void writeResponseBodyToDisk(ResponseBody body) throws IOException { + if (isDefaultDestination) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + InputStream inputStream = body.byteStream(); + OutputStream outputStream = new FileOutputStream(destinationFileUriString); + byte[] fileReader = new byte[4096]; + + long fileSize = body.contentLength(); + long fileSizeDownloaded = 0; + + while (true) { + int read = inputStream.read(fileReader); + + if (read == -1) { + break; + } + + outputStream.write(fileReader, 0, read); + + fileSizeDownloaded += read; + } + + outputStream.flush(); + } else { + String relativeLocation = Environment.DIRECTORY_PICTURES + "/Infinity/"; + + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, destinationFileName); + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/*"); + contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 1); + + final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + Uri uri = contentResolver.insert(contentUri, contentValues); + + if (uri == null) { + throw new IOException("Failed to create new MediaStore record."); + } + + OutputStream stream = contentResolver.openOutputStream(uri); + + if (stream == null) { + throw new IOException("Failed to get output stream."); + } + + InputStream in = body.byteStream(); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + stream.write(buf, 0, len); + } + contentValues.clear(); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 0); + contentResolver.update(uri, contentValues, null, null); + destinationFileUriString = uri.toString(); + } + } else { + try (OutputStream stream = contentResolver.openOutputStream(Uri.parse(destinationFileUriString))) { + if (stream == null) { + throw new IOException("Failed to get output stream."); + } + + InputStream in = body.byteStream(); + + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + stream.write(buf, 0, len); + } + destinationFileUriString = destinationFileUriString.replaceAll("%3A", ":").replaceAll("%2F", "/"); + } + } + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java index 1f39c604..4926785f 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java @@ -39,7 +39,7 @@ import java.nio.ByteBuffer; import javax.inject.Inject; import javax.inject.Named; -import ml.docilealligator.infinityforreddit.API.DownloadVideo; +import ml.docilealligator.infinityforreddit.API.DownloadFile; import ml.docilealligator.infinityforreddit.CustomTheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.Event.DownloadRedditVideoEvent; import ml.docilealligator.infinityforreddit.Infinity; @@ -72,7 +72,7 @@ public class DownloadVideoService extends Service { private boolean isRedditVideo; @Inject - @Named("download_reddit_video") + @Named("download_media") Retrofit retrofit; @Inject CustomThemeWrapper mCustomThemeWrapper; @@ -132,19 +132,19 @@ public class DownloadVideoService extends Service { ); } - DownloadVideo downloadVideo = retrofit.create(DownloadVideo.class); + DownloadFile downloadFile = retrofit.create(DownloadFile.class); File directory = getExternalCacheDir(); String destinationFileName = fileName + ".mp4"; if (directory != null) { String directoryPath = directory.getAbsolutePath() + "/"; - downloadVideo.downloadFile(videoUrl).enqueue(new Callback<ResponseBody>() { + downloadFile.downloadFile(videoUrl).enqueue(new Callback<ResponseBody>() { @Override public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> videoResponse) { if (videoResponse.isSuccessful() && videoResponse.body() != null) { if (isRedditVideo) { updateNotification(R.string.downloading_reddit_video_audio_track, destinationFileName, null); - downloadVideo.downloadFile(audioUrl).enqueue(new Callback<ResponseBody>() { + downloadFile.downloadFile(audioUrl).enqueue(new Callback<ResponseBody>() { @Override public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> audioResponse) { if (audioResponse.isSuccessful() && audioResponse.body() != null) { @@ -521,10 +521,12 @@ public class DownloadVideoService extends Service { @RequiresApi(api = Build.VERSION_CODES.Q) private void copyFileQ(File src, String outputFileName) throws IOException { + String relativeLocation = Environment.DIRECTORY_MOVIES + "/Infinity/"; ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, outputFileName); + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/*"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation); contentValues.put(MediaStore.Video.Media.IS_PENDING, 1); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Settings/DownloadLocationPreferenceFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Settings/DownloadLocationPreferenceFragment.java new file mode 100644 index 00000000..50c436eb --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Settings/DownloadLocationPreferenceFragment.java @@ -0,0 +1,115 @@ +package ml.docilealligator.infinityforreddit.Settings; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import javax.inject.Inject; +import javax.inject.Named; + +import ml.docilealligator.infinityforreddit.Infinity; +import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; + +import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; + +public class DownloadLocationPreferenceFragment extends PreferenceFragmentCompat { + private static final int IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE = 10; + private static final int GIF_DOWNLOAD_LOCATION_REQUEST_CODE = 11; + private static final int VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE = 12; + + Preference imageDownloadLocationPreference; + Preference gifDownloadLocationPreference; + Preference videoDownloadLocationPreference; + private Activity activity; + @Inject + @Named("default") + SharedPreferences sharedPreferences; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + ((Infinity) activity.getApplication()).getAppComponent().inject(this); + setPreferencesFromResource(R.xml.download_location_preferences, rootKey); + imageDownloadLocationPreference = findPreference(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION); + gifDownloadLocationPreference = findPreference(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION); + videoDownloadLocationPreference = findPreference(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION); + + if (imageDownloadLocationPreference != null) { + String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); + if (!downloadLocation.equals("")) { + imageDownloadLocationPreference.setSummary(downloadLocation); + } + + imageDownloadLocationPreference.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivityForResult(intent, IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE); + return true; + }); + } + + if (gifDownloadLocationPreference != null) { + String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); + if (!downloadLocation.equals("")) { + gifDownloadLocationPreference.setSummary(downloadLocation); + } + + gifDownloadLocationPreference.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivityForResult(intent, GIF_DOWNLOAD_LOCATION_REQUEST_CODE); + return true; + }); + } + + if (videoDownloadLocationPreference != null) { + String downloadLocation = sharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); + if (!downloadLocation.equals("")) { + videoDownloadLocationPreference.setSummary(downloadLocation); + } + + videoDownloadLocationPreference.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivityForResult(intent, VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE); + return true; + }); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == Activity.RESULT_OK && data != null) { + if (requestCode == IMAGE_DOWNLOAD_LOCATION_REQUEST_CODE) { + sharedPreferences.edit().putString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, data.getDataString()).apply(); + if (imageDownloadLocationPreference != null) { + imageDownloadLocationPreference.setSummary(data.getDataString()); + } + } else if (requestCode == GIF_DOWNLOAD_LOCATION_REQUEST_CODE) { + sharedPreferences.edit().putString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, data.getDataString()).apply(); + if (gifDownloadLocationPreference != null) { + gifDownloadLocationPreference.setSummary(data.getDataString()); + } + } else if (requestCode == VIDEO_DOWNLOAD_LOCATION_REQUEST_CODE) { + sharedPreferences.edit().putString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, data.getDataString()).apply(); + if (videoDownloadLocationPreference != null) { + videoDownloadLocationPreference.setSummary(data.getDataString()); + } + } + } + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + activity = (Activity) context; + } +}
\ No newline at end of file diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/GetPathUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/GetPathUtils.java new file mode 100644 index 00000000..1ccb6b47 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/GetPathUtils.java @@ -0,0 +1,238 @@ +package ml.docilealligator.infinityforreddit.Utils; + +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import androidx.core.content.ContextCompat; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class GetPathUtils { + + private static final String PATH_TREE = "tree"; + private static final String PRIMARY_TYPE = "primary"; + private static final String RAW_TYPE = "raw"; + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static String getFilePathFromUri(final Context context, final Uri uri) { + + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + //Timber.d("docId -> %s", docId); + final String[] split = docId.split(":"); + final String type = split[0]; + + if (PRIMARY_TYPE.equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + // TODO handle non-primary volumes + StringBuilder path = new StringBuilder(); + String pathSegment[] = docId.split(":"); + return path.append(getRemovableStorageRootPath(context, pathSegment[0])).append(File.separator).append(pathSegment[1]).toString(); + } + } else if (isDownloadsDocument(uri)) { // DownloadsProvider + + final String id = DocumentsContract.getDocumentId(uri); + + if (id.contains("raw:")) { + return id.substring(id.indexOf(File.separator)); + } else { + Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + + } else if (isMediaDocument(uri)) { // MediaProvider + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } else if ("content".equalsIgnoreCase(uri.getScheme())) { // MediaStore (and general) + return getDataColumn(context, uri, null, null); + } else if ("file".equalsIgnoreCase(uri.getScheme())) { // File + return uri.getPath(); + } + + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + + Cursor cursor = null; + final String column = MediaStore.MediaColumns.DATA; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri + * @return file path of Uri + */ + public static String getDirectoryPathFromUri(Context context, Uri uri) { + + if ("file".equals(uri.getScheme())) { + return uri.getPath(); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + && isTreeUri(uri)) { + + String treeId = getTreeDocumentId(uri); + + if (treeId != null) { + String[] paths = treeId.split(":"); + String type = paths[0]; + String subPath = paths.length == 2 ? paths[1] : ""; + + if (RAW_TYPE.equalsIgnoreCase(type)) { + return treeId.substring(treeId.indexOf(File.separator)); + } else if (PRIMARY_TYPE.equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + File.separator + subPath; + } else { + StringBuilder path = new StringBuilder(); + String[] pathSegment = treeId.split(":"); + if (pathSegment.length == 1) { + path.append(getRemovableStorageRootPath(context, paths[0])); + } else { + String rootPath = getRemovableStorageRootPath(context, paths[0]); + path.append(rootPath).append(File.separator).append(pathSegment[1]); + } + + return path.toString(); + } + } + } + return null; + } + + private static String getRemovableStorageRootPath(Context context, String storageId) { + StringBuilder rootPath = new StringBuilder(); + File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null); + for (File fileDir : externalFilesDirs) { + if (fileDir.getPath().contains(storageId)) { + String[] pathSegment = fileDir.getPath().split(File.separator); + for (String segment : pathSegment) { + if (segment.equals(storageId)) { + rootPath.append(storageId); + break; + } + rootPath.append(segment).append(File.separator); + } + //rootPath.append(fileDir.getPath().split("/Android")[0]); // faster + break; + } + } + return rootPath.toString(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static List<String> getListRemovableStorage(Context context) { + + List<String> paths = new ArrayList<>(); + + File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null); + for (File fileDir : externalFilesDirs) { + if (Environment.isExternalStorageRemovable(fileDir)) { + String path = fileDir.getPath(); + if (path.contains("/Android")) { + paths.add(path.substring(0, path.indexOf("/Android"))); + } + } + } + + return paths; + } + + //https://github.com/rcketscientist/DocumentActivity/blob/master/library/src/main/java/com/anthonymandra/framework/DocumentUtil.java#L56 + /** + * Extract the via {@link DocumentsContract.Document#COLUMN_DOCUMENT_ID} from the given URI. + * From {@link DocumentsContract} but return null instead of throw + */ + public static String getTreeDocumentId(Uri uri) { + final List<String> paths = uri.getPathSegments(); + if (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))) { + return paths.get(1); + } + return null; + } + + public static boolean isTreeUri(Uri uri) { + final List<String> paths = uri.getPathSegments(); + return (paths.size() == 2 && PATH_TREE.equals(paths.get(0))); + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/SharedPreferencesUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/SharedPreferencesUtils.java index fab9bdd4..16df65e4 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/SharedPreferencesUtils.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Utils/SharedPreferencesUtils.java @@ -117,6 +117,9 @@ public class SharedPreferencesUtils { public static final String DELETE_FRONT_PAGE_SCROLLED_POSITIONS_IN_DATABASE = "delete_front_page_scrolled_positions_in_database"; public static final String DELETE_ALL_LEGACY_SETTINGS = "delete_all_legacy_settings"; public static final String RESET_ALL_SETTINGS = "reset_all_settings"; + public static final String IMAGE_DOWNLOAD_LOCATION = "image_download_location"; + public static final String GIF_DOWNLOAD_LOCATION = "gif_download_location"; + public static final String VIDEO_DOWNLOAD_LOCATION = "video_download_location"; public static final String MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.main_page_tabs"; public static final String MAIN_PAGE_TAB_1_TITLE = "_main_page_tab_1_title"; diff --git a/app/src/main/res/drawable-night/ic_download_24dp.xml b/app/src/main/res/drawable-night/ic_download_24dp.xml new file mode 100644 index 00000000..eb39e3f3 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_download_24dp.xml @@ -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:pathData="M13,5v6h1.17L12,13.17 9.83,11L11,11L11,5h2m2,-2L9,3v6L5,9l7,7 7,-7h-4L15,3zM19,18L5,18v2h14v-2z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/app/src/main/res/drawable/ic_download_24dp.xml b/app/src/main/res/drawable/ic_download_24dp.xml new file mode 100644 index 00000000..ad852174 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_24dp.xml @@ -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:pathData="M13,5v6h1.17L12,13.17 9.83,11L11,11L11,5h2m2,-2L9,3v6L5,9l7,7 7,-7h-4L15,3zM19,18L5,18v2h14v-2z" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 38edbb2a..94df20c9 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -160,7 +160,7 @@ <item>ManropeBold</item> <item>Sriracha</item> </string-array> - + <string-array name="settings_tab_post_type"> <item>Home</item> <item>Popular</item> @@ -228,9 +228,19 @@ <item>yyyy.M.d HH:mm</item> <item>yyyy.M.d hh:mm a</item> </string-array> - + <string-array name="settings_number_of_columns_in_post_feed"> <item>1</item> <item>2</item> </string-array> + <!-- Reply Preference --> + <string-array name="reply_entries"> + <item>Reply</item> + <item>Reply to all</item> + </string-array> + + <string-array name="reply_values"> + <item>reply</item> + <item>reply_all</item> + </string-array> </resources>
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f50a431d..5d31c088 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,6 +471,10 @@ <string name="settings_tab_multi_reddit_name">MultiReddit Name (/user/yourusername/m/yourmultiredditname)</string> <string name="settings_tab_username">Username (Without u/ prefix)</string> <string name="no_developer_easter_egg">There\'s no developer options here</string> + <string name="settings_download_location_title">Download Location</string> + <string name="settings_image_download_location_title">Image Download Location</string> + <string name="settings_gif_download_location_title">Gif Download Location</string> + <string name="settings_video_download_location_title">Video Download Location</string> <string name="no_link_available">Cannot get the link</string> @@ -840,6 +844,18 @@ <string name="downloading_reddit_video_failed_cannot_save_mux_video">Download failed: cannot save the video to public directory</string> <string name="downloading_video">Downloading Video</string> + <string name="downloading_image">Downloading Image</string> + <string name="downloading_gif">Downloading Gif</string> + <string name="downloading_image_save_image">Saving Image</string> + <string name="downloading_gif_save_gif">Saving Gif</string> + <string name="downloading_image_finished">Downloaded</string> + <string name="downloading_gif_finished">Downloaded</string> + <string name="downloading_image_or_gif_failed_cannot_get_destination_directory">Download failed: cannot access destination directory</string> + <string name="downloading_image_failed_cannot_download_image">Download image failed</string> + <string name="downloading_image_failed_cannot_save_image">Download failed: cannot save image to destination directory</string> + <string name="downloading_gif_failed_cannot_download_gif">Download gif failed</string> + <string name="downloading_gif_failed_cannot_save_gif">Download failed: cannot save gif to destination directory</string> + <string name="wallpaper_set">Wallpaper set</string> <string name="error_set_wallpaper">Cannot set wallpaper</string> @@ -858,5 +874,19 @@ <string name="block_user">Block User</string> <string name="block_user_success">Blocked</string> <string name="block_user_failed">Failed to block user</string> + <!-- Preference Titles --> + <string name="messages_header">Messages</string> + <string name="sync_header">Sync</string> + + <!-- Messages Preferences --> + <string name="signature_title">Your signature</string> + <string name="reply_title">Default reply action</string> + + <!-- Sync Preferences --> + <string name="sync_title">Sync email periodically</string> + <string name="attachment_title">Download incoming attachments</string> + <string name="attachment_summary_on">Automatically download attachments for incoming emails + </string> + <string name="attachment_summary_off">Only download attachments when manually requested</string> </resources> diff --git a/app/src/main/res/xml/download_location_preferences.xml b/app/src/main/res/xml/download_location_preferences.xml new file mode 100644 index 00000000..7954cb87 --- /dev/null +++ b/app/src/main/res/xml/download_location_preferences.xml @@ -0,0 +1,13 @@ +<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"> + <Preference + app:key="image_download_location" + app:title="@string/settings_image_download_location_title" /> + + <Preference + app:key="gif_download_location" + app:title="@string/settings_gif_download_location_title" /> + + <Preference + app:key="video_download_location" + app:title="@string/settings_video_download_location_title" /> +</PreferenceScreen>
\ No newline at end of file diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 9af72116..ce6ff184 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -36,6 +36,11 @@ app:title="@string/settings_lazy_mode_interval_title" app:useSimpleSummaryProvider="true" /> + <Preference + app:title="@string/settings_download_location_title" + android:icon="@drawable/ic_download_24dp" + app:fragment="ml.docilealligator.infinityforreddit.Settings.DownloadLocationPreferenceFragment" /> + <SwitchPreference app:defaultValue="false" app:key="save_front_page_scrolled_position" |