diff --git a/.gitignore b/.gitignore
index aec5bdaf..b071bd2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,18 @@ version
.pub-cache/
.pub/
build/
+.idea/
+
+android/.classpath
+android/.project
+android/.settings
+android/.settings/
+android/example/.classpath
+android/example/.project
+android/example/.settings
+android/example/.settings/
+example/ios/Flutter/flutter_export_environment.sh
+.vscode/
flutter_*.png
linked_*.ds
unlinked.ds
@@ -95,4 +107,4 @@ coverage/
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
-!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
\ No newline at end of file
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94559a48..cc6cc24d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,9 @@
+## 1.7.2 - 24-01-2022
+
+* fix bug on Android
+* upgrade Android dependencies (WorkerManager 2.7.1)
+
## 1.7.1 - 08-10-2021
* fix bug resume download on Android
diff --git a/android/.gitignore b/android/.gitignore
index c6cbe562..55ac714b 100644
--- a/android/.gitignore
+++ b/android/.gitignore
@@ -6,3 +6,6 @@
.DS_Store
/build
/captures
+/.classpath
+/.project
+/.settings
\ No newline at end of file
diff --git a/android/.idea/compiler.xml b/android/.idea/compiler.xml
index a1757ae5..3058433f 100644
--- a/android/.idea/compiler.xml
+++ b/android/.idea/compiler.xml
@@ -4,5 +4,6 @@
+
\ No newline at end of file
diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml
index e22b5728..f894050f 100644
--- a/android/.idea/gradle.xml
+++ b/android/.idea/gradle.xml
@@ -1,11 +1,10 @@
+
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
index 299e423c..e3f3f991 100644
--- a/android/.idea/misc.xml
+++ b/android/.idea/misc.xml
@@ -39,7 +39,7 @@
-
+
diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml
deleted file mode 100644
index 2fcf3d6b..00000000
--- a/android/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml
deleted file mode 100644
index 7f68460d..00000000
--- a/android/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
index 7374e29c..bbf46e1d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.6.4'
+ classpath 'com.android.tools.build:gradle:4.1.0'
}
}
@@ -38,8 +38,12 @@ android {
}
dependencies {
- implementation 'androidx.work:work-runtime:2.7.0-rc01'
+ implementation 'androidx.work:work-runtime:2.7.1'
implementation 'androidx.annotation:annotation:1.2.0'
implementation 'androidx.core:core:1.6.0'
implementation 'androidx.fragment:fragment:1.3.6'
+ implementation 'com.mpatric:mp3agic:0.9.0'
+// implementation files('/Users/trope/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar')
+
+
}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index da9702f9..14e30f74 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadQueue.java b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadQueue.java
new file mode 100644
index 00000000..0c7d6a03
--- /dev/null
+++ b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadQueue.java
@@ -0,0 +1,73 @@
+package vn.hunghd.flutterdownloader;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.work.WorkManager;
+import androidx.work.WorkRequest;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.UUID;
+
+public final class DownloadQueue {
+ private static DownloadQueue instance;
+ private final Queue queue = new LinkedList<>();
+ private final Map queueMap = new HashMap<>();
+
+ private Context context;
+ private boolean nothingOnWorker = true;
+
+ private DownloadQueue(Context context) {
+ this.context = context;
+ }
+
+ public static DownloadQueue getInstance(Context context) {
+ if (instance == null) {
+ instance = new DownloadQueue(context);
+ }
+ return instance;
+ }
+
+ public void doQueue() {
+ final UUID id = queue.poll();
+ if (id == null) {
+ reset();
+ } else {
+ if (queueMap.containsKey(id)){
+ final WorkRequest request = queueMap.get(id);
+ startWork(request);
+ } else {
+ doQueue();
+ }
+ }
+ }
+ private void startWork(WorkRequest e){
+ nothingOnWorker = false;
+ WorkManager.getInstance(context).enqueue(e);
+ }
+ public void reset() {
+ queue.clear();
+ queueMap.clear();
+ nothingOnWorker = true;
+ }
+
+ public void removeTask(UUID e) {
+ if(queue.remove(e)){
+ queueMap.remove(e);
+ if(queue.isEmpty()){
+ nothingOnWorker = true;
+ }
+ }
+ }
+ public void add(WorkRequest e) {
+ if (queue.isEmpty() && nothingOnWorker) {
+ startWork(e);
+ } else {
+ queue.add(e.getId());
+ queueMap.put(e.getId(),e);
+ }
+ }
+}
diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java
index 2cd6162e..d6ac51b0 100644
--- a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java
+++ b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java
@@ -15,9 +15,16 @@ public class DownloadTask {
boolean openFileFromNotification;
long timeCreated;
boolean saveInPublicStorage;
+ String albumName;
+ String artistName;
+ String artistId;
+ String playlistId;
+ String albumId;
+ String musicId;
DownloadTask(int primaryId, String taskId, int status, int progress, String url, String filename, String savedDir,
- String headers, String mimeType, boolean resumable, boolean showNotification, boolean openFileFromNotification, long timeCreated, boolean saveInPublicStorage) {
+ String headers, String mimeType, boolean resumable, boolean showNotification, boolean openFileFromNotification,
+ long timeCreated, boolean saveInPublicStorage, String albumName, String artistName, String artistId, String playlistId, String albumId, String musicId) {
this.primaryId = primaryId;
this.taskId = taskId;
this.status = status;
@@ -31,11 +38,23 @@ public class DownloadTask {
this.showNotification = showNotification;
this.openFileFromNotification = openFileFromNotification;
this.timeCreated = timeCreated;
+ this.albumName = albumName;
+ this.artistName = artistName;
+ this.artistId = artistId;
+ this.playlistId = playlistId;
+ this.albumId = albumId;
+ this.musicId = musicId;
this.saveInPublicStorage = saveInPublicStorage;
}
@Override
public String toString() {
- return "DownloadTask{taskId=" + taskId + ",status=" + status + ",progress=" + progress + ",url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",headers=" + headers + ", saveInPublicStorage= " + saveInPublicStorage + "}";
+ return "DownloadTask{taskId=" + taskId + ",status=" + status + ",progress=" + progress +
+ ",url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",headers=" +
+ headers + "saveInPublicStorage= " + saveInPublicStorage + ",albumName=" + albumName + ",artistName=" + artistName +
+ ",artistId=" + artistId +
+ ",playlistId=" + playlistId +
+ ",albumId=" + albumId +
+ ",musicId=" + musicId + "}";
}
}
diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java
index ccecdf63..e9c970c9 100644
--- a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java
+++ b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java
@@ -16,19 +16,26 @@
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.mpatric.mp3agic.AbstractID3v2Tag;
+import com.mpatric.mp3agic.ID3v1Tag;
+import com.mpatric.mp3agic.ID3v22Tag;
+import com.mpatric.mp3agic.ID3v23Tag;
+import com.mpatric.mp3agic.ID3v24Tag;
+import com.mpatric.mp3agic.Mp3File;
import org.json.JSONException;
import org.json.JSONObject;
@@ -47,20 +54,16 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
-
import io.flutter.FlutterInjector;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.dart.DartExecutor;
-import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
-import io.flutter.plugin.common.PluginRegistry;
import io.flutter.view.FlutterCallbackInformation;
public class DownloadWorker extends Worker implements MethodChannel.MethodCallHandler {
@@ -73,6 +76,18 @@ public class DownloadWorker extends Worker implements MethodChannel.MethodCallHa
public static final String ARG_OPEN_FILE_FROM_NOTIFICATION = "open_file_from_notification";
public static final String ARG_CALLBACK_HANDLE = "callback_handle";
public static final String ARG_DEBUG = "debug";
+ public static final String ARG_MUSIC_ARTIST = "music_artist";
+ public static final String ARG_MUSIC_ALBUM = "music_album";
+ public static final String ARG_SM_EXTRAS = "sm_extras";
+ public static final String ARG_ARTIST_ID = "artist_id";
+ public static final String ARG_PLAYLIST_ID = "playlist_id";
+ public static final String ARG_ALBUM_ID = "album_id";
+ public static final String ARG_MUSIC_ID = "music_id";
+ public static final String ARG_QUEUE_ENABLED = "queue_enabled";
+
+ public static final String IS_PENDING = "is_pending";
+ public static final String USER_AGENT = "SuaMusica/downloader (Linux; Android "
+ + Build.VERSION.SDK_INT + "; " + Build.BRAND + "/" + Build.MODEL + ")";
public static final String ARG_SAVE_IN_PUBLIC_STORAGE = "save_in_public_storage";
private static final String TAG = DownloadWorker.class.getSimpleName();
@@ -87,21 +102,32 @@ public class DownloadWorker extends Worker implements MethodChannel.MethodCallHa
private final Pattern charsetPattern = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
private final Pattern filenameStarPattern = Pattern.compile("(?i)\\bfilename\\*=([^']+)'([^']*)'\"?([^\"]+)\"?");
private final Pattern filenamePattern = Pattern.compile("(?i)\\bfilename=\"?([^\"]+)\"?");
-
private MethodChannel backgroundChannel;
private TaskDbHelper dbHelper;
private TaskDao taskDao;
private boolean showNotification;
private boolean clickToOpenDownloadedFile;
private boolean debug;
+ private boolean queueEnabled;
private int lastProgress = 0;
private int primaryId;
- private String msgStarted, msgInProgress, msgCanceled, msgFailed, msgPaused, msgComplete;
+ private String msgStarted;
+ private String msgInProgress;
+ private String msgCanceled;
+ private String msgFailed;
+ private String msgPaused;
+ private String msgComplete;
+ private String argMusicArtist;
+ private String argMusicAlbum;
+ private String argArtistId;
+ private String argPlaylistId;
+ private String argAlbumId;
+ private String argMusicId;
+ private String argSMExtras;
private long lastCallUpdateNotification = 0;
private boolean saveInPublicStorage;
- public DownloadWorker(@NonNull final Context context,
- @NonNull WorkerParameters params) {
+ public DownloadWorker(@NonNull final Context context, @NonNull WorkerParameters params) {
super(context, params);
new Handler(context.getMainLooper()).post(new Runnable() {
@@ -118,31 +144,24 @@ private void startBackgroundIsolate(Context context) {
SharedPreferences pref = context.getSharedPreferences(FlutterDownloaderPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
long callbackHandle = pref.getLong(FlutterDownloaderPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, 0);
- String appBundlePath = FlutterInjector.instance().flutterLoader().findAppBundlePath();;
- AssetManager assets = context.getAssets();
+ backgroundFlutterEngine = new FlutterEngine(getApplicationContext(), null, false);
// We need to create an instance of `FlutterEngine` before looking up the
// callback. If we don't, the callback cache won't be initialized and the
// lookup will fail.
FlutterCallbackInformation flutterCallback =
FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
-
- backgroundFlutterEngine = new FlutterEngine(context);
-
- DartExecutor executor = backgroundFlutterEngine.getDartExecutor();
- DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(assets, appBundlePath, flutterCallback);
- executor.executeDartCallback(dartCallback);
-
- /// backward compatibility with V1 embedding
- if (getApplicationContext() instanceof PluginRegistry.PluginRegistrantCallback) {
- PluginRegistry.PluginRegistrantCallback pluginRegistrantCallback = (PluginRegistry.PluginRegistrantCallback) getApplicationContext();
- pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine));
+ if (flutterCallback == null) {
+ log("Fatal: failed to find callback");
+ return;
}
+
+ final String appBundlePath = FlutterInjector.instance().flutterLoader().findAppBundlePath();
+ final AssetManager assets = getApplicationContext().getAssets();
+ backgroundFlutterEngine.getDartExecutor().executeDartCallback(new DartExecutor.DartCallback(assets, appBundlePath, flutterCallback));
}
}
-
- DartExecutor executor = backgroundFlutterEngine.getDartExecutor();
- backgroundChannel = new MethodChannel(executor, "vn.hunghd/downloader_background");
+ backgroundChannel = new MethodChannel(backgroundFlutterEngine.getDartExecutor(), "vn.hunghd/downloader_background");
backgroundChannel.setMethodCallHandler(this);
}
@@ -161,25 +180,26 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
}
}
-// @Override
-// public void onStopped() {
-// Context context = getApplicationContext();
-// dbHelper = TaskDbHelper.getInstance(context);
-// taskDao = new TaskDao(dbHelper);
+ @Override
+ public void onStopped() {
+ Context context = getApplicationContext();
+ dbHelper = TaskDbHelper.getInstance(context);
+ taskDao = new TaskDao(dbHelper);
-// String url = getInputData().getString(ARG_URL);
-// String filename = getInputData().getString(ARG_FILE_NAME);
+ String url = getInputData().getString(ARG_URL);
+ String filename = getInputData().getString(ARG_FILE_NAME);
-// DownloadTask task = taskDao.loadTask(getId().toString());
-// if (task.status == DownloadStatus.ENQUEUED) {
-// updateNotification(context, filename == null ? url : filename, DownloadStatus.CANCELED, -1, null, true);
-// taskDao.updateTask(getId().toString(), DownloadStatus.CANCELED, lastProgress);
-// }
-// }
+ DownloadTask task = taskDao.loadTask(getId().toString());
+ if (task != null && task.status == DownloadStatus.ENQUEUED) {
+ updateNotification(context, filename == null ? url : filename, DownloadStatus.CANCELED, -1, null, true,"");
+ taskDao.updateTask(getId().toString(), DownloadStatus.CANCELED, lastProgress);
+ }
+ }
@NonNull
@Override
public Result doWork() {
+
Context context = getApplicationContext();
dbHelper = TaskDbHelper.getInstance(context);
taskDao = new TaskDao(dbHelper);
@@ -190,7 +210,13 @@ public Result doWork() {
String headers = getInputData().getString(ARG_HEADERS);
boolean isResume = getInputData().getBoolean(ARG_IS_RESUME, false);
debug = getInputData().getBoolean(ARG_DEBUG, false);
-
+ argMusicArtist = getInputData().getString(ARG_MUSIC_ARTIST);
+ argMusicAlbum = getInputData().getString(ARG_MUSIC_ALBUM);
+ argArtistId = getInputData().getString(ARG_ARTIST_ID);
+ argPlaylistId = getInputData().getString(ARG_PLAYLIST_ID);
+ argAlbumId = getInputData().getString(ARG_ALBUM_ID);
+ argMusicId = getInputData().getString(ARG_MUSIC_ID);
+ queueEnabled = getInputData().getBoolean(ARG_QUEUE_ENABLED, false);
Resources res = getApplicationContext().getResources();
msgStarted = res.getString(R.string.flutter_downloader_notification_started);
msgInProgress = res.getString(R.string.flutter_downloader_notification_in_progress);
@@ -199,6 +225,13 @@ public Result doWork() {
msgPaused = res.getString(R.string.flutter_downloader_notification_paused);
msgComplete = res.getString(R.string.flutter_downloader_notification_complete);
+ Log.i(TAG, "DownloadWorker{url=" + url + ",filename=" + filename + ",savedDir=" + savedDir
+ + ",header=" + headers + ",isResume=" + isResume + ",argMusicArtist=" + argMusicArtist
+ + ",argMusicAlbum=" + argMusicAlbum + ",argArtistId=" + argArtistId +
+ ",argArtistId=" + argArtistId + ",argPlaylistId=" + argPlaylistId +
+ ",argAlbumId=" + argAlbumId + ",argMusicId=" + argMusicId + ",argSMExtras="
+ + argSMExtras + ", queueEnabled = " + queueEnabled);
+
DownloadTask task = taskDao.loadTask(getId().toString());
log("DownloadWorker{url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",header=" + headers + ",isResume=" + isResume + ",status=" + (task != null ? task.status : "GONE"));
@@ -216,17 +249,18 @@ public Result doWork() {
setupNotification(context);
- updateNotification(context, filename == null ? url : filename, DownloadStatus.RUNNING, task.progress, null, false);
+ updateNotification(context, filename == null ? url : filename, DownloadStatus.RUNNING,
+ task.progress, null, false, "");
taskDao.updateTask(getId().toString(), DownloadStatus.RUNNING, task.progress);
//automatic resume for partial files. (if the workmanager unexpectedly quited in background)
String saveFilePath = savedDir + File.separator + filename;
- File partialFile = new File(saveFilePath);
- if (partialFile.exists()) {
- isResume = true;
- log("exists file for "+ filename + "automatic resuming...");
- }
-
+ // File partialFile = new File(saveFilePath);
+ // if (partialFile.exists()) {
+ // log("exists file for " + filename + "automatic resuming...");
+ // }
+ //Disabling resume because our server does not accepts it.
+ isResume = false;
try {
downloadFile(context, url, savedDir, filename, headers, isResume);
cleanUp();
@@ -234,7 +268,9 @@ public Result doWork() {
taskDao = null;
return Result.success();
} catch (Exception e) {
- updateNotification(context, filename == null ? url : filename, DownloadStatus.FAILED, -1, null, true);
+ String errorMessage = e.getMessage();
+ updateNotification(context, filename == null ? url : filename, DownloadStatus.FAILED,
+ -1, null, true, (errorMessage != null) ? errorMessage : "No Message");
taskDao.updateTask(getId().toString(), DownloadStatus.FAILED, lastProgress);
e.printStackTrace();
dbHelper = null;
@@ -259,7 +295,8 @@ private void setupHeaders(HttpURLConnection conn, String headers) {
}
}
- private long setupPartialDownloadedDataHeader(HttpURLConnection conn, String filename, String savedDir) {
+ private long setupPartialDownloadedDataHeader(HttpURLConnection conn, String filename,
+ String savedDir) {
String saveFilePath = savedDir + File.separator + filename;
File partialFile = new File(saveFilePath);
long downloadedBytes = partialFile.length();
@@ -271,7 +308,8 @@ private long setupPartialDownloadedDataHeader(HttpURLConnection conn, String fil
return downloadedBytes;
}
- private void downloadFile(Context context, String fileURL, String savedDir, String filename, String headers, boolean isResume) throws IOException {
+ private void downloadFile(Context context, String fileURL, String savedDir, String filename,
+ String headers, boolean isResume) throws IOException {
String url = fileURL;
URL resourceUrl, base, next;
Map visited;
@@ -304,14 +342,17 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
httpConn.setConnectTimeout(15000);
httpConn.setReadTimeout(15000);
- httpConn.setInstanceFollowRedirects(false); // Make the logic below easier to detect redirections
- httpConn.setRequestProperty("User-Agent", "Mozilla/5.0...");
+ httpConn.setInstanceFollowRedirects(false); // Make the logic below easier to detect
+ // redirections
+ log("Using Agent " + USER_AGENT);
+ httpConn.setRequestProperty("User-Agent", USER_AGENT);
// setup request headers if it is set
setupHeaders(httpConn, headers);
// try to continue downloading a file from its partial downloaded data.
if (isResume) {
- downloadedBytes = setupPartialDownloadedDataHeader(httpConn, filename, savedDir);
+ downloadedBytes =
+ setupPartialDownloadedDataHeader(httpConn, filename, savedDir);
}
responseCode = httpConn.getResponseCode();
@@ -339,6 +380,10 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
if ((responseCode == HttpURLConnection.HTTP_OK || (isResume && responseCode == HttpURLConnection.HTTP_PARTIAL)) && !isStopped()) {
contentType = httpConn.getContentType();
long contentLength = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N ? httpConn.getContentLengthLong() : httpConn.getContentLength();
+
+ if (contentType.contains("multipart/")) {
+ contentType = "application/octet-stream";
+ }
log("Content-Type = " + contentType);
log("Content-Length = " + contentLength);
@@ -385,12 +430,15 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
// From Android 11 onwards, file is only downloaded to app-specific directory (internal storage)
// or public shared download directory (external storage).
// The second option will ignore `savedDir` parameter.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && saveInPublicStorage) {
- Uri uri = createFileInPublicDownloadsDir(filename, contentType);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && saveInPublicStorage) {
+ Set volumeNames = MediaStore.getExternalVolumeNames(context);
+ String firstVolumeName = volumeNames.iterator().next();
+ Uri uri = createFileInPublicMusicDir(filename, savedDir);
savedFilePath = getMediaStoreEntryPathApi29(uri);
outputStream = context.getContentResolver().openOutputStream(uri, "w");
} else {
File file = createFileInAppSpecificDir(filename, savedDir);
+ assert file != null;
savedFilePath = file.getPath();
outputStream = new FileOutputStream(file, false);
}
@@ -405,34 +453,36 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
int progress = (int) ((count * 100) / (contentLength + downloadedBytes));
outputStream.write(buffer, 0, bytesRead);
- if ((lastProgress == 0 || progress > lastProgress + STEP_UPDATE || progress == 100)
- && progress != lastProgress) {
+ if ((lastProgress == 0 || progress > lastProgress + STEP_UPDATE
+ || progress == 100) && progress != lastProgress) {
lastProgress = progress;
-
+ updateNotification(context, filename, DownloadStatus.RUNNING, progress, null, false, "");
// This line possibly causes system overloaded because of accessing to DB too many ?!!!
// but commenting this line causes tasks loaded from DB missing current downloading progress,
// however, this missing data should be temporary and it will be updated as soon as
// a new bunch of data fetched and a notification sent
taskDao.updateTask(getId().toString(), DownloadStatus.RUNNING, progress);
- updateNotification(context, filename, DownloadStatus.RUNNING, progress, null, false);
}
}
DownloadTask task = taskDao.loadTask(getId().toString());
int progress = isStopped() && task.resumable ? lastProgress : 100;
- int status = isStopped() ? (task.resumable ? DownloadStatus.PAUSED : DownloadStatus.CANCELED) : DownloadStatus.COMPLETE;
- int storage = ContextCompat.checkSelfPermission(getApplicationContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ int status = isStopped()
+ ? (task.resumable ? DownloadStatus.PAUSED : DownloadStatus.CANCELED)
+ : DownloadStatus.COMPLETE;
+ int storage = ContextCompat.checkSelfPermission(getApplicationContext(),
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
PendingIntent pendingIntent = null;
if (status == DownloadStatus.COMPLETE) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- if (isImageOrVideoFile(contentType) && isExternalStoragePath(savedFilePath)) {
- addImageOrVideoToGallery(filename, savedFilePath, getContentTypeWithoutCharset(contentType));
+ if (isMediaFile(contentType) && isExternalStoragePath(savedFilePath)) {
+ addMediaToGallery(filename, savedFilePath, getContentTypeWithoutCharset(contentType));
}
}
if (clickToOpenDownloadedFile) {
- if(android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && storage != PackageManager.PERMISSION_GRANTED)
+ if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && storage != PackageManager.PERMISSION_GRANTED)
return;
Intent intent = IntentUtils.validatedFileIntent(getApplicationContext(), savedFilePath, contentType);
if (intent != null) {
@@ -444,20 +494,30 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
}
}
}
+ Log.d(TAG, "===> updateNotification: (filename: " + filename + ", status: " + status
+ + ")");
+ updateNotification(context, filename, status, progress, pendingIntent, true,
+ isStopped() ? "Download canceled" : "");
taskDao.updateTask(getId().toString(), status, progress);
- updateNotification(context, filename, status, progress, pendingIntent, true);
-
log(isStopped() ? "Download canceled" : "File downloaded");
} else {
DownloadTask task = taskDao.loadTask(getId().toString());
- int status = isStopped() ? (task.resumable ? DownloadStatus.PAUSED : DownloadStatus.CANCELED) : DownloadStatus.FAILED;
+ String errorMessage = isStopped() ? "Download canceled"
+ : "Server replied HTTP code: " + responseCode;
+ int status = isStopped()
+ ? (task.resumable ? DownloadStatus.PAUSED : DownloadStatus.CANCELED)
+ : DownloadStatus.FAILED;
+ updateNotification(context, filename == null ? fileURL : filename, status, -1, null,
+ true, errorMessage);
taskDao.updateTask(getId().toString(), status, lastProgress);
- updateNotification(context, filename == null ? fileURL : filename, status, -1, null, true);
- log(isStopped() ? "Download canceled" : "Server replied HTTP code: " + responseCode);
+ log(errorMessage);
}
} catch (IOException e) {
+ String errorMessage = e.getMessage();
+ updateNotification(context, filename == null ? fileURL : filename,
+ DownloadStatus.FAILED, -1, null, true,
+ (errorMessage != null) ? errorMessage : "No Message 2");
taskDao.updateTask(getId().toString(), DownloadStatus.FAILED, lastProgress);
- updateNotification(context, filename == null ? fileURL : filename, DownloadStatus.FAILED, -1, null, true);
e.printStackTrace();
} finally {
if (outputStream != null) {
@@ -487,7 +547,7 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri
private File createFileInAppSpecificDir(String filename, String savedDir) {
File newFile = new File(savedDir, filename);
try {
- boolean rs = newFile.createNewFile();
+ boolean rs = newFile.exists() || newFile.createNewFile();
if(rs) {
return newFile;
} else {
@@ -500,19 +560,59 @@ private File createFileInAppSpecificDir(String filename, String savedDir) {
return null;
}
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ private List getContentUris(@NonNull final Context context) {
+
+ final List allVolumes = new ArrayList<>();
+
+ // Add the internal storage volumes as last resort.
+ // These will be kept at the bottom of the list if
+ // any SD-card volumes are found
+ allVolumes.add(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ allVolumes.add(MediaStore.VOLUME_EXTERNAL);
+
+ // Obtain the list of volume name candidates
+
+ final Set externalVolumeNames = MediaStore.getExternalVolumeNames(context);
+
+ for (final String entry : externalVolumeNames) {
+ // If the volume is "not" already cached in the list,
+ // then is an SD-card, so prioritize it by adding it
+ // at the top of the list
+ if (!allVolumes.contains(entry))
+ allVolumes.add(0, entry);
+ }
+
+ // Finally resolve the target Image content Uris
+
+ final List output = new ArrayList<>();
+
+ for (final String entry : allVolumes) {
+ output.add(MediaStore.Audio.Media.getContentUri(entry));
+ }
+
+ return output;
+ }
+
+
/**
* Create a file inside the Download folder using MediaStore API
*/
- @RequiresApi(Build.VERSION_CODES.Q)
- private Uri createFileInPublicDownloadsDir(String filename, String mimeType) {
- Uri collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
+ @RequiresApi(Build.VERSION_CODES.R)
+ private Uri createFileInPublicMusicDir(String filename, String savedDir) {
+ Set volumeNames = MediaStore.getExternalVolumeNames(getApplicationContext());
+ List contentUris = getContentUris(getApplicationContext());
+ boolean isInternal = volumeNames.size() == 1 || savedDir.toLowerCase().startsWith("/storage/emulated");
+ int contentIndex = isInternal? contentUris.size()-1 : 0; //
+ Uri contentUriSelectToSaveOn =contentUris.get(contentIndex);
+ String path = savedDir.split("/Music")[1];
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, filename);
- values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
- values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
+ values.put(MediaStore.Downloads.MIME_TYPE, "audio/mpeg");
+ values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + path);
ContentResolver contentResolver = getApplicationContext().getContentResolver();
try {
- return contentResolver.insert(collection, values);
+ return contentResolver.insert(contentUriSelectToSaveOn, values);
} catch (Exception e) {
e.printStackTrace();
logError("Create a file using MediaStore API failed.");
@@ -535,7 +635,8 @@ private String getMediaStoreEntryPathApi29(Uri uri) {
return null;
if (!cursor.moveToFirst())
return null;
- return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
+
+ return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
} catch (IllegalArgumentException e) {
e.printStackTrace();
logError("Get a path for a MediaStore failed");
@@ -570,9 +671,12 @@ private void cleanUp() {
private int getNotificationIconRes() {
try {
- ApplicationInfo applicationInfo = getApplicationContext().getPackageManager().getApplicationInfo(getApplicationContext().getPackageName(), PackageManager.GET_META_DATA);
+ ApplicationInfo applicationInfo =
+ getApplicationContext().getPackageManager().getApplicationInfo(
+ getApplicationContext().getPackageName(), PackageManager.GET_META_DATA);
int appIconResId = applicationInfo.icon;
- return applicationInfo.metaData.getInt("vn.hunghd.flutterdownloader.NOTIFICATION_ICON", appIconResId);
+ return applicationInfo.metaData.getInt("vn.hunghd.flutterdownloader.NOTIFICATION_ICON",
+ appIconResId);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
@@ -580,7 +684,8 @@ private int getNotificationIconRes() {
}
private void setupNotification(Context context) {
- if (!showNotification) return;
+ if (!showNotification)
+ return;
// Make a channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create the NotificationChannel, but only on API 26+ because
@@ -588,10 +693,13 @@ private void setupNotification(Context context) {
Resources res = getApplicationContext().getResources();
- String channelName = res.getString(R.string.flutter_downloader_notification_channel_name);
- String channelDescription = res.getString(R.string.flutter_downloader_notification_channel_description);
+ String channelName =
+ res.getString(R.string.flutter_downloader_notification_channel_name);
+ String channelDescription =
+ res.getString(R.string.flutter_downloader_notification_channel_description);
int importance = NotificationManager.IMPORTANCE_LOW;
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, channelName, importance);
+ NotificationChannel channel =
+ new NotificationChannel(CHANNEL_ID, channelName, importance);
channel.setDescription(channelDescription);
channel.setSound(null, null);
@@ -599,53 +707,50 @@ private void setupNotification(Context context) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.createNotificationChannel(channel);
}
- }
- private void updateNotification(Context context, String title, int status, int progress, PendingIntent intent, boolean finalize) {
- sendUpdateProcessEvent(status, progress);
+ }
+ private void updateNotification(Context context, String title, int status, int progress,
+ PendingIntent intent, boolean finalize, String errorType) {
+ sendUpdateProcessEvent(status, progress, errorType);
+ if (queueEnabled && status != 2) {
+ DownloadQueue.getInstance(context).doQueue();
+ }
// Show the notification
if (showNotification) {
+ Boolean cancelNotification = false;
// Create the notification
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID).
- setContentTitle(title)
- .setContentIntent(intent)
- .setOnlyAlertOnce(true)
- .setAutoCancel(true)
- .setPriority(NotificationCompat.PRIORITY_LOW);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setContentTitle(title).setContentIntent(intent).setOnlyAlertOnce(true)
+ .setAutoCancel(true).setPriority(NotificationCompat.PRIORITY_LOW);
if (status == DownloadStatus.RUNNING) {
if (progress <= 0) {
- builder.setContentText(msgStarted)
- .setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(getNotificationIconRes());
+ builder.setContentText(msgStarted).setProgress(0, 0, false);
+ builder.setOngoing(false).setSmallIcon(getNotificationIconRes());
} else if (progress < 100) {
- builder.setContentText(msgInProgress)
- .setProgress(100, progress, false);
- builder.setOngoing(true)
- .setSmallIcon(android.R.drawable.stat_sys_download);
+ builder.setContentText(msgInProgress).setProgress(100, progress, false);
+ builder.setOngoing(true).setSmallIcon(android.R.drawable.stat_sys_download);
} else {
- builder.setContentText(msgComplete).setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ // builder.setContentText(msgComplete).setProgress(0, 0, false);
+ // builder.setOngoing(false)
+ // .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ cancelNotification = true;
}
} else if (status == DownloadStatus.CANCELED) {
builder.setContentText(msgCanceled).setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ builder.setOngoing(false).setSmallIcon(android.R.drawable.stat_sys_download_done);
} else if (status == DownloadStatus.FAILED) {
builder.setContentText(msgFailed).setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ builder.setOngoing(false).setSmallIcon(android.R.drawable.stat_sys_download_done);
} else if (status == DownloadStatus.PAUSED) {
builder.setContentText(msgPaused).setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ builder.setOngoing(false).setSmallIcon(android.R.drawable.stat_sys_download_done);
} else if (status == DownloadStatus.COMPLETE) {
- builder.setContentText(msgComplete).setProgress(0, 0, false);
- builder.setOngoing(false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ // builder.setContentText(msgComplete).setProgress(0, 0, false);
+ // builder.setOngoing(false)
+ // .setSmallIcon(android.R.drawable.stat_sys_download_done);
+ cancelNotification = true;
} else {
builder.setProgress(0, 0, false);
builder.setOngoing(false).setSmallIcon(getNotificationIconRes());
@@ -653,10 +758,13 @@ private void updateNotification(Context context, String title, int status, int p
// Note: Android applies a rate limit when updating a notification.
// If you post updates to a notification too frequently (many in less than one second),
- // the system might drop some updates. (https://developer.android.com/training/notify-user/build-notification#Updating)
+ // the system might drop some updates.
+ // (https://developer.android.com/training/notify-user/build-notification#Updating)
//
- // If this is progress update, it's not much important if it is dropped because there're still incoming updates later
- // If this is the final update, it must be success otherwise the notification will be stuck at the processing state
+ // If this is progress update, it's not much important if it is dropped because there're
+ // still incoming updates later
+ // If this is the final update, it must be success otherwise the notification will be
+ // stuck at the processing state
// In order to ensure the final one is success, we check and sleep a second if need.
if (System.currentTimeMillis() - lastCallUpdateNotification < 1000) {
if (finalize) {
@@ -671,19 +779,26 @@ private void updateNotification(Context context, String title, int status, int p
return;
}
}
- log("Update notification: {notificationId: " + primaryId + ", title: " + title + ", status: " + status + ", progress: " + progress + "}");
- NotificationManagerCompat.from(context).notify(primaryId, builder.build());
+
+ if (cancelNotification) {
+ NotificationManagerCompat.from(context).cancel(primaryId);
+ } else {
+ log("Update notification: {notificationId: " + primaryId + ", title: " + title
+ + ", status: " + status + ", progress: " + progress + "}");
+ NotificationManagerCompat.from(context).notify(primaryId, builder.build());
+ }
lastCallUpdateNotification = System.currentTimeMillis();
}
}
- private void sendUpdateProcessEvent(int status, int progress) {
+ private void sendUpdateProcessEvent(int status, int progress, String errorType) {
final List