diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9980720a..2a2a74bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,11 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:replace="android:label"> + + + android:windowSoftInputMode="adjustResize" /> downloadFile(@Url String fileUrl); +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AccessTokenAuthenticator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AccessTokenAuthenticator.java index 709a8cd5..408d180d 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AccessTokenAuthenticator.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AccessTokenAuthenticator.java @@ -67,7 +67,7 @@ class AccessTokenAuthenticator implements Authenticator { Call accessTokenCall = api.getAccessToken(APIUtils.getHttpBasicAuthHeader(), params); try { - retrofit2.Response response = accessTokenCall.execute(); + retrofit2.Response response = accessTokenCall.execute(); if (response.isSuccessful() && response.body() != null) { JSONObject jsonObject = new JSONObject((String) response.body()); String newAccessToken = jsonObject.getString(APIUtils.ACCESS_TOKEN_KEY); 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 29b78878..0bd5f7fc 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Activity/ViewVideoActivity.java @@ -59,6 +59,7 @@ import butterknife.ButterKnife; import ml.docilealligator.infinityforreddit.FetchGfycatVideoLinks; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.Service.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.Utils.SharedPreferencesUtils; import retrofit2.Retrofit; @@ -94,11 +95,14 @@ public class ViewVideoActivity extends AppCompatActivity { private String videoDownloadUrl; private String videoFileName; + private String subredditName; + private String id; private boolean wasPlaying; private boolean isDownloading = false; private boolean isMute = false; private String postTitle; private long resumePosition = -1; + private int videoType; @Inject @Named("gfycat") @@ -181,7 +185,7 @@ public class ViewVideoActivity extends AppCompatActivity { TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector); videoPlayerView.setPlayer(player); - int videoType = getIntent().getIntExtra(EXTRA_VIDEO_TYPE, VIDEO_TYPE_NORMAL); + videoType = getIntent().getIntExtra(EXTRA_VIDEO_TYPE, VIDEO_TYPE_NORMAL); if (videoType == VIDEO_TYPE_GFYCAT || videoType == VIDEO_TYPE_REDGIFS) { if (savedInstanceState != null) { String videoUrl = savedInstanceState.getString(VIDEO_URI_STATE); @@ -243,7 +247,9 @@ public class ViewVideoActivity extends AppCompatActivity { player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); } else { videoDownloadUrl = intent.getStringExtra(EXTRA_VIDEO_DOWNLOAD_URL); - videoFileName = intent.getStringExtra(EXTRA_SUBREDDIT) + "-" + intent.getStringExtra(EXTRA_ID) + ".mp4"; + subredditName = intent.getStringExtra(EXTRA_SUBREDDIT); + id = intent.getStringExtra(EXTRA_ID); + videoFileName = subredditName + "-" + id + ".mp4"; // Produces DataSource instances through which media data is loaded. dataSourceFactory = new DefaultHttpDataSourceFactory(Util.getUserAgent(this, "Infinity")); // Prepare the player with the source. @@ -387,46 +393,59 @@ public class ViewVideoActivity extends AppCompatActivity { private void download() { isDownloading = false; - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(videoDownloadUrl)); - request.setTitle(videoFileName); + if (videoType != VIDEO_TYPE_NORMAL) { + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(videoDownloadUrl)); + request.setTitle(videoFileName); - request.allowScanningByMediaScanner(); - request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.allowScanningByMediaScanner(); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - //Android Q support - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, videoFileName); - } else { - String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString(); - File directory = new File(path + "/Infinity/"); - boolean saveToInfinityFolder = true; - if (!directory.exists()) { - if (!directory.mkdir()) { - saveToInfinityFolder = false; - } + //Android Q support + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, videoFileName); } else { - if (directory.isFile()) { - if (!(directory.delete() && directory.mkdir())) { + String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString(); + File directory = new File(path + "/Infinity/"); + boolean saveToInfinityFolder = true; + if (!directory.exists()) { + if (!directory.mkdir()) { saveToInfinityFolder = false; } + } else { + if (directory.isFile()) { + if (!(directory.delete() && directory.mkdir())) { + saveToInfinityFolder = false; + } + } + } + + if (saveToInfinityFolder) { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES + "/Infinity/", videoFileName); + } else { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, videoFileName); } } - if (saveToInfinityFolder) { - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES + "/Infinity/", videoFileName); + DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); + + if (manager == null) { + Toast.makeText(this, R.string.download_failed, Toast.LENGTH_SHORT).show(); + return; + } + + manager.enqueue(request); + } else { + 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); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); } else { - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, videoFileName); + startService(intent); } } - - DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); - - if (manager == null) { - Toast.makeText(this, R.string.download_failed, Toast.LENGTH_SHORT).show(); - return; - } - - manager.enqueue(request); Toast.makeText(this, R.string.download_started, Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Adapter/PostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Adapter/PostRecyclerViewAdapter.java index eea62a78..07009344 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Adapter/PostRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Adapter/PostRecyclerViewAdapter.java @@ -879,7 +879,7 @@ public class PostRecyclerViewAdapter extends PagedListAdapter= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel = new NotificationChannel( + NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO, + NotificationUtils.CHANNEL_DOWNLOAD_REDDIT_VIDEO, + NotificationManager.IMPORTANCE_LOW + ); + + NotificationManagerCompat manager = NotificationManagerCompat.from(this); + manager.createNotificationChannel(serviceChannel); + } + + startForeground(NotificationUtils.DOWNLOAD_REDDIT_VIDEO_NOTIFICATION_ID, createNotification(R.string.downloading_reddit_video)); + + ml.docilealligator.infinityforreddit.API.DownloadRedditVideo downloadRedditVideo = retrofit.create(ml.docilealligator.infinityforreddit.API.DownloadRedditVideo.class); + downloadRedditVideo.downloadFile(videoUrl).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response videoResponse) { + if (videoResponse.isSuccessful() && videoResponse.body() != null) { + String fileNameWithoutExtension = intent.getStringExtra(EXTRA_SUBREDDIT) + + "-" + intent.getStringExtra(EXTRA_POST_ID); + + downloadRedditVideo.downloadFile(audioUrl).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response audioResponse) { + File directory = getExternalCacheDir(); + if (directory != null) { + String directoryPath = directory.getAbsolutePath() + "/"; + if (audioResponse.isSuccessful() && audioResponse.body() != null) { + String videoFilePath = writeResponseBodyToDisk(videoResponse.body(), directoryPath + fileNameWithoutExtension+ "-cache.mp4"); + if (videoFilePath != null) { + String audioFilePath = writeResponseBodyToDisk(audioResponse.body(), directoryPath + fileNameWithoutExtension + "-cache.mp3"); + if (audioFilePath != null) { + String outputFilePath = directoryPath + fileNameWithoutExtension + ".mp4"; + if(muxVideoAndAudio(videoFilePath, audioFilePath, outputFilePath)) { + new CopyFileAsyncTask(new File(outputFilePath), fileNameWithoutExtension + ".mp4", + getContentResolver(), new CopyFileAsyncTask.CopyFileAsyncTaskListener() { + @Override + public void successful() { + new File(videoFilePath).delete(); + new File(audioFilePath).delete(); + new File(outputFilePath).delete(); + + EventBus.getDefault().post(new DownloadRedditVideoEvent(true)); + + stopService(); + } + + @Override + public void failed() { + new File(videoFilePath).delete(); + new File(audioFilePath).delete(); + new File(outputFilePath).delete(); + + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + }).execute(); + } else { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } else { + new File(videoFilePath).delete(); + + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } else { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } else { + //No audio + String videoFilePath = writeResponseBodyToDisk(videoResponse.body(), directoryPath + fileNameWithoutExtension+ ".mp4"); + if (videoFilePath != null) { + new CopyFileAsyncTask(new File(videoFilePath), fileNameWithoutExtension + ".mp4", + getContentResolver(), new CopyFileAsyncTask.CopyFileAsyncTaskListener() { + @Override + public void successful() { + new File(videoFilePath).delete(); + + EventBus.getDefault().post(new DownloadRedditVideoEvent(true)); + + stopService(); + } + + @Override + public void failed() { + new File(videoFilePath).delete(); + + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + }).execute(); + } else { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } + } else { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + }); + } else { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + EventBus.getDefault().post(new DownloadRedditVideoEvent(false)); + + stopService(); + } + }); + + return START_NOT_STICKY; + } + + private Notification createNotification(int stringResId) { + return new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD_REDDIT_VIDEO) + .setContentTitle(getString(stringResId)) + .setContentText(getString(R.string.please_wait)) + .setSmallIcon(R.drawable.ic_notification) + .setColor(mCustomThemeWrapper.getColorPrimaryLightTheme()) + .build(); + } + + 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; + + Log.i("asdfsadf", "file download: " + fileSizeDownloaded + " of " + fileSize); + } + + 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 = 256 * 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) + { + // Log.d(TAG, "saw input EOS."); + 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(); + + } + } + + muxer.stop(); + muxer.release(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + } + + private void stopService() { + stopForeground(true); + stopSelf(); + } + + private static class CopyFileAsyncTask extends AsyncTask { + private File src; + private String destinationFileName; + private ContentResolver contentResolver; + private CopyFileAsyncTaskListener copyFileAsyncTaskListener; + private boolean successful; + + interface CopyFileAsyncTaskListener { + void successful(); + void failed(); + } + + CopyFileAsyncTask(File src, String destinationFileName, ContentResolver contentResolver, CopyFileAsyncTaskListener copyFileAsyncTaskListener) { + this.src = src; + this.destinationFileName = destinationFileName; + this.contentResolver = contentResolver; + this.copyFileAsyncTaskListener = copyFileAsyncTaskListener; + } + + @Override + protected Void doInBackground(Void... voids) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + successful = copy(src, destinationFileName); + } else { + try { + copyFileQ(src, destinationFileName); + successful = true; + } catch (IOException e) { + e.printStackTrace(); + successful = false; + } + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + if (successful) { + copyFileAsyncTaskListener.successful(); + } else { + copyFileAsyncTaskListener.failed(); + } + } + + @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.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); + } 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/";; + File dst = new File(directoryPath, outputFileName); + + try (InputStream in = new FileInputStream(src)) { + try (OutputStream out = new FileOutputStream(dst)) { + 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; + } + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/SubmitPostService.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/SubmitPostService.java index cedf9730..5177e415 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Service/SubmitPostService.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Service/SubmitPostService.java @@ -4,7 +4,6 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; @@ -110,22 +109,22 @@ public class SubmitPostService extends Service { if (postType == EXTRA_POST_TEXT_OR_LINK) { content = intent.getStringExtra(EXTRA_CONTENT); kind = intent.getStringExtra(EXTRA_KIND); - startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(this, R.string.posting)); + startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(R.string.posting)); submitTextOrLinkPost(); } else if (postType == EXTRA_POST_TYPE_IMAGE) { mediaUri = intent.getData(); - startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(this, R.string.posting_image)); + startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(R.string.posting_image)); submitImagePost(); } else { mediaUri = intent.getData(); - startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(this, R.string.posting_video)); + startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID, createNotification(R.string.posting_video)); submitVideoPost(); } return START_NOT_STICKY; } - private Notification createNotification(Context context, int stringResId) { + private Notification createNotification(int stringResId) { return new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_SUBMIT_POST) .setContentTitle(getString(stringResId)) .setContentText(getString(R.string.please_wait)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc088ae2..6100247c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -739,7 +739,7 @@ Fetching video info. Please wait. Cannot load images - - Hello blank fragment + + Downloading video.