From 1ee48844d2a8fb78c7ff07a8a37c292c0f047ac9 Mon Sep 17 00:00:00 2001 From: Alex Ning Date: Sun, 6 Sep 2020 14:02:04 +0800 Subject: Support different download locations in ViewImgurImageFragment, ViewImgurVideoFragment, ViewRedditGalleryImageOrGifFragment and ViewRedditGalleryVideoFragment. --- app/src/main/AndroidManifest.xml | 4 +- .../Activity/ViewImageOrGifActivity.java | 17 +- .../Activity/ViewVideoActivity.java | 12 +- .../infinityforreddit/AppComponent.java | 8 +- .../Event/DownloadImageOrGifEvent.java | 9 - .../Event/DownloadMediaEvent.java | 9 + .../Fragment/ViewImgurImageFragment.java | 14 +- .../Fragment/ViewImgurVideoFragment.java | 18 +- .../ViewRedditGalleryImageOrGifFragment.java | 14 +- .../Fragment/ViewRedditGalleryVideoFragment.java | 18 +- .../Service/DownloadImageService.java | 345 ------------ .../Service/DownloadMediaService.java | 415 ++++++++++++++ .../Service/DownloadRedditVideoService.java | 610 +++++++++++++++++++++ .../Service/DownloadVideoService.java | 610 --------------------- app/src/main/res/values/strings.xml | 10 +- 15 files changed, 1090 insertions(+), 1023 deletions(-) delete mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadMediaEvent.java delete mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadMediaService.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadRedditVideoService.java delete mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java (limited to 'app') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 690f1026..912d1051 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:usesCleartextTraffic="true" tools:replace="android:label"> @@ -42,7 +42,7 @@ android:windowSoftInputMode="adjustResize" /> 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 22a921e6..3e517ae1 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewImageOrGifActivity.java @@ -63,10 +63,8 @@ import ml.docilealligator.infinityforreddit.Font.FontStyle; import ml.docilealligator.infinityforreddit.Font.TitleFontFamily; import ml.docilealligator.infinityforreddit.Font.TitleFontStyle; 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.Service.DownloadMediaService; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.WallpaperSetter; @@ -87,7 +85,6 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa @Inject @Named("default") SharedPreferences mSharedPreferences; - private MediaDownloader mediaDownloader; private boolean isActionBarHidden = false; private boolean isDownloading = false; private RequestManager glide; @@ -134,8 +131,6 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa Slidr.attach(this, new SlidrConfig.Builder().position(SlidrPosition.VERTICAL).distanceThreshold(0.125f).build()); - mediaDownloader = new MediaDownloaderImpl(); - glide = Glide.with(this); Intent intent = getIntent(); @@ -339,10 +334,10 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa private void download() { isDownloading = false; - 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); + Intent intent = new Intent(this, DownloadMediaService.class); + intent.putExtra(DownloadMediaService.EXTRA_URL, mImageUrl); + intent.putExtra(DownloadMediaService.EXTRA_MEDIA_TYPE, isGif ? DownloadMediaService.EXTRA_MEDIA_TYPE_GIF : DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE); + intent.putExtra(DownloadMediaService.EXTRA_FILE_NAME, mImageFileName); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent); } else { @@ -437,7 +432,7 @@ public class ViewImageOrGifActivity extends AppCompatActivity implements SetAsWa if (grantResults[0] == PackageManager.PERMISSION_DENIED) { Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED && isDownloading) { - mediaDownloader.download(mImageUrl, mImageFileName, this); + download(); } isDownloading = false; } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java index c1c2c45c..ae709246 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java @@ -62,7 +62,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.DownloadVideoService; +import ml.docilealligator.infinityforreddit.Service.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; import retrofit2.Retrofit; @@ -440,11 +440,11 @@ public class ViewVideoActivity extends AppCompatActivity { if (videoType != VIDEO_TYPE_NORMAL) { mediaDownloader.download(videoDownloadUrl, videoFileName, this); } else { - Intent intent = new Intent(this, DownloadVideoService.class); - intent.putExtra(DownloadVideoService.EXTRA_VIDEO_URL, videoDownloadUrl); - intent.putExtra(DownloadVideoService.EXTRA_POST_ID, id); - intent.putExtra(DownloadVideoService.EXTRA_SUBREDDIT, subredditName); - intent.putExtra(DownloadVideoService.EXTRA_IS_REDDIT_VIDEO, true); + Intent intent = new Intent(this, DownloadRedditVideoService.class); + intent.putExtra(DownloadRedditVideoService.EXTRA_VIDEO_URL, videoDownloadUrl); + intent.putExtra(DownloadRedditVideoService.EXTRA_POST_ID, id); + intent.putExtra(DownloadRedditVideoService.EXTRA_SUBREDDIT, subredditName); + intent.putExtra(DownloadRedditVideoService.EXTRA_IS_REDDIT_VIDEO, true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java index 0d3e6aec..add19533 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java @@ -54,8 +54,8 @@ 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.DownloadMediaService; +import ml.docilealligator.infinityforreddit.Service.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.Service.SubmitPostService; import ml.docilealligator.infinityforreddit.Settings.AdvancedPreferenceFragment; import ml.docilealligator.infinityforreddit.Settings.CustomizeMainPageTabsFragment; @@ -173,7 +173,7 @@ public interface AppComponent { void inject(ViewImgurVideoFragment viewImgurVideoFragment); - void inject(DownloadVideoService downloadVideoService); + void inject(DownloadRedditVideoService downloadRedditVideoService); void inject(MultiRedditListingFragment multiRedditListingFragment); @@ -191,7 +191,7 @@ public interface AppComponent { void inject(CustomizeMainPageTabsFragment customizeMainPageTabsFragment); - void inject(DownloadImageService downloadImageService); + void inject(DownloadMediaService downloadMediaService); void inject(DownloadLocationPreferenceFragment downloadLocationPreferenceFragment); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java deleted file mode 100644 index 61e87c1f..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadImageOrGifEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -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/Event/DownloadMediaEvent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadMediaEvent.java new file mode 100644 index 00000000..174b49ea --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Event/DownloadMediaEvent.java @@ -0,0 +1,9 @@ +package ml.docilealligator.infinityforreddit.Event; + +public class DownloadMediaEvent { + public boolean isSuccessful; + + public DownloadMediaEvent(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 6a801091..29101dd6 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurImageFragment.java @@ -45,10 +45,8 @@ import ml.docilealligator.infinityforreddit.AsyncTask.SaveBitmapImageToFileAsync import ml.docilealligator.infinityforreddit.BottomSheetFragment.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.BuildConfig; import ml.docilealligator.infinityforreddit.ImgurMedia; -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.Service.DownloadMediaService; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; public class ViewImgurImageFragment extends Fragment { @@ -65,7 +63,6 @@ public class ViewImgurImageFragment extends Fragment { private ViewImgurMediaActivity activity; private RequestManager glide; - private MediaDownloader mediaDownloader; private ImgurMedia imgurMedia; private boolean isDownloading = false; private boolean isActionBarHidden = false; @@ -85,7 +82,6 @@ public class ViewImgurImageFragment extends Fragment { imgurMedia = getArguments().getParcelable(EXTRA_IMGUR_IMAGES); glide = Glide.with(activity); - mediaDownloader = new MediaDownloaderImpl(); loadImage(); imageView.setOnClickListener(view -> { @@ -236,10 +232,10 @@ public class ViewImgurImageFragment extends Fragment { private void download() { isDownloading = false; - Intent intent = new Intent(activity, DownloadImageService.class); - intent.putExtra(DownloadImageService.EXTRA_URL, imgurMedia.getLink()); - intent.putExtra(DownloadImageService.EXTRA_IS_GIF, false); - intent.putExtra(DownloadImageService.EXTRA_FILE_NAME, imgurMedia.getFileName()); + Intent intent = new Intent(activity, DownloadMediaService.class); + intent.putExtra(DownloadMediaService.EXTRA_URL, imgurMedia.getLink()); + intent.putExtra(DownloadMediaService.EXTRA_MEDIA_TYPE, DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE); + intent.putExtra(DownloadMediaService.EXTRA_FILE_NAME, imgurMedia.getFileName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity.startForegroundService(intent); } else { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurVideoFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurVideoFragment.java index 1e968f57..154901bb 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurVideoFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewImgurVideoFragment.java @@ -3,6 +3,7 @@ package ml.docilealligator.infinityforreddit.Fragment; import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -46,9 +47,8 @@ import butterknife.BindView; import butterknife.ButterKnife; import ml.docilealligator.infinityforreddit.ImgurMedia; 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.DownloadMediaService; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; public class ViewImgurVideoFragment extends Fragment { @@ -65,7 +65,6 @@ public class ViewImgurVideoFragment extends Fragment { private ImgurMedia imgurMedia; private SimpleExoPlayer player; private DataSource.Factory dataSourceFactory; - private MediaDownloader mediaDownloader; private boolean wasPlaying = false; private boolean isMute = false; private boolean isDownloading = false; @@ -128,8 +127,6 @@ public class ViewImgurVideoFragment extends Fragment { } }); - mediaDownloader = new MediaDownloaderImpl(); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(activity, trackSelector); @@ -188,7 +185,16 @@ public class ViewImgurVideoFragment extends Fragment { private void download() { isDownloading = false; - mediaDownloader.download(imgurMedia.getLink(), imgurMedia.getFileName(), getContext()); + Intent intent = new Intent(activity, DownloadMediaService.class); + intent.putExtra(DownloadMediaService.EXTRA_URL, imgurMedia.getLink()); + intent.putExtra(DownloadMediaService.EXTRA_MEDIA_TYPE, DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO); + intent.putExtra(DownloadMediaService.EXTRA_FILE_NAME, imgurMedia.getFileName()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.startForegroundService(intent); + } else { + activity.startService(intent); + } + Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void preparePlayer(Bundle savedInstanceState) { 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 eb809fa5..a34e8628 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryImageOrGifFragment.java @@ -50,11 +50,9 @@ import ml.docilealligator.infinityforreddit.AsyncTask.SaveBitmapImageToFileAsync import ml.docilealligator.infinityforreddit.AsyncTask.SaveGIFToFileAsyncTask; import ml.docilealligator.infinityforreddit.BottomSheetFragment.SetAsWallpaperBottomSheetFragment; import ml.docilealligator.infinityforreddit.BuildConfig; -import ml.docilealligator.infinityforreddit.MediaDownloader; -import ml.docilealligator.infinityforreddit.MediaDownloaderImpl; import ml.docilealligator.infinityforreddit.Post.Post; import ml.docilealligator.infinityforreddit.R; -import ml.docilealligator.infinityforreddit.Service.DownloadImageService; +import ml.docilealligator.infinityforreddit.Service.DownloadMediaService; import ml.docilealligator.infinityforreddit.SetAsWallpaperCallback; public class ViewRedditGalleryImageOrGifFragment extends Fragment { @@ -71,7 +69,6 @@ public class ViewRedditGalleryImageOrGifFragment extends Fragment { private ViewRedditGalleryActivity activity; private RequestManager glide; - private MediaDownloader mediaDownloader; private Post.Gallery media; private boolean isDownloading = false; private boolean isActionBarHidden = false; @@ -93,7 +90,6 @@ public class ViewRedditGalleryImageOrGifFragment extends Fragment { media = getArguments().getParcelable(EXTRA_REDDIT_GALLERY_MEDIA); glide = Glide.with(activity); - mediaDownloader = new MediaDownloaderImpl(); imageView.setImageViewFactory(new GlideImageViewFactory()); @@ -269,10 +265,10 @@ public class ViewRedditGalleryImageOrGifFragment extends Fragment { private void download() { isDownloading = false; - Intent intent = new Intent(activity, DownloadImageService.class); - intent.putExtra(DownloadImageService.EXTRA_URL, media.url); - intent.putExtra(DownloadImageService.EXTRA_IS_GIF, media.mediaType == Post.Gallery.TYPE_GIF); - intent.putExtra(DownloadImageService.EXTRA_FILE_NAME, media.fileName); + Intent intent = new Intent(activity, DownloadMediaService.class); + intent.putExtra(DownloadMediaService.EXTRA_URL, media.url); + intent.putExtra(DownloadMediaService.EXTRA_MEDIA_TYPE, media.mediaType == Post.Gallery.TYPE_GIF ? DownloadMediaService.EXTRA_MEDIA_TYPE_GIF: DownloadMediaService.EXTRA_MEDIA_TYPE_IMAGE); + intent.putExtra(DownloadMediaService.EXTRA_FILE_NAME, media.fileName); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity.startForegroundService(intent); } else { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryVideoFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryVideoFragment.java index 34962aab..3357b44e 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryVideoFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Fragment/ViewRedditGalleryVideoFragment.java @@ -3,6 +3,7 @@ package ml.docilealligator.infinityforreddit.Fragment; import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -45,10 +46,9 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; import ml.docilealligator.infinityforreddit.Infinity; -import ml.docilealligator.infinityforreddit.MediaDownloader; -import ml.docilealligator.infinityforreddit.MediaDownloaderImpl; import ml.docilealligator.infinityforreddit.Post.Post; import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.Service.DownloadMediaService; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; public class ViewRedditGalleryVideoFragment extends Fragment { @@ -65,7 +65,6 @@ public class ViewRedditGalleryVideoFragment extends Fragment { private Post.Gallery galleryVideo; private SimpleExoPlayer player; private DataSource.Factory dataSourceFactory; - private MediaDownloader mediaDownloader; private boolean wasPlaying = false; private boolean isMute = false; private boolean isDownloading = false; @@ -128,8 +127,6 @@ public class ViewRedditGalleryVideoFragment extends Fragment { } }); - mediaDownloader = new MediaDownloaderImpl(); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(activity, trackSelector); @@ -188,7 +185,16 @@ public class ViewRedditGalleryVideoFragment extends Fragment { private void download() { isDownloading = false; - mediaDownloader.download(galleryVideo.url, galleryVideo.fileName, getContext()); + Intent intent = new Intent(activity, DownloadMediaService.class); + intent.putExtra(DownloadMediaService.EXTRA_URL, galleryVideo.url); + intent.putExtra(DownloadMediaService.EXTRA_MEDIA_TYPE, DownloadMediaService.EXTRA_MEDIA_TYPE_VIDEO); + intent.putExtra(DownloadMediaService.EXTRA_FILE_NAME, galleryVideo.fileName); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.startForegroundService(intent); + } else { + activity.startService(intent); + } + Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show(); } private void preparePlayer(Bundle savedInstanceState) { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java deleted file mode 100644 index 06d7449e..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadImageService.java +++ /dev/null @@ -1,345 +0,0 @@ -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() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response 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 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 { - - 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/DownloadMediaService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadMediaService.java new file mode 100644 index 00000000..51eea592 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadMediaService.java @@ -0,0 +1,415 @@ +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.DownloadMediaEvent; +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 DownloadMediaService extends Service { + public static final String EXTRA_URL = "EU"; + public static final String EXTRA_FILE_NAME = "EFN"; + public static final String EXTRA_MEDIA_TYPE = "EIG"; + public static final int EXTRA_MEDIA_TYPE_IMAGE = 0; + public static final int EXTRA_MEDIA_TYPE_GIF = 1; + public static final int EXTRA_MEDIA_TYPE_VIDEO = 2; + + 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 int mediaType; + + public DownloadMediaService() { + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private String getNotificationChannelId() { + switch (mediaType) { + case EXTRA_MEDIA_TYPE_GIF: + return NotificationUtils.CHANNEL_ID_DOWNLOAD_GIF; + case EXTRA_MEDIA_TYPE_VIDEO: + return NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO; + default: + return NotificationUtils.CHANNEL_ID_DOWNLOAD_IMAGE; + } + } + + private String getNotificationChannel() { + switch (mediaType) { + case EXTRA_MEDIA_TYPE_GIF: + return NotificationUtils.CHANNEL_DOWNLOAD_GIF; + case EXTRA_MEDIA_TYPE_VIDEO: + return NotificationUtils.CHANNEL_DOWNLOAD_VIDEO; + default: + return NotificationUtils.CHANNEL_DOWNLOAD_IMAGE; + } + } + + private int getNotificationId() { + switch (mediaType) { + case EXTRA_MEDIA_TYPE_GIF: + return NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID; + case EXTRA_MEDIA_TYPE_VIDEO: + return NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID; + default: + return NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID; + } + } + + private String getDownloadLocation() { + switch (mediaType) { + case EXTRA_MEDIA_TYPE_GIF: + return mSharedPreferences.getString(SharedPreferencesUtils.GIF_DOWNLOAD_LOCATION, ""); + case EXTRA_MEDIA_TYPE_VIDEO: + return mSharedPreferences.getString(SharedPreferencesUtils.VIDEO_DOWNLOAD_LOCATION, ""); + default: + return mSharedPreferences.getString(SharedPreferencesUtils.IMAGE_DOWNLOAD_LOCATION, ""); + } + } + + @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); + mediaType = intent.getIntExtra(EXTRA_MEDIA_TYPE, EXTRA_MEDIA_TYPE_IMAGE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel; + serviceChannel = new NotificationChannel( + getNotificationChannelId(), + getNotificationChannel(), + NotificationManager.IMPORTANCE_LOW + ); + + NotificationManagerCompat manager = NotificationManagerCompat.from(this); + manager.createNotificationChannel(serviceChannel); + } + + switch (mediaType) { + case EXTRA_MEDIA_TYPE_GIF: + startForeground( + NotificationUtils.DOWNLOAD_GIF_NOTIFICATION_ID, + createNotification(R.string.downloading_gif, fileName, null) + ); + break; + case EXTRA_MEDIA_TYPE_VIDEO: + startForeground( + NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID, + createNotification(R.string.downloading_video, fileName, null) + ); + break; + default: + startForeground( + NotificationUtils.DOWNLOAD_IMAGE_NOTIFICATION_ID, + createNotification(R.string.downloading_image, fileName, null) + ); + } + + retrofit.create(DownloadFile.class).downloadFile(fileUrl).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + String destinationFileDirectory = getDownloadLocation(); + 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(DownloadMediaService.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(), mediaType, + 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 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, getNotificationChannelId()); + 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(getNotificationId(), 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(R.string.downloading_media_failed_cannot_download_media, fileName, null); + break; + case ERROR_FILE_CANNOT_SAVE: + updateNotification(R.string.downloading_media_failed_cannot_save_to_destination_directory, fileName, null); + break; + } + EventBus.getDefault().post(new DownloadMediaEvent(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(R.string.downloading_media_finished, fileName, pendingIntent); + EventBus.getDefault().post(new DownloadMediaEvent(true)); + } + ); + } + stopForeground(false); + } + + private static class SaveImageOrGifAndCopyToExternalStorageAsyncTask extends AsyncTask { + + private ResponseBody response; + private int mediaType; + 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, int mediaType, + boolean isDefaultDestination, + String destinationFileName, + @NonNull String destinationFileUriString, + ContentResolver contentResolver, + SaveImageOrGifAndCopyToExternalStorageAsyncTaskListener saveImageOrGifAndCopyToExternalStorageAsyncTaskListener) { + this.response = response; + this.mediaType = mediaType; + 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) { + switch (mediaType) { + case EXTRA_MEDIA_TYPE_IMAGE: + publishProgress(R.string.downloading_image_save_image); + break; + case EXTRA_MEDIA_TYPE_GIF: + publishProgress(R.string.downloading_gif_save_gif); + break; + case EXTRA_MEDIA_TYPE_VIDEO: + publishProgress(R.string.downloading_video_save_video); + } + + 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/DownloadRedditVideoService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadRedditVideoService.java new file mode 100644 index 00000000..ef58a747 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadRedditVideoService.java @@ -0,0 +1,610 @@ +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.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMuxer; +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.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +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.DownloadRedditVideoEvent; +import ml.docilealligator.infinityforreddit.Infinity; +import ml.docilealligator.infinityforreddit.NotificationUtils; +import ml.docilealligator.infinityforreddit.R; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; + +import static android.os.Environment.getExternalStoragePublicDirectory; + +public class DownloadRedditVideoService extends Service { + + public static final String EXTRA_VIDEO_URL = "EVU"; + public static final String EXTRA_SUBREDDIT = "ES"; + public static final String EXTRA_POST_ID = "EPI"; + public static final String EXTRA_IS_REDDIT_VIDEO = "EIRV"; + public static final String EXTRA_FILE_NAME = "EFN"; + + private static final int NO_ERROR = -1; + private static final int ERROR_CANNOT_GET_CACHE_DIRECTORY = 0; + private static final int ERROR_VIDEO_FILE_CANNOT_DOWNLOAD = 1; + private static final int ERROR_VIDEO_FILE_CANNOT_SAVE = 2; + private static final int ERROR_AUDIO_FILE_CANNOT_SAVE = 3; + private static final int ERROR_MUX_FAILED = 4; + private static final int ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE = 5; + + private boolean isRedditVideo; + + @Inject + @Named("download_media") + Retrofit retrofit; + @Inject + CustomThemeWrapper mCustomThemeWrapper; + String resultFile; + + public DownloadRedditVideoService() { + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + ((Infinity) getApplication()).getAppComponent().inject(this); + + isRedditVideo = intent.getBooleanExtra(EXTRA_IS_REDDIT_VIDEO, false); + String videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL); + String audioUrl = isRedditVideo ? videoUrl.substring(0, videoUrl.lastIndexOf('/')) + "/DASH_audio.mp4" : ""; + String fileName; + if (isRedditVideo) { + fileName = intent.getStringExtra(EXTRA_SUBREDDIT) + "-" + intent.getStringExtra(EXTRA_POST_ID); + } else { + fileName = intent.getStringExtra(EXTRA_FILE_NAME); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel; + if (isRedditVideo) { + serviceChannel = new NotificationChannel( + NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO, + NotificationUtils.CHANNEL_DOWNLOAD_REDDIT_VIDEO, + NotificationManager.IMPORTANCE_LOW + ); + } else { + serviceChannel = new NotificationChannel( + NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO, + NotificationUtils.CHANNEL_DOWNLOAD_VIDEO, + NotificationManager.IMPORTANCE_LOW + ); + } + + NotificationManagerCompat manager = NotificationManagerCompat.from(this); + manager.createNotificationChannel(serviceChannel); + } + + if (isRedditVideo) { + startForeground( + NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID, + createNotification(R.string.downloading_reddit_video, fileName + ".mp4", null) + ); + } else { + startForeground( + NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID, + createNotification(R.string.downloading_video, fileName, null) + ); + } + + DownloadFile downloadFile = retrofit.create(DownloadFile.class); + + File directory = getExternalCacheDir(); + String destinationFileName = fileName + ".mp4"; + if (directory != null) { + String directoryPath = directory.getAbsolutePath() + "/"; + downloadFile.downloadFile(videoUrl).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response videoResponse) { + if (videoResponse.isSuccessful() && videoResponse.body() != null) { + if (isRedditVideo) { + updateNotification(R.string.downloading_reddit_video_audio_track, destinationFileName, null); + downloadFile.downloadFile(audioUrl).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response audioResponse) { + if (audioResponse.isSuccessful() && audioResponse.body() != null) { + String videoFilePath = directoryPath + fileName + "-cache.mp4"; + String audioFilePath = directoryPath + fileName + "-cache.mp3"; + String outputFilePath = directoryPath + fileName + ".mp4"; + new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), + audioResponse.body(), videoFilePath, audioFilePath, outputFilePath, + destinationFileName, getContentResolver(), + new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { + @Override + public void finished(Uri destinationFileUri, int errorCode) { + new File(videoFilePath).delete(); + new File(audioFilePath).delete(); + new File(outputFilePath).delete(); + downloadFinished(destinationFileUri, destinationFileName, errorCode); + } + + @Override + public void updateProgressNotification(int stringResId) { + updateNotification(stringResId, destinationFileName, null); + } + }).execute(); + } else { + String videoFilePath = directoryPath + fileName + "-cache.mp4"; + String destinationFileName = fileName + ".mp4"; + new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), + null, videoFilePath, null, null, + destinationFileName, getContentResolver(), + new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { + @Override + public void finished(Uri destinationFileUri, int errorCode) { + new File(videoFilePath).delete(); + downloadFinished(destinationFileUri, destinationFileName, errorCode); + } + + @Override + public void updateProgressNotification(int stringResId) { + updateNotification(stringResId, destinationFileName, null); + } + }).execute(); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + String videoFilePath = directoryPath + fileName + "-cache.mp4"; + String destinationFileName = fileName + ".mp4"; + new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), + null, videoFilePath, null, null, + destinationFileName, getContentResolver(), + new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { + @Override + public void finished(Uri destinationFileUri, int errorCode) { + new File(videoFilePath).delete(); + downloadFinished(destinationFileUri, destinationFileName, errorCode); + } + + @Override + public void updateProgressNotification(int stringResId) { + updateNotification(stringResId, destinationFileName, null); + } + }).execute(); + } + }); + } else { + String videoFilePath = directoryPath + fileName; + new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), + null, videoFilePath, null, null, + fileName, getContentResolver(), + new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { + @Override + public void finished(Uri destinationFileUri, int errorCode) { + new File(videoFilePath).delete(); + downloadFinished(destinationFileUri, fileName, errorCode); + } + + @Override + public void updateProgressNotification(int stringResId) { + updateNotification(stringResId, fileName, null); + } + }).execute(); + } + } else { + downloadFinished(null, destinationFileName, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + downloadFinished(null, destinationFileName, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD); + } + }); + } else { + downloadFinished(null, destinationFileName, ERROR_CANNOT_GET_CACHE_DIRECTORY); + } + + return START_NOT_STICKY; + } + + private void downloadFinished(Uri destinationFileUri, String fileName, int errorCode) { + if (errorCode != NO_ERROR) { + switch (errorCode) { + case ERROR_CANNOT_GET_CACHE_DIRECTORY: + updateNotification(R.string.downloading_reddit_video_failed_cannot_get_cache_directory, fileName, null); + break; + case ERROR_VIDEO_FILE_CANNOT_DOWNLOAD: + updateNotification(R.string.downloading_reddit_video_failed_cannot_download_video, fileName, null); + break; + case ERROR_VIDEO_FILE_CANNOT_SAVE: + updateNotification(R.string.downloading_reddit_video_failed_cannot_save_video, fileName, null); + break; + case ERROR_AUDIO_FILE_CANNOT_SAVE: + updateNotification(R.string.downloading_reddit_video_failed_cannot_save_audio, fileName, null); + break; + case ERROR_MUX_FAILED: + updateNotification(R.string.downloading_reddit_video_failed_cannot_mux, fileName, null); + break; + case ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE: + updateNotification(R.string.downloading_reddit_video_failed_cannot_save_mux_video, fileName, null); + break; + } + EventBus.getDefault().post(new DownloadRedditVideoEvent(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, "video/*"); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + updateNotification(R.string.downloading_reddit_video_finished, fileName, pendingIntent); + EventBus.getDefault().post(new DownloadRedditVideoEvent(true)); + } + ); + } + stopForeground(false); + } + + private Notification createNotification(int stringResId, String fileName, PendingIntent pendingIntent) { + NotificationCompat.Builder builder; + if (isRedditVideo) { + builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO); + } else { + builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO); + } + 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) { + if (isRedditVideo) { + notificationManager.notify(NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID, + createNotification(stringResId, fileName, pendingIntent)); + } else { + notificationManager.notify(NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID, + createNotification(stringResId, fileName, pendingIntent)); + } + } + } + + private static class SaveTempMuxAndCopyAsyncTask extends AsyncTask { + + private ResponseBody videoResponse; + private ResponseBody audioResponse; + private String videoFilePath; + private String audioFilePath; + private String outputFilePath; + private String destinationFileName; + private ContentResolver contentResolver; + private SaveTempMuxAndCopyAsyncTaskListener saveTempMuxAndCopyAsyncTaskListener; + private Uri destinationFileUri; + private int errorCode = NO_ERROR; + + public SaveTempMuxAndCopyAsyncTask(ResponseBody videoResponse, ResponseBody audioResponse, + String videoFilePath, String audioFilePath, String outputFilePath, + String destinationFileName, ContentResolver contentResolver, + SaveTempMuxAndCopyAsyncTaskListener saveTempMuxAndCopyAsyncTaskListener) { + this.videoResponse = videoResponse; + this.audioResponse = audioResponse; + this.videoFilePath = videoFilePath; + this.audioFilePath = audioFilePath; + this.outputFilePath = outputFilePath; + this.destinationFileName = destinationFileName; + this.contentResolver = contentResolver; + this.saveTempMuxAndCopyAsyncTaskListener = saveTempMuxAndCopyAsyncTaskListener; + } + + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + saveTempMuxAndCopyAsyncTaskListener.updateProgressNotification(values[0]); + } + + @Override + protected Void doInBackground(Void... voids) { + publishProgress(R.string.downloading_reddit_video_save_video); + String savedVideoFilePath = writeResponseBodyToDisk(videoResponse, videoFilePath); + if (savedVideoFilePath == null) { + errorCode = ERROR_VIDEO_FILE_CANNOT_SAVE; + return null; + } + if (audioResponse != null) { + publishProgress(R.string.downloading_reddit_video_save_audio); + String savedAudioFilePath = writeResponseBodyToDisk(audioResponse, audioFilePath); + if (savedAudioFilePath == null) { + errorCode = ERROR_AUDIO_FILE_CANNOT_SAVE; + return null; + } + + publishProgress(R.string.downloading_reddit_video_muxing); + if (!muxVideoAndAudio(videoFilePath, audioFilePath, outputFilePath)) { + errorCode = ERROR_MUX_FAILED; + return null; + } + + publishProgress(R.string.downloading_reddit_video_save_file_to_public_dir); + copyToDestination(outputFilePath); + } else { + publishProgress(R.string.downloading_reddit_video_save_file_to_public_dir); + copyToDestination(videoFilePath); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + saveTempMuxAndCopyAsyncTaskListener.finished(destinationFileUri, errorCode); + } + + private String writeResponseBodyToDisk(ResponseBody body, String filePath) { + try { + File file = new File(filePath); + + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + byte[] fileReader = new byte[4096]; + + long fileSize = body.contentLength(); + long fileSizeDownloaded = 0; + + inputStream = body.byteStream(); + outputStream = new FileOutputStream(file); + + while (true) { + int read = inputStream.read(fileReader); + + if (read == -1) { + break; + } + + outputStream.write(fileReader, 0, read); + + fileSizeDownloaded += read; + } + + outputStream.flush(); + + return file.getPath(); + } catch (IOException e) { + return null; + } finally { + if (inputStream != null) { + inputStream.close(); + } + + if (outputStream != null) { + outputStream.close(); + } + } + } catch (IOException e) { + return null; + } + } + + private boolean muxVideoAndAudio(String videoFilePath, String audioFilePath, String outputFilePath) { + try { + File file = new File(outputFilePath); + file.createNewFile(); + MediaExtractor videoExtractor = new MediaExtractor(); + videoExtractor.setDataSource(videoFilePath); + MediaExtractor audioExtractor = new MediaExtractor(); + audioExtractor.setDataSource(audioFilePath); + MediaMuxer muxer = new MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + + videoExtractor.selectTrack(0); + MediaFormat videoFormat = videoExtractor.getTrackFormat(0); + int videoTrack = muxer.addTrack(videoFormat); + + audioExtractor.selectTrack(0); + MediaFormat audioFormat = audioExtractor.getTrackFormat(0); + int audioTrack = muxer.addTrack(audioFormat); + boolean sawEOS = false; + int offset = 100; + int sampleSize = 2048 * 1024; + ByteBuffer videoBuf = ByteBuffer.allocate(sampleSize); + ByteBuffer audioBuf = ByteBuffer.allocate(sampleSize); + MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo(); + MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo(); + + videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + + muxer.start(); + + while (!sawEOS) { + videoBufferInfo.offset = offset; + videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset); + + if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) { + sawEOS = true; + videoBufferInfo.size = 0; + } else { + videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime(); + videoBufferInfo.flags = videoExtractor.getSampleFlags(); + muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo); + videoExtractor.advance(); + } + } + + boolean sawEOS2 = false; + while (!sawEOS2) { + audioBufferInfo.offset = offset; + audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset); + + if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) { + sawEOS2 = true; + audioBufferInfo.size = 0; + } else { + audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime(); + audioBufferInfo.flags = audioExtractor.getSampleFlags(); + muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo); + audioExtractor.advance(); + + } + } + + try { + muxer.stop(); + muxer.release(); + } catch (IllegalStateException ignore) {} + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + } + + private void copyToDestination(String srcPath) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (!copy(new File(srcPath), destinationFileName)) { + errorCode = ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE; + } + } else { + try { + copyFileQ(new File(srcPath), destinationFileName); + } catch (IOException e) { + e.printStackTrace(); + errorCode = ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE; + } + } + } + + @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); + + OutputStream stream = null; + Uri uri = null; + + try { + final Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + uri = contentResolver.insert(contentUri, contentValues); + + if (uri == null) { + throw new IOException("Failed to create new MediaStore record."); + } + + stream = contentResolver.openOutputStream(uri); + + if (stream == null) { + throw new IOException("Failed to get output stream."); + } + + InputStream in = new FileInputStream(src); + + 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); + destinationFileUri = uri; + } catch (IOException e) { + if (uri != null) { + // Don't leave an orphan entry in the MediaStore + contentResolver.delete(uri, null, null); + } + + throw e; + } finally { + if (stream != null) { + stream.close(); + } + } + } + + private boolean copy(File src, String outputFileName) { + File directory = getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + if (directory != null) { + String directoryPath = directory.getAbsolutePath() + "/Infinity/"; + String destinationFilePath = directoryPath + outputFileName; + destinationFileUri = Uri.parse(destinationFilePath); + + try (InputStream in = new FileInputStream(src)) { + try (OutputStream out = new FileOutputStream(destinationFilePath)) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + + src.delete(); + return true; + } + } catch (IOException e) { + e.printStackTrace(); + src.delete(); + return false; + } + } else { + src.delete(); + return false; + } + } + + interface SaveTempMuxAndCopyAsyncTaskListener { + void finished(Uri destinationFileUri, int errorCode); + void updateProgressNotification(int stringResId); + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java deleted file mode 100644 index 4926785f..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/DownloadVideoService.java +++ /dev/null @@ -1,610 +0,0 @@ -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.media.MediaCodec; -import android.media.MediaExtractor; -import android.media.MediaFormat; -import android.media.MediaMuxer; -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.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -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.DownloadRedditVideoEvent; -import ml.docilealligator.infinityforreddit.Infinity; -import ml.docilealligator.infinityforreddit.NotificationUtils; -import ml.docilealligator.infinityforreddit.R; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; - -import static android.os.Environment.getExternalStoragePublicDirectory; - -public class DownloadVideoService extends Service { - - public static final String EXTRA_VIDEO_URL = "EVU"; - public static final String EXTRA_SUBREDDIT = "ES"; - public static final String EXTRA_POST_ID = "EPI"; - public static final String EXTRA_IS_REDDIT_VIDEO = "EIRV"; - public static final String EXTRA_FILE_NAME = "EFN"; - - private static final int NO_ERROR = -1; - private static final int ERROR_CANNOT_GET_CACHE_DIRECTORY = 0; - private static final int ERROR_VIDEO_FILE_CANNOT_DOWNLOAD = 1; - private static final int ERROR_VIDEO_FILE_CANNOT_SAVE = 2; - private static final int ERROR_AUDIO_FILE_CANNOT_SAVE = 3; - private static final int ERROR_MUX_FAILED = 4; - private static final int ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE = 5; - - private boolean isRedditVideo; - - @Inject - @Named("download_media") - Retrofit retrofit; - @Inject - CustomThemeWrapper mCustomThemeWrapper; - String resultFile; - - public DownloadVideoService() { - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - ((Infinity) getApplication()).getAppComponent().inject(this); - - isRedditVideo = intent.getBooleanExtra(EXTRA_IS_REDDIT_VIDEO, false); - String videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL); - String audioUrl = isRedditVideo ? videoUrl.substring(0, videoUrl.lastIndexOf('/')) + "/DASH_audio.mp4" : ""; - String fileName; - if (isRedditVideo) { - fileName = intent.getStringExtra(EXTRA_SUBREDDIT) + "-" + intent.getStringExtra(EXTRA_POST_ID); - } else { - fileName = intent.getStringExtra(EXTRA_FILE_NAME); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel serviceChannel; - if (isRedditVideo) { - serviceChannel = new NotificationChannel( - NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO, - NotificationUtils.CHANNEL_DOWNLOAD_REDDIT_VIDEO, - NotificationManager.IMPORTANCE_LOW - ); - } else { - serviceChannel = new NotificationChannel( - NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO, - NotificationUtils.CHANNEL_DOWNLOAD_VIDEO, - NotificationManager.IMPORTANCE_LOW - ); - } - - NotificationManagerCompat manager = NotificationManagerCompat.from(this); - manager.createNotificationChannel(serviceChannel); - } - - if (isRedditVideo) { - startForeground( - NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID, - createNotification(R.string.downloading_reddit_video, fileName + ".mp4", null) - ); - } else { - startForeground( - NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID, - createNotification(R.string.downloading_video, fileName, null) - ); - } - - DownloadFile downloadFile = retrofit.create(DownloadFile.class); - - File directory = getExternalCacheDir(); - String destinationFileName = fileName + ".mp4"; - if (directory != null) { - String directoryPath = directory.getAbsolutePath() + "/"; - downloadFile.downloadFile(videoUrl).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response videoResponse) { - if (videoResponse.isSuccessful() && videoResponse.body() != null) { - if (isRedditVideo) { - updateNotification(R.string.downloading_reddit_video_audio_track, destinationFileName, null); - downloadFile.downloadFile(audioUrl).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response audioResponse) { - if (audioResponse.isSuccessful() && audioResponse.body() != null) { - String videoFilePath = directoryPath + fileName + "-cache.mp4"; - String audioFilePath = directoryPath + fileName + "-cache.mp3"; - String outputFilePath = directoryPath + fileName + ".mp4"; - new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), - audioResponse.body(), videoFilePath, audioFilePath, outputFilePath, - destinationFileName, getContentResolver(), - new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { - @Override - public void finished(Uri destinationFileUri, int errorCode) { - new File(videoFilePath).delete(); - new File(audioFilePath).delete(); - new File(outputFilePath).delete(); - downloadFinished(destinationFileUri, destinationFileName, errorCode); - } - - @Override - public void updateProgressNotification(int stringResId) { - updateNotification(stringResId, destinationFileName, null); - } - }).execute(); - } else { - String videoFilePath = directoryPath + fileName + "-cache.mp4"; - String destinationFileName = fileName + ".mp4"; - new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), - null, videoFilePath, null, null, - destinationFileName, getContentResolver(), - new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { - @Override - public void finished(Uri destinationFileUri, int errorCode) { - new File(videoFilePath).delete(); - downloadFinished(destinationFileUri, destinationFileName, errorCode); - } - - @Override - public void updateProgressNotification(int stringResId) { - updateNotification(stringResId, destinationFileName, null); - } - }).execute(); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - String videoFilePath = directoryPath + fileName + "-cache.mp4"; - String destinationFileName = fileName + ".mp4"; - new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), - null, videoFilePath, null, null, - destinationFileName, getContentResolver(), - new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { - @Override - public void finished(Uri destinationFileUri, int errorCode) { - new File(videoFilePath).delete(); - downloadFinished(destinationFileUri, destinationFileName, errorCode); - } - - @Override - public void updateProgressNotification(int stringResId) { - updateNotification(stringResId, destinationFileName, null); - } - }).execute(); - } - }); - } else { - String videoFilePath = directoryPath + fileName; - new SaveTempMuxAndCopyAsyncTask(videoResponse.body(), - null, videoFilePath, null, null, - fileName, getContentResolver(), - new SaveTempMuxAndCopyAsyncTask.SaveTempMuxAndCopyAsyncTaskListener() { - @Override - public void finished(Uri destinationFileUri, int errorCode) { - new File(videoFilePath).delete(); - downloadFinished(destinationFileUri, fileName, errorCode); - } - - @Override - public void updateProgressNotification(int stringResId) { - updateNotification(stringResId, fileName, null); - } - }).execute(); - } - } else { - downloadFinished(null, destinationFileName, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - downloadFinished(null, destinationFileName, ERROR_VIDEO_FILE_CANNOT_DOWNLOAD); - } - }); - } else { - downloadFinished(null, destinationFileName, ERROR_CANNOT_GET_CACHE_DIRECTORY); - } - - return START_NOT_STICKY; - } - - private void downloadFinished(Uri destinationFileUri, String fileName, int errorCode) { - if (errorCode != NO_ERROR) { - switch (errorCode) { - case ERROR_CANNOT_GET_CACHE_DIRECTORY: - updateNotification(R.string.downloading_reddit_video_failed_cannot_get_cache_directory, fileName, null); - break; - case ERROR_VIDEO_FILE_CANNOT_DOWNLOAD: - updateNotification(R.string.downloading_reddit_video_failed_cannot_download_video, fileName, null); - break; - case ERROR_VIDEO_FILE_CANNOT_SAVE: - updateNotification(R.string.downloading_reddit_video_failed_cannot_save_video, fileName, null); - break; - case ERROR_AUDIO_FILE_CANNOT_SAVE: - updateNotification(R.string.downloading_reddit_video_failed_cannot_save_audio, fileName, null); - break; - case ERROR_MUX_FAILED: - updateNotification(R.string.downloading_reddit_video_failed_cannot_mux, fileName, null); - break; - case ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE: - updateNotification(R.string.downloading_reddit_video_failed_cannot_save_mux_video, fileName, null); - break; - } - EventBus.getDefault().post(new DownloadRedditVideoEvent(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, "video/*"); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - updateNotification(R.string.downloading_reddit_video_finished, fileName, pendingIntent); - EventBus.getDefault().post(new DownloadRedditVideoEvent(true)); - } - ); - } - stopForeground(false); - } - - private Notification createNotification(int stringResId, String fileName, PendingIntent pendingIntent) { - NotificationCompat.Builder builder; - if (isRedditVideo) { - builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO); - } else { - builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD_VIDEO); - } - 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) { - if (isRedditVideo) { - notificationManager.notify(NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID, - createNotification(stringResId, fileName, pendingIntent)); - } else { - notificationManager.notify(NotificationUtils.DOWNLOAD_VIDEO_NOTIFICATION_ID, - createNotification(stringResId, fileName, pendingIntent)); - } - } - } - - private static class SaveTempMuxAndCopyAsyncTask extends AsyncTask { - - private ResponseBody videoResponse; - private ResponseBody audioResponse; - private String videoFilePath; - private String audioFilePath; - private String outputFilePath; - private String destinationFileName; - private ContentResolver contentResolver; - private SaveTempMuxAndCopyAsyncTaskListener saveTempMuxAndCopyAsyncTaskListener; - private Uri destinationFileUri; - private int errorCode = NO_ERROR; - - public SaveTempMuxAndCopyAsyncTask(ResponseBody videoResponse, ResponseBody audioResponse, - String videoFilePath, String audioFilePath, String outputFilePath, - String destinationFileName, ContentResolver contentResolver, - SaveTempMuxAndCopyAsyncTaskListener saveTempMuxAndCopyAsyncTaskListener) { - this.videoResponse = videoResponse; - this.audioResponse = audioResponse; - this.videoFilePath = videoFilePath; - this.audioFilePath = audioFilePath; - this.outputFilePath = outputFilePath; - this.destinationFileName = destinationFileName; - this.contentResolver = contentResolver; - this.saveTempMuxAndCopyAsyncTaskListener = saveTempMuxAndCopyAsyncTaskListener; - } - - @Override - protected void onProgressUpdate(Integer... values) { - super.onProgressUpdate(values); - saveTempMuxAndCopyAsyncTaskListener.updateProgressNotification(values[0]); - } - - @Override - protected Void doInBackground(Void... voids) { - publishProgress(R.string.downloading_reddit_video_save_video); - String savedVideoFilePath = writeResponseBodyToDisk(videoResponse, videoFilePath); - if (savedVideoFilePath == null) { - errorCode = ERROR_VIDEO_FILE_CANNOT_SAVE; - return null; - } - if (audioResponse != null) { - publishProgress(R.string.downloading_reddit_video_save_audio); - String savedAudioFilePath = writeResponseBodyToDisk(audioResponse, audioFilePath); - if (savedAudioFilePath == null) { - errorCode = ERROR_AUDIO_FILE_CANNOT_SAVE; - return null; - } - - publishProgress(R.string.downloading_reddit_video_muxing); - if (!muxVideoAndAudio(videoFilePath, audioFilePath, outputFilePath)) { - errorCode = ERROR_MUX_FAILED; - return null; - } - - publishProgress(R.string.downloading_reddit_video_save_file_to_public_dir); - copyToDestination(outputFilePath); - } else { - publishProgress(R.string.downloading_reddit_video_save_file_to_public_dir); - copyToDestination(videoFilePath); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - saveTempMuxAndCopyAsyncTaskListener.finished(destinationFileUri, errorCode); - } - - private String writeResponseBodyToDisk(ResponseBody body, String filePath) { - try { - File file = new File(filePath); - - InputStream inputStream = null; - OutputStream outputStream = null; - - try { - byte[] fileReader = new byte[4096]; - - long fileSize = body.contentLength(); - long fileSizeDownloaded = 0; - - inputStream = body.byteStream(); - outputStream = new FileOutputStream(file); - - while (true) { - int read = inputStream.read(fileReader); - - if (read == -1) { - break; - } - - outputStream.write(fileReader, 0, read); - - fileSizeDownloaded += read; - } - - outputStream.flush(); - - return file.getPath(); - } catch (IOException e) { - return null; - } finally { - if (inputStream != null) { - inputStream.close(); - } - - if (outputStream != null) { - outputStream.close(); - } - } - } catch (IOException e) { - return null; - } - } - - private boolean muxVideoAndAudio(String videoFilePath, String audioFilePath, String outputFilePath) { - try { - File file = new File(outputFilePath); - file.createNewFile(); - MediaExtractor videoExtractor = new MediaExtractor(); - videoExtractor.setDataSource(videoFilePath); - MediaExtractor audioExtractor = new MediaExtractor(); - audioExtractor.setDataSource(audioFilePath); - MediaMuxer muxer = new MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); - - videoExtractor.selectTrack(0); - MediaFormat videoFormat = videoExtractor.getTrackFormat(0); - int videoTrack = muxer.addTrack(videoFormat); - - audioExtractor.selectTrack(0); - MediaFormat audioFormat = audioExtractor.getTrackFormat(0); - int audioTrack = muxer.addTrack(audioFormat); - boolean sawEOS = false; - int offset = 100; - int sampleSize = 2048 * 1024; - ByteBuffer videoBuf = ByteBuffer.allocate(sampleSize); - ByteBuffer audioBuf = ByteBuffer.allocate(sampleSize); - MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo(); - MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo(); - - videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); - audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); - - muxer.start(); - - while (!sawEOS) { - videoBufferInfo.offset = offset; - videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset); - - if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) { - sawEOS = true; - videoBufferInfo.size = 0; - } else { - videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime(); - videoBufferInfo.flags = videoExtractor.getSampleFlags(); - muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo); - videoExtractor.advance(); - } - } - - boolean sawEOS2 = false; - while (!sawEOS2) { - audioBufferInfo.offset = offset; - audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset); - - if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) { - sawEOS2 = true; - audioBufferInfo.size = 0; - } else { - audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime(); - audioBufferInfo.flags = audioExtractor.getSampleFlags(); - muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo); - audioExtractor.advance(); - - } - } - - try { - muxer.stop(); - muxer.release(); - } catch (IllegalStateException ignore) {} - } catch (IOException e) { - e.printStackTrace(); - return false; - } - - return true; - } - - private void copyToDestination(String srcPath) { - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - if (!copy(new File(srcPath), destinationFileName)) { - errorCode = ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE; - } - } else { - try { - copyFileQ(new File(srcPath), destinationFileName); - } catch (IOException e) { - e.printStackTrace(); - errorCode = ERROR_MUXED_VIDEO_FILE_CANNOT_SAVE; - } - } - } - - @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); - - OutputStream stream = null; - Uri uri = null; - - try { - final Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - uri = contentResolver.insert(contentUri, contentValues); - - if (uri == null) { - throw new IOException("Failed to create new MediaStore record."); - } - - stream = contentResolver.openOutputStream(uri); - - if (stream == null) { - throw new IOException("Failed to get output stream."); - } - - InputStream in = new FileInputStream(src); - - 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); - destinationFileUri = uri; - } catch (IOException e) { - if (uri != null) { - // Don't leave an orphan entry in the MediaStore - contentResolver.delete(uri, null, null); - } - - throw e; - } finally { - if (stream != null) { - stream.close(); - } - } - } - - private boolean copy(File src, String outputFileName) { - File directory = getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); - if (directory != null) { - String directoryPath = directory.getAbsolutePath() + "/Infinity/"; - String destinationFilePath = directoryPath + outputFileName; - destinationFileUri = Uri.parse(destinationFilePath); - - try (InputStream in = new FileInputStream(src)) { - try (OutputStream out = new FileOutputStream(destinationFilePath)) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - - src.delete(); - return true; - } - } catch (IOException e) { - e.printStackTrace(); - src.delete(); - return false; - } - } else { - src.delete(); - return false; - } - } - - interface SaveTempMuxAndCopyAsyncTaskListener { - void finished(Uri destinationFileUri, int errorCode); - void updateProgressNotification(int stringResId); - } - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d31c088..b694941d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -843,18 +843,16 @@ Download failed: cannot mux video and audio Download failed: cannot save the video to public directory Downloading Video + Saving Video Downloading Image Downloading Gif Saving Image Saving Gif - Downloaded - Downloaded + Downloaded Download failed: cannot access destination directory - Download image failed - Download failed: cannot save image to destination directory - Download gif failed - Download failed: cannot save gif to destination directory + Download failed + Download failed: cannot save the file to destination directory Wallpaper set Cannot set wallpaper -- cgit v1.2.3