diff --git a/.fvm/flutter_sdk b/.fvm/flutter_sdk
new file mode 120000
index 0000000..a334f59
--- /dev/null
+++ b/.fvm/flutter_sdk
@@ -0,0 +1 @@
+/Users/sebastian/fvm/versions/1.22.5
\ No newline at end of file
diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
new file mode 100644
index 0000000..4a41da6
--- /dev/null
+++ b/.fvm/fvm_config.json
@@ -0,0 +1,3 @@
+{
+ "flutterSdkVersion": "1.22.5"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 1c8d9d8..d8b3eb0 100644
--- a/README.md
+++ b/README.md
@@ -70,29 +70,33 @@ func registerPlugins(registry: FlutterPluginRegistry) {
### Optional configuration:
-- **Configure maximum number of concurrent tasks:** the plugin depends on `WorkManager` library and `WorkManager` depends on the number of available processor to configure the maximum number of tasks running at a moment. You can setup a fixed number for this configuration by adding following codes to your `AndroidManifest.xml`:
+#### Configure maximum number of concurrent tasks
-```xml
-
-
-
-
-
-
-
-
-
+The plugin depends on the `WorkManager` library. The configuration can be done using the instructions at [https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration](https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration).
+
+The example project shows a custom configuration of up to 10 simultaneous uploads.
+
+Two steps are required:
+
+Depend on the appropriate work-runtime in your host App.
+``` gradle
+implementation "androidx.work:work-runtime:$work_version"
```
+Override the default `Application` and implement the `androidx.work.Configuration.Provider` interface:
+
+``` java
+@NonNull
+@Override
+public Configuration getWorkManagerConfiguration() {
+ return new Configuration.Builder()
+ .setMinimumLoggingLevel(android.util.Log.INFO)
+ .setExecutor(Executors.newFixedThreadPool(10))
+ .build();
+}
+```
+
+
## Usage
#### Import package:
diff --git a/android/build.gradle b/android/build.gradle
index 0b9c5f5..6fe959a 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -40,6 +40,8 @@ android {
}
dependencies {
+ implementation 'com.microsoft.azure.android:azure-storage-android:2.0.0@aar'
+
implementation "androidx.work:work-runtime:2.4.0"
implementation "androidx.concurrent:concurrent-futures:1.1.0"
implementation "androidx.annotation:annotation:1.1.0"
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 077a608..bba894e 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1,5 @@
-
+
+
+
diff --git a/android/src/main/java/com/bluechilli/flutteruploader/AzureUploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/AzureUploadWorker.java
new file mode 100644
index 0000000..d1fefb8
--- /dev/null
+++ b/android/src/main/java/com/bluechilli/flutteruploader/AzureUploadWorker.java
@@ -0,0 +1,166 @@
+package com.bluechilli.flutteruploader;
+
+import static com.bluechilli.flutteruploader.UploadWorker.EXTRA_ID;
+import static com.bluechilli.flutteruploader.UploadWorker.EXTRA_STATUS;
+import static com.bluechilli.flutteruploader.UploadWorker.EXTRA_STATUS_CODE;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.work.Data;
+import androidx.work.ListenableWorker;
+import androidx.work.WorkerParameters;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.microsoft.azure.storage.AccessCondition;
+import com.microsoft.azure.storage.CloudStorageAccount;
+import com.microsoft.azure.storage.OperationContext;
+import com.microsoft.azure.storage.RetryNoRetry;
+import com.microsoft.azure.storage.blob.BlobRequestOptions;
+import com.microsoft.azure.storage.blob.CloudAppendBlob;
+import com.microsoft.azure.storage.blob.CloudBlobClient;
+import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.Channels;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AzureUploadWorker extends ListenableWorker {
+
+ private static final String TAG = "AzureUploadWorker";
+
+ /**
+ * @param appContext The application {@link Context}
+ * @param workerParams Parameters to setup the internal state of this worker
+ */
+ public AzureUploadWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
+ super(appContext, workerParams);
+ }
+
+ private Executor backgroundExecutor = Executors.newSingleThreadExecutor();
+
+ @NonNull
+ @Override
+ public ListenableFuture startWork() {
+ FlutterEngineHelper.start(getApplicationContext());
+
+ return CallbackToFutureAdapter.getFuture(
+ completer -> {
+ backgroundExecutor.execute(
+ () -> {
+ try {
+ final Result result = doWorkInternal();
+ completer.set(result);
+ } catch (Throwable e) {
+ Log.e(TAG, "Error while uploading to Azure", e);
+ completer.setException(e);
+ } finally {
+ // Do not destroy the engine at this very moment.
+ // Keep it running in the background for just a little while.
+ // stopEngine();
+ }
+ });
+
+ return getId().toString();
+ });
+ }
+
+ private Result doWorkInternal() throws Throwable {
+ final String connectionString = getInputData().getString("connectionString");
+ final String containerName = getInputData().getString("container");
+ final boolean createContainer = getInputData().getBoolean("createContainer", false);
+ final String blobName = getInputData().getString("blobName");
+ final String path = getInputData().getString("path");
+ final int blockSize = getInputData().getInt("blockSize", 1024 * 1024);
+
+ final SharedPreferences preferences =
+ getApplicationContext().getSharedPreferences("AzureUploadWorker", Context.MODE_PRIVATE);
+ final String bytesWrittenKey = "bytesWritten." + getId();
+
+ // Log.d(TAG, "bytesWrittenKey: " + bytesWrittenKey);
+
+ long bytesWritten = preferences.getInt(bytesWrittenKey, 0);
+
+ // Log.d(TAG, "bytesWritten : " + bytesWritten);
+
+ CloudStorageAccount account = CloudStorageAccount.parse(connectionString);
+ CloudBlobClient blobClient = account.createCloudBlobClient();
+
+ final CloudBlobContainer container = blobClient.getContainerReference(containerName);
+
+ final OperationContext opContext = new OperationContext();
+ // opContext.setLogLevel(BuildConfig.DEBUG ? Log.VERBOSE : Log.WARN);
+ opContext.setLogLevel(Log.WARN);
+
+ final BlobRequestOptions options = new BlobRequestOptions();
+ options.setRetryPolicyFactory(new RetryNoRetry());
+
+ if (createContainer) {
+ container.createIfNotExists(options, opContext);
+ }
+
+ final CloudAppendBlob appendBlob = container.getAppendBlobReference(blobName);
+
+ if (bytesWritten == 0) {
+ appendBlob.createOrReplace(AccessCondition.generateEmptyCondition(), options, opContext);
+ }
+
+ try (final RandomAccessFile file = new RandomAccessFile(path, "r");
+ final InputStream is = Channels.newInputStream(file.getChannel())) {
+ final long contentLength = file.length();
+
+ Log.d(TAG, "file contentLength: " + contentLength + ", blockSize: " + blockSize);
+ if (bytesWritten != 0) {
+ if (is.skip(bytesWritten) != bytesWritten) {
+ throw new IllegalArgumentException("source file length mismatch?");
+ }
+ }
+
+ while (bytesWritten < contentLength && !isStopped()) {
+ final long thisBlock = Math.min(contentLength - bytesWritten, blockSize);
+
+ Log.d(TAG, "Appending block at " + bytesWritten + ", thisBlock: " + thisBlock);
+
+ appendBlob.append(
+ is, thisBlock, AccessCondition.generateEmptyCondition(), options, opContext);
+
+ bytesWritten += thisBlock;
+
+ double p = ((double) (bytesWritten + thisBlock) / (double) contentLength) * 100;
+ int progress = (int) Math.round(p);
+
+ if (!isStopped()) {
+ setProgressAsync(
+ new Data.Builder()
+ .putInt("status", UploadStatus.RUNNING)
+ .putInt("progress", progress)
+ .build());
+ }
+
+ preferences.edit().putInt(bytesWrittenKey, (int) bytesWritten).apply();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Source path not found: " + path, e);
+ preferences.edit().remove(bytesWrittenKey).apply();
+ return Result.failure();
+ } catch (IOException e) {
+ return Result.retry();
+ } catch (Exception e) {
+ Log.e(TAG, "Unrecoverable exception: " + e);
+ preferences.edit().remove(bytesWrittenKey).apply();
+ return Result.failure();
+ }
+
+ final Data.Builder output =
+ new Data.Builder()
+ .putString(EXTRA_ID, getId().toString())
+ .putInt(EXTRA_STATUS, UploadStatus.COMPLETE)
+ .putInt(EXTRA_STATUS_CODE, 200);
+
+ return Result.success(output.build());
+ }
+}
diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java
new file mode 100644
index 0000000..ab21c2b
--- /dev/null
+++ b/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java
@@ -0,0 +1,43 @@
+package com.bluechilli.flutteruploader;
+
+import android.content.Context;
+import androidx.annotation.Nullable;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.view.FlutterCallbackInformation;
+import io.flutter.view.FlutterMain;
+
+public class FlutterEngineHelper {
+ @Nullable private static FlutterEngine engine;
+
+ public static void start(Context context) {
+ long callbackHandle = SharedPreferenceHelper.getCallbackHandle(context);
+
+ if (callbackHandle != -1L && engine == null) {
+ engine = new FlutterEngine(context);
+ FlutterMain.ensureInitializationComplete(context, null);
+
+ FlutterCallbackInformation callbackInfo =
+ FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
+ String dartBundlePath = FlutterMain.findAppBundlePath();
+
+ engine
+ .getDartExecutor()
+ .executeDartCallback(
+ new DartExecutor.DartCallback(context.getAssets(), dartBundlePath, callbackInfo));
+ }
+ }
+
+ // private void stopEngine() {
+ // Log.d(TAG, "Destroying worker engine.");
+ //
+ // if (engine != null) {
+ // try {
+ // engine.destroy();
+ // } catch (Throwable e) {
+ // Log.e(TAG, "Can not destroy engine", e);
+ // }
+ // engine = null;
+ // }
+ // }
+}
diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java
deleted file mode 100644
index 730edce..0000000
--- a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.bluechilli.flutteruploader;
-
-import android.content.ComponentName;
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.work.Configuration;
-import androidx.work.WorkManager;
-import java.util.concurrent.Executors;
-
-public class FlutterUploaderInitializer extends ContentProvider {
-
- private static final String TAG = "UploaderInitializer";
- private static final int DEFAULT_MAX_CONCURRENT_TASKS = 3;
- private static final int DEFAULT_UPLOAD_CONNECTION_TIMEOUT = 3600;
-
- @Override
- public boolean onCreate() {
- int maximumConcurrentTask = getMaxConcurrentTaskMetadata(getContext());
- WorkManager.initialize(
- getContext(),
- new Configuration.Builder()
- .setExecutor(Executors.newFixedThreadPool(maximumConcurrentTask))
- .build());
- return true;
- }
-
- @Nullable
- @Override
- public Cursor query(
- @NonNull Uri uri,
- @Nullable String[] strings,
- @Nullable String s,
- @Nullable String[] strings1,
- @Nullable String s1) {
- return null;
- }
-
- @Nullable
- @Override
- public String getType(@NonNull Uri uri) {
- return null;
- }
-
- @Nullable
- @Override
- public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
- return null;
- }
-
- @Override
- public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
- return 0;
- }
-
- @Override
- public int update(
- @NonNull Uri uri,
- @Nullable ContentValues contentValues,
- @Nullable String s,
- @Nullable String[] strings) {
- return 0;
- }
-
- private static int getMaxConcurrentTaskMetadata(Context context) {
- try {
- ProviderInfo pi =
- context
- .getPackageManager()
- .getProviderInfo(
- new ComponentName(
- context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"),
- PackageManager.GET_META_DATA);
- Bundle bundle = pi.metaData;
- int max =
- bundle.getInt(
- "com.bluechilli.flutteruploader.MAX_CONCURRENT_TASKS", DEFAULT_MAX_CONCURRENT_TASKS);
- Log.d(TAG, "MAX_CONCURRENT_TASKS = " + max);
- return max;
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage());
- } catch (NullPointerException e) {
- Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage());
- }
- return DEFAULT_MAX_CONCURRENT_TASKS;
- }
-
- public static int getConnectionTimeout(Context context) {
- try {
- ProviderInfo pi =
- context
- .getPackageManager()
- .getProviderInfo(
- new ComponentName(
- context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"),
- PackageManager.GET_META_DATA);
- Bundle bundle = pi.metaData;
- int max =
- bundle.getInt(
- "com.bluechilli.flutteruploader.UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS",
- DEFAULT_UPLOAD_CONNECTION_TIMEOUT);
- Log.d(TAG, "UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS = " + max);
- return max;
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage());
- } catch (NullPointerException e) {
- Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage());
- }
-
- return DEFAULT_UPLOAD_CONNECTION_TIMEOUT;
- }
-}
diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java
index c0d0dca..0a42d78 100644
--- a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java
+++ b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java
@@ -5,6 +5,8 @@
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.bluechilli.flutteruploader.plugin.CachingStreamHandler;
import com.bluechilli.flutteruploader.plugin.StatusListener;
@@ -18,6 +20,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/** FlutterUploaderPlugin */
@@ -38,6 +41,7 @@ public class FlutterUploaderPlugin implements FlutterPlugin, StatusListener {
private EventChannel resultEventChannel;
private final CachingStreamHandler