diff --git a/.gitignore b/.gitignore index 2362ec2e..b071bd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,48 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter repo-specific +/bin/cache/ +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/packages/flutter/coverage/ +version +.flutter-plugins-dependencies + +# packages file containing multi-root paths +.packages.generated +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins .packages +.pub-cache/ .pub/ -pubspec.lock - build/ .idea/ @@ -17,4 +55,56 @@ android/example/.project android/example/.settings android/example/.settings/ example/ios/Flutter/flutter_export_environment.sh -.vscode/ \ No newline at end of file +.vscode/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Coverage +coverage/ + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/CHANGELOG.md b/CHANGELOG.md index acc7c778..8413bc2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.5.2 - 25.10.2020 + +* Android: fix bug notification stuck in processing + +## 1.5.1 - 27.09.2020 + +* iOS: fix bug missing update download progress + +## 1.5.0 - 09.08.2020 + +* Update `pubspec` to new format +* Upgrade `AndroidWorkManager` to v2.4.0 + +## 1.4.4 - 18.04.2020 + +* add `debug` (optional) parameter in `initialize()` method that supports disable logging to console + +## 1.4.3 - 09.04.2020 + +* iOS: fix bug on `remove` method + +## 1.4.2 - 02.04.2020 + +* add `timeCreated` in `DownloadTask` model +* iOS: fix bug MissingPluginException + ## 1.4.1 - 30.01.2020 * Android: fix bug `ensureInitializationComplete must be called after startInitialization` diff --git a/README.md b/README.md index a8f04ce3..ed4ed365 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ import flutter_downloader private func registerPlugins(registry: FlutterPluginRegistry) { if (!registry.hasPlugin("FlutterDownloaderPlugin")) { - FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")) + FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!) } } @@ -183,8 +183,7 @@ private func registerPlugins(registry: FlutterPluginRegistry) { + tools:node="remove" /> + diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index 299e423c..d04819ce 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 index 2fcf3d6b..61eaaf7b 100644 --- a/android/.idea/modules.xml +++ b/android/.idea/modules.xml @@ -3,6 +3,7 @@ + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 4f2b1d85..b553a7c1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.6.3' } } @@ -34,8 +34,9 @@ android { } dependencies { - implementation 'androidx.work:work-runtime:2.2.0' + implementation 'androidx.work:work-runtime:2.4.0' implementation 'androidx.annotation:annotation:1.1.0' - implementation 'androidx.core:core:1.1.0' - implementation 'androidx.fragment:fragment:1.1.0' + implementation 'androidx.core:core:1.3.1' + implementation 'androidx.fragment:fragment:1.2.5' + implementation 'com.mpatric:mp3agic:0.9.0' } diff --git a/android/gradle.properties b/android/gradle.properties index 8bd86f68..94adc3a3 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,3 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..74dc4275 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jul 12 10:54:04 BRT 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java index 84b3ae3f..ff5998fe 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadTask.java @@ -14,9 +14,16 @@ public class DownloadTask { boolean showNotification; boolean openFileFromNotification; long timeCreated; + 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) { + String headers, String mimeType, boolean resumable, boolean showNotification, boolean openFileFromNotification, + long timeCreated, String albumName, String artistName, String artistId, String playlistId, String albumId, String musicId) { this.primaryId = primaryId; this.taskId = taskId; this.status = status; @@ -30,10 +37,22 @@ 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; } @Override public String toString() { - return "DownloadTask{taskId=" + taskId + ",status=" + status + ",progress=" + progress + ",url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",headers=" + headers + "}"; + return "DownloadTask{taskId=" + taskId + ",status=" + status + ",progress=" + progress + + ",url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",headers=" + + headers + ",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 f267f84d..7d4aec39 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/DownloadWorker.java @@ -8,10 +8,14 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.os.Build; +import android.net.Uri; + +import java.io.File; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; @@ -47,6 +51,13 @@ import androidx.work.Worker; import androidx.work.WorkerParameters; +import com.mpatric.mp3agic.ID3v1Tag; +import com.mpatric.mp3agic.ID3v24Tag; +import com.mpatric.mp3agic.InvalidDataException; +import com.mpatric.mp3agic.Mp3File; +import com.mpatric.mp3agic.NotSupportedException; +import com.mpatric.mp3agic.UnsupportedTagException; + import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; @@ -65,6 +76,18 @@ public class DownloadWorker extends Worker implements MethodChannel.MethodCallHa public static final String ARG_SHOW_NOTIFICATION = "show_notification"; 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 IS_PENDING = "is_pending"; + public static final String USER_AGENT = "SuaMusica/downloader (Linux; Android " + + Build.VERSION.SDK_INT + "; " + Build.BRAND + "/" + Build.MODEL + ")"; private static final String TAG = DownloadWorker.class.getSimpleName(); private static final int BUFFER_SIZE = 4096; @@ -80,15 +103,27 @@ public class DownloadWorker extends Worker implements MethodChannel.MethodCallHa private MethodChannel backgroundChannel; private TaskDbHelper dbHelper; private TaskDao taskDao; - private NotificationCompat.Builder builder; private boolean showNotification; private boolean clickToOpenDownloadedFile; + private boolean debug; private int lastProgress = 0; private int primaryId; - private String msgStarted, msgInProgress, msgCanceled, msgFailed, msgPaused, msgComplete; - - public DownloadWorker(@NonNull final Context context, - @NonNull WorkerParameters params) { + 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; + + public DownloadWorker(@NonNull final Context context, @NonNull WorkerParameters params) { super(context, params); new Handler(context.getMainLooper()).post(new Runnable() { @@ -102,13 +137,18 @@ public void run() { private void startBackgroundIsolate(Context context) { synchronized (isolateStarted) { if (backgroundFlutterView == null) { - SharedPreferences pref = context.getSharedPreferences(FlutterDownloaderPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); - long callbackHandle = pref.getLong(FlutterDownloaderPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, 0); - - FlutterMain.startInitialization(context); // Starts initialization of the native system, if already initialized this does nothing + SharedPreferences pref = context.getSharedPreferences( + FlutterDownloaderPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); + long callbackHandle = + pref.getLong(FlutterDownloaderPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, 0); + + FlutterMain.startInitialization(context); // Starts initialization of the native + // system, if already initialized this + // does nothing FlutterMain.ensureInitializationComplete(context, null); - FlutterCallbackInformation callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); + FlutterCallbackInformation callbackInfo = + FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); if (callbackInfo == null) { Log.e(TAG, "Fatal: failed to find callback"); return; @@ -118,13 +158,14 @@ private void startBackgroundIsolate(Context context) { /// backward compatibility with V1 embedding if (getApplicationContext() instanceof PluginRegistrantCallback) { - PluginRegistrantCallback pluginRegistrantCallback = (PluginRegistrantCallback) getApplicationContext(); + PluginRegistrantCallback pluginRegistrantCallback = + (PluginRegistrantCallback) getApplicationContext(); PluginRegistry registry = backgroundFlutterView.getPluginRegistry(); pluginRegistrantCallback.registerWith(registry); } FlutterRunArguments args = new FlutterRunArguments(); - args.bundlePath = FlutterMain.findAppBundlePath(context); + args.bundlePath = FlutterMain.findAppBundlePath(); args.entrypoint = callbackInfo.callbackName; args.libraryPath = callbackInfo.callbackLibraryPath; @@ -132,7 +173,8 @@ private void startBackgroundIsolate(Context context) { } } - backgroundChannel = new MethodChannel(backgroundFlutterView, "vn.hunghd/downloader_background"); + backgroundChannel = + new MethodChannel(backgroundFlutterView, "vn.hunghd/downloader_background"); backgroundChannel.setMethodCallHandler(this); } @@ -163,6 +205,13 @@ public Result doWork() { String savedDir = getInputData().getString(ARG_SAVED_DIR); 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); Resources res = getApplicationContext().getResources(); msgStarted = res.getString(R.string.flutter_downloader_notification_started); @@ -172,18 +221,25 @@ public Result doWork() { msgPaused = res.getString(R.string.flutter_downloader_notification_paused); msgComplete = res.getString(R.string.flutter_downloader_notification_complete); - Log.d(TAG, "DownloadWorker{url=" + url + ",filename=" + filename + ",savedDir=" + savedDir + ",header=" + headers + ",isResume=" + isResume); + 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); showNotification = getInputData().getBoolean(ARG_SHOW_NOTIFICATION, false); - clickToOpenDownloadedFile = getInputData().getBoolean(ARG_OPEN_FILE_FROM_NOTIFICATION, false); + clickToOpenDownloadedFile = + getInputData().getBoolean(ARG_OPEN_FILE_FROM_NOTIFICATION, false); DownloadTask task = taskDao.loadTask(getId().toString()); primaryId = task.primaryId; - buildNotification(context); + setupNotification(context); - updateNotification(context, filename == null ? url : filename, DownloadStatus.RUNNING, task.progress, null); - taskDao.updateTask(getId().toString(), DownloadStatus.RUNNING, 0); + updateNotification(context, filename == null ? url : filename, DownloadStatus.RUNNING, + task.progress, null, false, ""); + taskDao.updateTask(getId().toString(), DownloadStatus.RUNNING, task.progress); try { downloadFile(context, url, savedDir, filename, headers, isResume); @@ -192,7 +248,9 @@ public Result doWork() { taskDao = null; return Result.success(); } catch (Exception e) { - updateNotification(context, filename == null ? url : filename, DownloadStatus.FAILED, -1, null); + 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; @@ -203,10 +261,10 @@ public Result doWork() { private void setupHeaders(HttpURLConnection conn, String headers) { if (!TextUtils.isEmpty(headers)) { - Log.d(TAG, "Headers = " + headers); + log("Headers = " + headers); try { JSONObject json = new JSONObject(headers); - for (Iterator it = json.keys(); it.hasNext(); ) { + for (Iterator it = json.keys(); it.hasNext();) { String key = it.next(); conn.setRequestProperty(key, json.getString(key)); } @@ -217,18 +275,20 @@ 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(); - Log.d(TAG, "Resume download: Range: bytes=" + downloadedBytes + "-"); + log("Resume download: Range: bytes=" + downloadedBytes + "-"); conn.setRequestProperty("Accept-Encoding", "identity"); conn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-"); conn.setDoInput(true); 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; @@ -257,33 +317,36 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri throw new IOException("Stuck in redirect loop"); resourceUrl = new URL(url); - Log.d(TAG, "Open connection to " + url); + log("Open connection to " + url); httpConn = (HttpURLConnection) resourceUrl.openConnection(); 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(); switch (responseCode) { case HttpURLConnection.HTTP_MOVED_PERM: - case HttpURLConnection.HTTP_SEE_OTHER: + case HttpURLConnection.HTTP_SEE_OTHER: case HttpURLConnection.HTTP_MOVED_TEMP: - Log.d(TAG, "Response with redirection code"); + log("Response with redirection code"); location = httpConn.getHeaderField("Location"); - Log.d(TAG, "Location = " + location); + log("Location = " + location); base = new URL(fileURL); - next = new URL(base, location); // Deal with relative URLs + next = new URL(base, location); // Deal with relative URLs url = next.toExternalForm(); - Log.d(TAG, "New url: " + url); + log("New url: " + url); continue; } @@ -292,22 +355,29 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri httpConn.connect(); - if ((responseCode == HttpURLConnection.HTTP_OK || (isResume && responseCode == HttpURLConnection.HTTP_PARTIAL)) && !isStopped()) { + if ((responseCode == HttpURLConnection.HTTP_OK + || (isResume && responseCode == HttpURLConnection.HTTP_PARTIAL)) + && !isStopped()) { String contentType = httpConn.getContentType(); + if(contentType.contains("multipart/")){ + contentType = "application/octet-stream"; + } int contentLength = httpConn.getContentLength(); - Log.d(TAG, "Content-Type = " + contentType); - Log.d(TAG, "Content-Length = " + contentLength); + log("Content-Type = " + contentType); + log("Content-Length = " + contentLength); String charset = getCharsetFromContentType(contentType); - Log.d(TAG, "Charset = " + charset); + log("Charset = " + charset); if (!isResume) { // try to extract filename from HTTP headers if it is not given by user if (filename == null) { String disposition = httpConn.getHeaderField("Content-Disposition"); - Log.d(TAG, "Content-Disposition = " + disposition); + log("Content-Disposition = " + disposition); if (disposition != null && !disposition.isEmpty()) { - String name = disposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1"); - filename = URLDecoder.decode(name, charset != null ? charset : "ISO-8859-1"); + String name = disposition + .replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1"); + filename = URLDecoder.decode(name, + charset != null ? charset : "ISO-8859-1"); } if (filename == null || filename.isEmpty()) { filename = url.substring(url.lastIndexOf("/") + 1); @@ -316,7 +386,7 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri } saveFilePath = savedDir + File.separator + filename; - Log.d(TAG, "fileName = " + filename); + log("fileName = " + filename); taskDao.updateTask(getId().toString(), filename, contentType); @@ -334,14 +404,18 @@ 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); - - // 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 + 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); } @@ -349,38 +423,53 @@ private void downloadFile(Context context, String fileURL, String savedDir, Stri 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 (isImageOrVideoFile(contentType) && isExternalStoragePath(saveFilePath)) { - addImageOrVideoToGallery(filename, saveFilePath, getContentTypeWithoutCharset(contentType)); + if (isMediaFile(contentType) && isExternalStoragePath(saveFilePath)) { + addMediaToGallery(filename, saveFilePath, + getContentTypeWithoutCharset(contentType)); } - if (clickToOpenDownloadedFile && storage == PackageManager.PERMISSION_GRANTED) { - Intent intent = IntentUtils.validatedFileIntent(getApplicationContext(), saveFilePath, contentType); + Intent intent = IntentUtils.validatedFileIntent(getApplicationContext(), + saveFilePath, contentType); if (intent != null) { - Log.d(TAG, "Setting an intent to open the file " + saveFilePath); - pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + log("Setting an intent to open the file " + saveFilePath); + pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, + intent, PendingIntent.FLAG_CANCEL_CURRENT); } else { - Log.d(TAG, "There's no application that can open the file " + saveFilePath); + log("There's no application that can open the file " + saveFilePath); } } } - Log.d(TAG, "===> updateNotification: (filename: " + filename + ", status: " + status + ")"); - updateNotification(context, filename, status, progress, pendingIntent); + Log.d(TAG, "===> updateNotification: (filename: " + filename + ", status: " + status + + ")"); + updateNotification(context, filename, status, progress, pendingIntent, true, + isStopped() ? "Download canceled" : ""); taskDao.updateTask(getId().toString(), status, progress); - Log.d(TAG, isStopped() ? "Download canceled" : "File downloaded"); + log(isStopped() ? "Download canceled" : "File downloaded"); } else { DownloadTask task = taskDao.loadTask(getId().toString()); - int status = isStopped() ? (task.resumable ? DownloadStatus.PAUSED : DownloadStatus.CANCELED) : DownloadStatus.FAILED; - updateNotification(context, filename, status, -1, null); + 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); - Log.d(TAG, isStopped() ? "Download canceled" : "Server replied HTTP code: " + responseCode); + log(errorMessage); } } catch (IOException e) { - updateNotification(context, filename == null ? fileURL : filename, DownloadStatus.FAILED, -1, null); + 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); e.printStackTrace(); } finally { @@ -421,92 +510,134 @@ private void cleanUp() { } } - private void buildNotification(Context context) { + private int getNotificationIconRes() { + try { + 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); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return 0; + } + + private void setupNotification(Context context) { + 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 // the NotificationChannel class is new and not in the support library - CharSequence name = context.getApplicationInfo().loadLabel(context.getPackageManager()); - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + + 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); + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel channel = + new NotificationChannel(CHANNEL_ID, channelName, importance); + channel.setDescription(channelDescription); channel.setSound(null, null); // Add the channel - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - - if (notificationManager != null) { - notificationManager.createNotificationChannel(channel); - } + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.createNotificationChannel(channel); } - // Create the notification - builder = new NotificationCompat.Builder(context, CHANNEL_ID) -// .setSmallIcon(R.drawable.ic_download) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT); - } - private void updateNotification(Context context, String title, int status, int progress, PendingIntent intent) { - builder.setContentTitle(title); - builder.setContentIntent(intent); - boolean shouldUpdate = false; - - if (status == DownloadStatus.RUNNING) { - shouldUpdate = true; - builder.setContentText(progress == 0 ? msgStarted : msgInProgress) - .setProgress(100, progress, progress == 0); - builder.setOngoing(true) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), - android.R.drawable.stat_sys_download)); - } else if (status == DownloadStatus.CANCELED) { - shouldUpdate = true; - builder.setContentText(msgCanceled).setProgress(0, 0, false); - builder.setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), - android.R.drawable.stat_sys_download_done)); - } else if (status == DownloadStatus.FAILED) { - shouldUpdate = true; - builder.setContentText(msgFailed).setProgress(0, 0, false); - builder.setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), - android.R.drawable.stat_sys_download_done)); - } else if (status == DownloadStatus.PAUSED) { - shouldUpdate = true; - builder.setContentText(msgPaused).setProgress(0, 0, false); - builder.setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), - android.R.drawable.stat_sys_download_done)); - } else if (status == DownloadStatus.COMPLETE) { - shouldUpdate = true; - builder.setContentText(msgComplete).setProgress(0, 0, false); - builder.setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), - android.R.drawable.stat_sys_download_done)); - } + private void updateNotification(Context context, String title, int status, int progress, + PendingIntent intent, boolean finalize, String errorType) { + sendUpdateProcessEvent(status, progress, errorType); // Show the notification - if (showNotification && shouldUpdate) { - NotificationManagerCompat.from(context).notify(primaryId, builder.build()); - } + 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); + + if (status == DownloadStatus.RUNNING) { + if (progress <= 0) { + 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); + } else { + // 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); + } else if (status == DownloadStatus.FAILED) { + builder.setContentText(msgFailed).setProgress(0, 0, false); + 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); + } else if (status == DownloadStatus.COMPLETE) { + // 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()); + } - sendUpdateProcessEvent(status, progress); + // 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) + // + // 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) { + log("Update too frequently!!!!, but it is the final update, we should sleep a second to ensure the update call can be processed"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + log("Update too frequently!!!!, this should be dropped"); + return; + } + } + + 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 args = new ArrayList<>(); long callbackHandle = getInputData().getLong(ARG_CALLBACK_HANDLE, 0); args.add(callbackHandle); args.add(getId().toString()); args.add(status); args.add(progress); + args.add(errorType); synchronized (isolateStarted) { if (!isolateStarted.get()) { @@ -539,17 +670,20 @@ private String getContentTypeWithoutCharset(String contentType) { return contentType.split(";")[0].trim(); } - private boolean isImageOrVideoFile(String contentType) { + private boolean isMediaFile(String contentType) { contentType = getContentTypeWithoutCharset(contentType); - return (contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video"))); + return (contentType != null && (contentType.startsWith("image/") + || contentType.startsWith("video") || contentType.startsWith("audio") + || contentType.contains("octet-stream"))); } private boolean isExternalStoragePath(String filePath) { File externalStorageDir = Environment.getExternalStorageDirectory(); - return filePath != null && externalStorageDir != null && filePath.startsWith(externalStorageDir.getPath()); + return filePath != null && externalStorageDir != null + && filePath.startsWith(externalStorageDir.getPath()); } - private void addImageOrVideoToGallery(String fileName, String filePath, String contentType) { + private void addMediaToGallery(String fileName, String filePath, String contentType) { if (contentType != null && filePath != null && fileName != null) { if (contentType.startsWith("image/")) { ContentValues values = new ContentValues(); @@ -562,7 +696,7 @@ private void addImageOrVideoToGallery(String fileName, String filePath, String c values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(MediaStore.Images.Media.DATA, filePath); - Log.d(TAG, "insert " + values + " to MediaStore"); + log("insert " + values + " to MediaStore"); ContentResolver contentResolver = getApplicationContext().getContentResolver(); contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); @@ -577,11 +711,84 @@ private void addImageOrVideoToGallery(String fileName, String filePath, String c values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(MediaStore.Video.Media.DATA, filePath); - Log.d(TAG, "insert " + values + " to MediaStore"); + log("insert " + values + " to MediaStore"); ContentResolver contentResolver = getApplicationContext().getContentResolver(); contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); + } else if (contentType.startsWith("audio") || contentType.contains("octet-stream")) { + File file = new File(filePath); + if (file.exists()) { + if (android.os.Build.VERSION.SDK_INT >= 23) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.Media.TITLE, fileName); + values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName); + values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg"); + values.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis()); + values.put(MediaStore.Audio.Media.DATA, filePath); + values.put(MediaStore.Audio.Media.SIZE, file.getTotalSpace()); + + values.put(MediaStore.Audio.Media.ARTIST, argMusicArtist); + values.put(MediaStore.Audio.Media.ALBUM, argMusicAlbum); + + try { + Mp3File mp3File = new Mp3File(filePath); + ID3v1Tag id3v1Tag = new ID3v1Tag(); + id3v1Tag.setComment("Sua Música"); + mp3File.setId3v1Tag(id3v1Tag); + + ID3v24Tag id3v2Tag = new ID3v24Tag(); + id3v2Tag.setAlbum(argMusicAlbum); + id3v2Tag.setAlbumArtist(argMusicArtist); + id3v2Tag.setUrl(String.format("https://www.suamusica.com.br/perfil/%s?playlistId=%s&albumId=%s&musicId=%s", argArtistId, argPlaylistId, argAlbumId, argMusicId)); + mp3File.setId3v2Tag(id3v2Tag); + + String newFilename = filePath + ".tmp"; + mp3File.save(newFilename); + + File from = new File(newFilename); + from.renameTo(file); + + Log.i(TAG, "Successfully set ID3v1 tags"); + } catch (Exception e) { + Log.e(TAG, "Failed to set ID3v1 tags", e); + } + // For reasons I could not understand, Android SDK is failing to find the + // constant MediaStore.Audio.Media.ALBUM_ARTIST in pre-compilation time and + // obligated me to reference the column string value. + // However it's working just fine. + values.put("album_artist", argMusicArtist); + + values.put(IS_PENDING, 1); + log("insert " + values + " to MediaStore"); + ContentResolver contentResolver = + getApplicationContext().getContentResolver(); + Uri uriSavedMusic = contentResolver + .insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values); + if (uriSavedMusic != null) { + values.clear(); + values.put(IS_PENDING, 0); + contentResolver.update(uriSavedMusic, values, null, null); + } + + if (android.os.Build.VERSION.SDK_INT < 29) + callScanFileIntent(file); + } else { + callScanFileIntent(file); + } + } } } } + + private void callScanFileIntent(File file) { + Intent scanFileIntent = + new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)); + getApplicationContext().sendBroadcast(scanFileIntent); + } + + private void log(String message) { + if (debug) { + Log.d(TAG, message); + } + } } diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/FlutterDownloaderPlugin.java b/android/src/main/java/vn/hunghd/flutterdownloader/FlutterDownloaderPlugin.java index b5cecfb7..83f62ed3 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/FlutterDownloaderPlugin.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/FlutterDownloaderPlugin.java @@ -1,9 +1,15 @@ package vn.hunghd.flutterdownloader; import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.database.Cursor; +import android.net.Uri; import androidx.core.app.NotificationManagerCompat; @@ -11,10 +17,12 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Arrays; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +import android.net.Uri; import androidx.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.Data; @@ -43,6 +51,7 @@ public class FlutterDownloaderPlugin implements MethodCallHandler, FlutterPlugin private TaskDao taskDao; private Context context; private long callbackHandle; + private int debugMode; private final Object initializationLock = new Object(); @SuppressLint("NewApi") @@ -101,17 +110,22 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getFlutterEngine().getDartExecutor()); + onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); } @Override public void onDetachedFromEngine(FlutterPluginBinding binding) { context = null; - flutterChannel.setMethodCallHandler(null); - flutterChannel = null; + if (flutterChannel != null) { + flutterChannel.setMethodCallHandler(null); + flutterChannel = null; + } } - private WorkRequest buildRequest(String url, String savedDir, String filename, String headers, boolean showNotification, boolean openFileFromNotification, boolean isResume, boolean requiresStorageNotLow) { + private WorkRequest buildRequest(String url, String savedDir, String filename, String headers, + boolean showNotification, boolean openFileFromNotification, + boolean isResume, boolean requiresStorageNotLow, String albumName, + String artistName, String artistId, String playlistId, String albumId, String musicId) { WorkRequest request = new OneTimeWorkRequest.Builder(DownloadWorker.class) .setConstraints(new Constraints.Builder().setRequiresStorageNotLow(requiresStorageNotLow) .setRequiredNetworkType(NetworkType.CONNECTED).build()) @@ -123,22 +137,33 @@ private WorkRequest buildRequest(String url, String savedDir, String filename, S .putBoolean(DownloadWorker.ARG_SHOW_NOTIFICATION, showNotification) .putBoolean(DownloadWorker.ARG_OPEN_FILE_FROM_NOTIFICATION, openFileFromNotification) .putBoolean(DownloadWorker.ARG_IS_RESUME, isResume) - .putLong(DownloadWorker.ARG_CALLBACK_HANDLE, callbackHandle).build()) + .putLong(DownloadWorker.ARG_CALLBACK_HANDLE, callbackHandle) + .putBoolean(DownloadWorker.ARG_DEBUG, debugMode == 1) + .putString(DownloadWorker.ARG_MUSIC_ALBUM, albumName) + .putString(DownloadWorker.ARG_MUSIC_ARTIST, artistName) + .putString(DownloadWorker.ARG_ARTIST_ID, artistId) + .putString(DownloadWorker.ARG_PLAYLIST_ID, playlistId) + .putString(DownloadWorker.ARG_ALBUM_ID, albumId) + .putString(DownloadWorker.ARG_MUSIC_ID, musicId) + .build() + ) .build(); return request; } - private void sendUpdateProgress(String id, int status, int progress) { + private void sendUpdateProgress(String id, int status, int progress,String obs) { Map args = new HashMap<>(); args.put("task_id", id); args.put("status", status); args.put("progress", progress); + args.put("obs", obs); flutterChannel.invokeMethod("updateProgress", args); } private void initialize(MethodCall call, MethodChannel.Result result) { List args = (List) call.arguments; long callbackHandle = Long.parseLong(args.get(0).toString()); + debugMode = Integer.parseInt(args.get(1).toString()); SharedPreferences pref = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); pref.edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle).apply(); @@ -157,17 +182,23 @@ private void enqueue(MethodCall call, MethodChannel.Result result) { String savedDir = call.argument("saved_dir"); String filename = call.argument("file_name"); String headers = call.argument("headers"); + String albumName = call.argument("music_album"); + String artistName = call.argument("music_artist"); + String artistId = call.argument("artist_id"); + String playlistId = call.argument("playlist_id"); + String albumId = call.argument("album_id"); + String musicId = call.argument("music_id"); boolean showNotification = call.argument("show_notification"); boolean openFileFromNotification = call.argument("open_file_from_notification"); boolean requiresStorageNotLow = call.argument("requires_storage_not_low"); WorkRequest request = buildRequest(url, savedDir, filename, headers, showNotification, openFileFromNotification, - false, requiresStorageNotLow); + false, requiresStorageNotLow, albumName, artistName, artistId, playlistId, albumId, musicId); WorkManager.getInstance(context).enqueue(request); String taskId = request.getId().toString(); result.success(taskId); - sendUpdateProgress(taskId, DownloadStatus.ENQUEUED, 0); + sendUpdateProgress(taskId, DownloadStatus.ENQUEUED, 0,""); taskDao.insertOrUpdateNewTask(taskId, url, DownloadStatus.ENQUEUED, 0, filename, savedDir, headers, - showNotification, openFileFromNotification); + showNotification, openFileFromNotification, albumName, artistName, artistId, playlistId, albumId, musicId); } private void enqueueItems(MethodCall call, MethodChannel.Result result) { @@ -184,15 +215,23 @@ private void enqueueItems(MethodCall call, MethodChannel.Result result) { String url = downloads.get(i).get("url"); String savedDir = downloads.get(i).get("saved_dir"); String filename = downloads.get(i).get("file_name"); + String albumName = downloads.get(i).get("music_album"); + String artistName = downloads.get(i).get("music_artist"); + String artistId = downloads.get(i).get("artist_id"); + String playlistId = downloads.get(i).get("playlist_id"); + String albumId = downloads.get(i).get("album_id"); + String musicId = downloads.get(i).get("music_id"); WorkRequest request = buildRequest(url, savedDir, filename, headers, showNotification, - openFileFromNotification, false, requiresStorageNotLow); + openFileFromNotification, false, requiresStorageNotLow, albumName, artistName, + artistId, playlistId, albumId, musicId); WorkManager.getInstance(context).enqueue(request); String taskId = request.getId().toString(); taskIds.add(taskId); - sendUpdateProgress(taskId, DownloadStatus.ENQUEUED, 0); + sendUpdateProgress(taskId, DownloadStatus.ENQUEUED, 0,""); taskDao.insertOrUpdateNewTask(taskId, url, DownloadStatus.ENQUEUED, 0, filename, savedDir, headers, - showNotification, openFileFromNotification); + showNotification, openFileFromNotification, albumName, artistName, + artistId, playlistId, albumId, musicId); } result.success(taskIds); @@ -210,6 +249,12 @@ private void loadTasks(MethodCall call, MethodChannel.Result result) { item.put("file_name", task.filename); item.put("saved_dir", task.savedDir); item.put("time_created", task.timeCreated); + item.put("music_album", task.albumName); + item.put("music_artist", task.artistName); + item.put("artist_id", task.artistId); + item.put("playlist_id", task.playlistId); + item.put("album_id", task.albumId); + item.put("music_id", task.musicId); array.add(item); } result.success(array); @@ -228,6 +273,12 @@ private void loadTasksWithRawQuery(MethodCall call, MethodChannel.Result result) item.put("file_name", task.filename); item.put("saved_dir", task.savedDir); item.put("time_created", task.timeCreated); + item.put("music_album", task.albumName); + item.put("music_artist", task.artistName); + item.put("artist_id", task.artistId); + item.put("playlist_id", task.playlistId); + item.put("album_id", task.albumId); + item.put("music_id", task.musicId); array.add(item); } result.success(array); @@ -255,7 +306,9 @@ private void resume(MethodCall call, MethodChannel.Result result) { String taskId = call.argument("task_id"); DownloadTask task = taskDao.loadTask(taskId); boolean requiresStorageNotLow = call.argument("requires_storage_not_low"); + String headers = call.argument("headers"); if (task != null) { + final String finalHeaders = TextUtils.isEmpty(headers) ? task.headers : headers; if (task.status == DownloadStatus.PAUSED) { String filename = task.filename; if (filename == null) { @@ -264,11 +317,12 @@ private void resume(MethodCall call, MethodChannel.Result result) { String partialFilePath = task.savedDir + File.separator + filename; File partialFile = new File(partialFilePath); if (partialFile.exists()) { - WorkRequest request = buildRequest(task.url, task.savedDir, task.filename, task.headers, - task.showNotification, task.openFileFromNotification, true, requiresStorageNotLow); + WorkRequest request = buildRequest(task.url, task.savedDir, task.filename, finalHeaders, + task.showNotification, task.openFileFromNotification, true, requiresStorageNotLow, + task.albumName, task.artistName, task.artistId, task.playlistId, task.albumId, task.musicId); String newTaskId = request.getId().toString(); result.success(newTaskId); - sendUpdateProgress(newTaskId, DownloadStatus.RUNNING, task.progress); + sendUpdateProgress(newTaskId, DownloadStatus.RUNNING, task.progress,""); taskDao.updateTask(taskId, newTaskId, DownloadStatus.RUNNING, task.progress, false); WorkManager.getInstance(context).enqueue(request); } else { @@ -287,13 +341,16 @@ private void retry(MethodCall call, MethodChannel.Result result) { String taskId = call.argument("task_id"); DownloadTask task = taskDao.loadTask(taskId); boolean requiresStorageNotLow = call.argument("requires_storage_not_low"); + String headers = call.argument("headers"); if (task != null) { if (task.status == DownloadStatus.FAILED || task.status == DownloadStatus.CANCELED) { - WorkRequest request = buildRequest(task.url, task.savedDir, task.filename, task.headers, - task.showNotification, task.openFileFromNotification, false, requiresStorageNotLow); + final String finalHeaders = TextUtils.isEmpty(headers) ? task.headers : headers; + WorkRequest request = buildRequest(task.url, task.savedDir, task.filename, finalHeaders, + task.showNotification, task.openFileFromNotification, false, requiresStorageNotLow, + task.albumName, task.artistName, task.artistId, task.playlistId, task.albumId, task.musicId); String newTaskId = request.getId().toString(); result.success(newTaskId); - sendUpdateProgress(newTaskId, DownloadStatus.ENQUEUED, task.progress); + sendUpdateProgress(newTaskId, DownloadStatus.ENQUEUED, task.progress,""); taskDao.updateTask(taskId, newTaskId, DownloadStatus.ENQUEUED, task.progress, false); WorkManager.getInstance(context).enqueue(request); } else { @@ -341,15 +398,32 @@ private void remove(MethodCall call, MethodChannel.Result result) { } if (shouldDeleteContent) { String filename = task.filename; + String extension = ""; + List audioExtension = Arrays.asList("mp3", "m4a", "ogg"); + if (filename == null) { filename = task.url.substring(task.url.lastIndexOf("/") + 1, task.url.length()); } + int i = filename.lastIndexOf('.'); + if (i > 0) { + extension = filename.toLowerCase().substring(i + 1); + } String saveFilePath = task.savedDir + File.separator + filename; + if (audioExtension.contains(extension)) { + Uri rootUri = MediaStore.Audio.Media.getContentUriForPath(saveFilePath); + context.getContentResolver().delete(rootUri, + MediaStore.MediaColumns.DATA + "=?", new String[] {saveFilePath}); + } File tempFile = new File(saveFilePath); if (tempFile.exists()) { + deleteFileInMediaStore(tempFile); tempFile.delete(); } + File directory = new File(task.savedDir); + if (directory.exists() && directory.isDirectory() && directory.list().length == 0) { + directory.delete(); + } } taskDao.deleteTask(taskId); @@ -360,4 +434,42 @@ private void remove(MethodCall call, MethodChannel.Result result) { result.error("invalid_task_id", "not found task corresponding to given task id", null); } } + + private void deleteFileInMediaStore(File file) { + // Set up the projection (we only need the ID) + String[] projection = {MediaStore.Images.Media._ID}; + + // Match on the file path + String imageSelection = MediaStore.Images.Media.DATA + " = ?"; + String videoSelection = MediaStore.Video.Media.DATA + " = ?"; + String[] selectionArgs = new String[]{file.getAbsolutePath()}; + + // Query for the ID of the media matching the file path + Uri imageQueryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + Uri videoQueryUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + + ContentResolver contentResolver = context.getContentResolver(); + + // search the file in image store first + Cursor imageCursor = contentResolver.query(imageQueryUri, projection, imageSelection, selectionArgs, null); + if (imageCursor != null && imageCursor.moveToFirst()) { + // We found the ID. Deleting the item via the content provider will also remove the file + long id = imageCursor.getLong(imageCursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + Uri deleteUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); + contentResolver.delete(deleteUri, null, null); + } else { + // File not found in image store DB, try to search in video store + Cursor videoCursor = contentResolver.query(imageQueryUri, projection, imageSelection, selectionArgs, null); + if (videoCursor != null && videoCursor.moveToFirst()) { + // We found the ID. Deleting the item via the content provider will also remove the file + long id = videoCursor.getLong(videoCursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + Uri deleteUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); + contentResolver.delete(deleteUri, null, null); + } else { + // can not find the file in media store DB at all + } + if (videoCursor != null) videoCursor.close(); + } + if (imageCursor != null) imageCursor.close(); + } } diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/TaskContract.java b/android/src/main/java/vn/hunghd/flutterdownloader/TaskContract.java index 425d1277..b6fc4e94 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/TaskContract.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/TaskContract.java @@ -20,6 +20,12 @@ public static class TaskEntry implements BaseColumns { public static final String COLUMN_NAME_SHOW_NOTIFICATION = "show_notification"; public static final String COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION = "open_file_from_notification"; public static final String COLUMN_NAME_TIME_CREATED = "time_created"; + public static final String COLUMN_NAME_ALBUM_NAME = "music_album"; + public static final String COLUMN_NAME_ARTIST_NAME = "music_artist"; + public static final String COLUMN_NAME_ARTIST_ID = "artist_id"; + public static final String COLUMN_NAME_PLAYLIST_ID = "playlist_id"; + public static final String COLUMN_NAME_ALBUM_ID = "album_id"; + public static final String COLUMN_NAME_MUSIC_ID = "music_id"; } } diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/TaskDao.java b/android/src/main/java/vn/hunghd/flutterdownloader/TaskDao.java index 2c074dbb..8707c3de 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/TaskDao.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/TaskDao.java @@ -24,7 +24,13 @@ public class TaskDao { TaskContract.TaskEntry.COLUMN_NAME_RESUMABLE, TaskContract.TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION, TaskContract.TaskEntry.COLUMN_NAME_SHOW_NOTIFICATION, - TaskContract.TaskEntry.COLUMN_NAME_TIME_CREATED + TaskContract.TaskEntry.COLUMN_NAME_TIME_CREATED, + TaskContract.TaskEntry.COLUMN_NAME_ALBUM_NAME, + TaskContract.TaskEntry.COLUMN_NAME_ARTIST_NAME, + TaskContract.TaskEntry.COLUMN_NAME_ARTIST_ID, + TaskContract.TaskEntry.COLUMN_NAME_PLAYLIST_ID, + TaskContract.TaskEntry.COLUMN_NAME_ALBUM_ID, + TaskContract.TaskEntry.COLUMN_NAME_MUSIC_ID, }; public TaskDao(TaskDbHelper helper) { @@ -32,7 +38,8 @@ public TaskDao(TaskDbHelper helper) { } public void insertOrUpdateNewTask(String taskId, String url, int status, int progress, String fileName, - String savedDir, String headers, boolean showNotification, boolean openFileFromNotification) { + String savedDir, String headers, boolean showNotification, boolean openFileFromNotification, + String albumName, String artistName, String artistId, String playlistId, String albumId, String musicId) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); @@ -48,6 +55,12 @@ public void insertOrUpdateNewTask(String taskId, String url, int status, int pro values.put(TaskContract.TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION, openFileFromNotification ? 1 : 0); values.put(TaskContract.TaskEntry.COLUMN_NAME_RESUMABLE, 0); values.put(TaskContract.TaskEntry.COLUMN_NAME_TIME_CREATED, System.currentTimeMillis()); + values.put(TaskContract.TaskEntry.COLUMN_NAME_ALBUM_NAME, albumName); + values.put(TaskContract.TaskEntry.COLUMN_NAME_ARTIST_NAME, artistName); + values.put(TaskContract.TaskEntry.COLUMN_NAME_ARTIST_ID, artistId); + values.put(TaskContract.TaskEntry.COLUMN_NAME_PLAYLIST_ID, playlistId); + values.put(TaskContract.TaskEntry.COLUMN_NAME_ALBUM_ID, albumId); + values.put(TaskContract.TaskEntry.COLUMN_NAME_MUSIC_ID, musicId); db.beginTransaction(); try { @@ -223,8 +236,14 @@ private DownloadTask parseCursor(Cursor cursor) { int showNotification = cursor.getShort(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_SHOW_NOTIFICATION)); int clickToOpenDownloadedFile = cursor.getShort(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION)); long timeCreated = cursor.getLong(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_TIME_CREATED)); + String albumName = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_ALBUM_NAME)); + String artistName = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_ARTIST_NAME)); + String artistId = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_ARTIST_ID)); + String playlistId = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_PLAYLIST_ID)); + String albumId = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_ALBUM_ID)); + String musicId = cursor.getString(cursor.getColumnIndexOrThrow(TaskContract.TaskEntry.COLUMN_NAME_MUSIC_ID)); return new DownloadTask(primaryId, taskId, status, progress, url, filename, savedDir, headers, - mimeType, resumable == 1, showNotification == 1, clickToOpenDownloadedFile == 1, timeCreated); + mimeType, resumable == 1, showNotification == 1, clickToOpenDownloadedFile == 1, timeCreated, albumName, artistName, artistId, playlistId, albumId, musicId); } } diff --git a/android/src/main/java/vn/hunghd/flutterdownloader/TaskDbHelper.java b/android/src/main/java/vn/hunghd/flutterdownloader/TaskDbHelper.java index d6ddfafd..f4c80cde 100644 --- a/android/src/main/java/vn/hunghd/flutterdownloader/TaskDbHelper.java +++ b/android/src/main/java/vn/hunghd/flutterdownloader/TaskDbHelper.java @@ -7,7 +7,7 @@ import vn.hunghd.flutterdownloader.TaskContract.TaskEntry; public class TaskDbHelper extends SQLiteOpenHelper { - public static final int DATABASE_VERSION = 2; + public static final int DATABASE_VERSION = 5; public static final String DATABASE_NAME = "download_tasks.db"; private static TaskDbHelper instance = null; @@ -26,8 +26,14 @@ public class TaskDbHelper extends SQLiteOpenHelper { TaskEntry.COLUMN_NAME_RESUMABLE + " TINYINT DEFAULT 0, " + TaskEntry.COLUMN_NAME_SHOW_NOTIFICATION + " TINYINT DEFAULT 0, " + TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION + " TINYINT DEFAULT 0, " + - TaskEntry.COLUMN_NAME_TIME_CREATED + " INTEGER DEFAULT 0" - + ")"; + TaskEntry.COLUMN_NAME_TIME_CREATED + " INTEGER DEFAULT 0, " + + TaskEntry.COLUMN_NAME_ALBUM_NAME + " TEXT, " + + TaskEntry.COLUMN_NAME_ARTIST_NAME + " TEXT, " + + TaskEntry.COLUMN_NAME_ARTIST_ID + " TEXT," + + TaskEntry.COLUMN_NAME_PLAYLIST_ID + " TEXT," + + TaskEntry.COLUMN_NAME_ALBUM_ID + " TEXT," + + TaskEntry.COLUMN_NAME_MUSIC_ID + " TEXT" + + ")"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TaskEntry.TABLE_NAME; @@ -52,12 +58,28 @@ private TaskDbHelper(Context context) { @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_ENTRIES); + + } + public void tryStatement(SQLiteDatabase db, String query) { + try { + db.execSQL(query); + } catch (Exception e) {} } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL(SQL_DELETE_ENTRIES); - onCreate(db); + if (oldVersion < 5) { + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_ALBUM_NAME + " TEXT;"); + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_ARTIST_NAME + " TEXT;"); + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_ARTIST_ID + " TEXT;"); + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_PLAYLIST_ID + " TEXT;"); + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_ALBUM_ID + " TEXT;"); + tryStatement(db,"ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_NAME_MUSIC_ID + " TEXT;"); + } else { + // default migration. should only be a fallback solution + db.execSQL(SQL_DELETE_ENTRIES); + onCreate(db); + } } @Override diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 7af18d9b..0babf8ad 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,9 +1,11 @@ - Download started - Download in progress - Download canceled - Download failed - Download complete - Download paused + Iniciado + Baixando + cancelado + falhou + completo + pausado + Downloader + Progresso do download \ No newline at end of file diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies deleted file mode 100644 index 3d7e3dec..00000000 --- a/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]}]} \ No newline at end of file diff --git a/example/android/app/app.iml b/example/android/app/app.iml index 7504328b..cedcbb2c 100644 --- a/example/android/app/app.iml +++ b/example/android/app/app.iml @@ -4,8 +4,8 @@ @@ -19,7 +19,7 @@