Skip to content

Commit 29d0dc8

Browse files
authored
#408 Android: Add a wifi only flag to the downloader (#754)
- Add `allowCellular` flag to Dart and default - Add flag to Android code and pass it to the `WorkManager` - Migrate the database on Android
1 parent 80756c1 commit 29d0dc8

File tree

8 files changed

+65
-47
lines changed

8 files changed

+65
-47
lines changed

android/src/main/kotlin/vn/hunghd/flutterdownloader/DownloadTask.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ data class DownloadTask(
1414
var showNotification: Boolean,
1515
var openFileFromNotification: Boolean,
1616
var timeCreated: Long,
17-
var saveInPublicStorage: Boolean
17+
var saveInPublicStorage: Boolean,
18+
var allowCellular: Boolean
1819
)

android/src/main/kotlin/vn/hunghd/flutterdownloader/DownloadWorker.kt

+27-29
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import java.net.URL
4141
import java.net.URLDecoder
4242
import java.security.SecureRandom
4343
import java.security.cert.X509Certificate
44+
import java.util.ArrayDeque
4445
import java.util.Locale
4546
import java.util.concurrent.atomic.AtomicBoolean
4647
import java.util.regex.Pattern
@@ -49,9 +50,6 @@ import javax.net.ssl.HttpsURLConnection
4950
import javax.net.ssl.SSLContext
5051
import javax.net.ssl.TrustManager
5152
import javax.net.ssl.X509TrustManager
52-
import java.util.ArrayDeque
53-
import kotlin.collections.ArrayList
54-
import kotlin.collections.HashMap
5553

5654
class DownloadWorker(context: Context, params: WorkerParameters) :
5755
Worker(context, params),
@@ -158,7 +156,7 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
158156
val headers: String = inputData.getString(ARG_HEADERS)
159157
?: throw IllegalArgumentException("Argument '$ARG_HEADERS' should not be null")
160158
var isResume: Boolean = inputData.getBoolean(ARG_IS_RESUME, false)
161-
var timeout: Int = inputData.getInt(ARG_TIMEOUT, 15000)
159+
val timeout: Int = inputData.getInt(ARG_TIMEOUT, 15000)
162160
debug = inputData.getBoolean(ARG_DEBUG, false)
163161
step = inputData.getInt(ARG_STEP, 10)
164162
ignoreSsl = inputData.getBoolean(ARG_IGNORESSL, false)
@@ -172,9 +170,9 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
172170
val task = taskDao?.loadTask(id.toString())
173171
log(
174172
"DownloadWorker{url=$url,filename=$filename,savedDir=$savedDir,header=$headers,isResume=$isResume,status=" + (
175-
task?.status
176-
?: "GONE"
177-
)
173+
task?.status
174+
?: "GONE"
175+
)
178176
)
179177

180178
// Task has been deleted or cancelled
@@ -258,10 +256,10 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
258256
savedDir: String,
259257
filename: String?,
260258
headers: String,
261-
isResume: Boolean,
262-
timeout: Int,
259+
isResume: Boolean,
260+
timeout: Int,
263261
) {
264-
var filename = filename
262+
var actualFilename = filename
265263
var url = fileURL
266264
var resourceUrl: URL
267265
var base: URL?
@@ -274,7 +272,7 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
274272
var downloadedBytes: Long = 0
275273
var responseCode: Int
276274
var times: Int
277-
var timeout = timeout
275+
var actualTimeout = timeout
278276
visited = HashMap()
279277
try {
280278
val task = taskDao?.loadTask(id.toString())
@@ -306,16 +304,16 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
306304
resourceUrl.openConnection() as HttpsURLConnection
307305
}
308306
log("Open connection to $url")
309-
httpConn.connectTimeout = timeout
310-
httpConn.readTimeout = timeout
307+
httpConn.connectTimeout = actualTimeout
308+
httpConn.readTimeout = actualTimeout
311309
httpConn.instanceFollowRedirects = false // Make the logic below easier to detect redirections
312310
httpConn.setRequestProperty("User-Agent", "Mozilla/5.0...")
313311

314312
// setup request headers if it is set
315313
setupHeaders(httpConn, headers)
316314
// try to continue downloading a file from its partial downloaded data.
317315
if (isResume) {
318-
downloadedBytes = setupPartialDownloadedDataHeader(httpConn, filename, savedDir)
316+
downloadedBytes = setupPartialDownloadedDataHeader(httpConn, actualFilename, savedDir)
319317
}
320318
responseCode = httpConn.responseCode
321319
when (responseCode) {
@@ -348,25 +346,25 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
348346
log("Charset = $charset")
349347
if (!isResume) {
350348
// try to extract filename from HTTP headers if it is not given by user
351-
if (filename == null) {
349+
if (actualFilename == null) {
352350
val disposition: String? = httpConn.getHeaderField("Content-Disposition")
353351
log("Content-Disposition = $disposition")
354352
if (!disposition.isNullOrEmpty()) {
355-
filename = getFileNameFromContentDisposition(disposition, charset)
353+
actualFilename = getFileNameFromContentDisposition(disposition, charset)
356354
}
357-
if (filename.isNullOrEmpty()) {
358-
filename = url.substring(url.lastIndexOf("/") + 1)
355+
if (actualFilename.isNullOrEmpty()) {
356+
actualFilename = url.substring(url.lastIndexOf("/") + 1)
359357
try {
360-
filename = URLDecoder.decode(filename, "UTF-8")
358+
actualFilename = URLDecoder.decode(actualFilename, "UTF-8")
361359
} catch (e: IllegalArgumentException) {
362360
/* ok, just let filename be not encoded */
363361
e.printStackTrace()
364362
}
365363
}
366364
}
367365
}
368-
log("fileName = $filename")
369-
taskDao?.updateTask(id.toString(), filename, contentType)
366+
log("fileName = $actualFilename")
367+
taskDao?.updateTask(id.toString(), actualFilename, contentType)
370368

371369
// opens input stream from the HTTP connection
372370
inputStream = httpConn.inputStream
@@ -375,7 +373,7 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
375373
// there are two case:
376374
if (isResume) {
377375
// 1. continue downloading (append data to partial downloaded file)
378-
savedFilePath = savedDir + File.separator + filename
376+
savedFilePath = savedDir + File.separator + actualFilename
379377
outputStream = FileOutputStream(savedFilePath, true)
380378
} else {
381379
// 2. new download, create new file
@@ -384,11 +382,11 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
384382
// or public shared download directory (external storage).
385383
// The second option will ignore `savedDir` parameter.
386384
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && saveInPublicStorage) {
387-
val uri = createFileInPublicDownloadsDir(filename, contentType)
385+
val uri = createFileInPublicDownloadsDir(actualFilename, contentType)
388386
savedFilePath = getMediaStoreEntryPathApi29(uri!!)
389387
outputStream = context.contentResolver.openOutputStream(uri, "w")
390388
} else {
391-
val file = createFileInAppSpecificDir(filename!!, savedDir)
389+
val file = createFileInAppSpecificDir(actualFilename!!, savedDir)
392390
savedFilePath = file!!.path
393391
outputStream = FileOutputStream(file, false)
394392
}
@@ -413,7 +411,7 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
413411
taskDao!!.updateTask(id.toString(), DownloadStatus.RUNNING, progress)
414412
updateNotification(
415413
context,
416-
filename,
414+
actualFilename,
417415
DownloadStatus.RUNNING,
418416
progress,
419417
null,
@@ -434,7 +432,7 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
434432
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
435433
if (isImageOrVideoFile(contentType) && isExternalStoragePath(savedFilePath)) {
436434
addImageOrVideoToGallery(
437-
filename,
435+
actualFilename,
438436
savedFilePath,
439437
getContentTypeWithoutCharset(contentType)
440438
)
@@ -459,19 +457,19 @@ class DownloadWorker(context: Context, params: WorkerParameters) :
459457
}
460458
}
461459
taskDao!!.updateTask(id.toString(), status, progress)
462-
updateNotification(context, filename, status, progress, pendingIntent, true)
460+
updateNotification(context, actualFilename, status, progress, pendingIntent, true)
463461
log(if (isStopped) "Download canceled" else "File downloaded")
464462
} else {
465463
val task = taskDao!!.loadTask(id.toString())
466464
val status =
467465
if (isStopped) if (task!!.resumable) DownloadStatus.PAUSED else DownloadStatus.CANCELED else DownloadStatus.FAILED
468466
taskDao!!.updateTask(id.toString(), status, lastProgress)
469-
updateNotification(context, filename ?: fileURL, status, -1, null, true)
467+
updateNotification(context, actualFilename ?: fileURL, status, -1, null, true)
470468
log(if (isStopped) "Download canceled" else "Server replied HTTP code: $responseCode")
471469
}
472470
} catch (e: IOException) {
473471
taskDao!!.updateTask(id.toString(), DownloadStatus.FAILED, lastProgress)
474-
updateNotification(context, filename ?: fileURL, DownloadStatus.FAILED, -1, null, true)
472+
updateNotification(context, actualFilename ?: fileURL, DownloadStatus.FAILED, -1, null, true)
475473
e.printStackTrace()
476474
} finally {
477475
if (outputStream != null) {

android/src/main/kotlin/vn/hunghd/flutterdownloader/FlutterDownloaderPlugin.kt

+9-8
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import io.flutter.plugin.common.MethodChannel
2222
import java.io.File
2323
import java.util.UUID
2424
import java.util.concurrent.TimeUnit
25-
import kotlin.collections.ArrayList
26-
import kotlin.collections.HashMap
2725

2826
private const val invalidTaskId = "invalid_task_id"
2927
private const val invalidStatus = "invalid_status"
@@ -91,14 +89,15 @@ class FlutterDownloaderPlugin : MethodChannel.MethodCallHandler, FlutterPlugin {
9189
openFileFromNotification: Boolean,
9290
isResume: Boolean,
9391
requiresStorageNotLow: Boolean,
94-
saveInPublicStorage: Boolean,
92+
saveInPublicStorage: Boolean,
9593
timeout: Int,
94+
allowCellular: Boolean,
9695
): WorkRequest {
9796
return OneTimeWorkRequest.Builder(DownloadWorker::class.java)
9897
.setConstraints(
9998
Constraints.Builder()
10099
.setRequiresStorageNotLow(requiresStorageNotLow)
101-
.setRequiredNetworkType(NetworkType.CONNECTED)
100+
.setRequiredNetworkType(if (allowCellular) NetworkType.CONNECTED else NetworkType.UNMETERED)
102101
.build()
103102
)
104103
.addTag(TAG)
@@ -169,17 +168,18 @@ class FlutterDownloaderPlugin : MethodChannel.MethodCallHandler, FlutterPlugin {
169168
val openFileFromNotification: Boolean = call.requireArgument("open_file_from_notification")
170169
val requiresStorageNotLow: Boolean = call.requireArgument("requires_storage_not_low")
171170
val saveInPublicStorage: Boolean = call.requireArgument("save_in_public_storage")
171+
val allowCellular: Boolean = call.requireArgument("allow_cellular")
172172
val request: WorkRequest = buildRequest(
173173
url, savedDir, filename, headers, showNotification,
174-
openFileFromNotification, false, requiresStorageNotLow, saveInPublicStorage, timeout
174+
openFileFromNotification, false, requiresStorageNotLow, saveInPublicStorage, timeout, allowCellular = allowCellular
175175
)
176176
WorkManager.getInstance(requireContext()).enqueue(request)
177177
val taskId: String = request.id.toString()
178178
result.success(taskId)
179179
sendUpdateProgress(taskId, DownloadStatus.ENQUEUED, 0)
180180
taskDao!!.insertOrUpdateNewTask(
181181
taskId, url, DownloadStatus.ENQUEUED, 0, filename,
182-
savedDir, headers, showNotification, openFileFromNotification, saveInPublicStorage
182+
savedDir, headers, showNotification, openFileFromNotification, saveInPublicStorage, allowCellular = allowCellular
183183
)
184184
}
185185

@@ -195,6 +195,7 @@ class FlutterDownloaderPlugin : MethodChannel.MethodCallHandler, FlutterPlugin {
195195
item["file_name"] = task.filename
196196
item["saved_dir"] = task.savedDir
197197
item["time_created"] = task.timeCreated
198+
item["allow_cellular"] = task.allowCellular
198199
array.add(item)
199200
}
200201
result.success(array)
@@ -256,7 +257,7 @@ class FlutterDownloaderPlugin : MethodChannel.MethodCallHandler, FlutterPlugin {
256257
val request: WorkRequest = buildRequest(
257258
task.url, task.savedDir, task.filename,
258259
task.headers, task.showNotification, task.openFileFromNotification,
259-
true, requiresStorageNotLow, task.saveInPublicStorage, timeout
260+
true, requiresStorageNotLow, task.saveInPublicStorage, timeout, allowCellular = task.allowCellular
260261
)
261262
val newTaskId: String = request.id.toString()
262263
result.success(newTaskId)
@@ -295,7 +296,7 @@ class FlutterDownloaderPlugin : MethodChannel.MethodCallHandler, FlutterPlugin {
295296
val request: WorkRequest = buildRequest(
296297
task.url, task.savedDir, task.filename,
297298
task.headers, task.showNotification, task.openFileFromNotification,
298-
false, requiresStorageNotLow, task.saveInPublicStorage, timeout
299+
false, requiresStorageNotLow, task.saveInPublicStorage, timeout, allowCellular = task.allowCellular
299300
)
300301
val newTaskId: String = request.id.toString()
301302
result.success(newTaskId)

android/src/main/kotlin/vn/hunghd/flutterdownloader/TaskDao.kt

+8-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class TaskDao(private val dbHelper: TaskDbHelper) {
2020
TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION,
2121
TaskEntry.COLUMN_NAME_SHOW_NOTIFICATION,
2222
TaskEntry.COLUMN_NAME_TIME_CREATED,
23-
TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE
23+
TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE,
24+
TaskEntry.COLUMN_ALLOW_CELLULAR,
2425
)
2526

2627
fun insertOrUpdateNewTask(
@@ -33,7 +34,8 @@ class TaskDao(private val dbHelper: TaskDbHelper) {
3334
headers: String?,
3435
showNotification: Boolean,
3536
openFileFromNotification: Boolean,
36-
saveInPublicStorage: Boolean
37+
saveInPublicStorage: Boolean,
38+
allowCellular: Boolean
3739
) {
3840
val db = dbHelper.writableDatabase
3941
val values = ContentValues()
@@ -53,6 +55,7 @@ class TaskDao(private val dbHelper: TaskDbHelper) {
5355
values.put(TaskEntry.COLUMN_NAME_RESUMABLE, 0)
5456
values.put(TaskEntry.COLUMN_NAME_TIME_CREATED, System.currentTimeMillis())
5557
values.put(TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE, if (saveInPublicStorage) 1 else 0)
58+
values.put(TaskEntry.COLUMN_ALLOW_CELLULAR, if(allowCellular) 1 else 0)
5659
db.beginTransaction()
5760
try {
5861
db.insertWithOnConflict(
@@ -243,6 +246,7 @@ class TaskDao(private val dbHelper: TaskDbHelper) {
243246
val clickToOpenDownloadedFile = cursor.getShort(cursor.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION)).toInt()
244247
val timeCreated = cursor.getLong(cursor.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_TIME_CREATED))
245248
val saveInPublicStorage = cursor.getShort(cursor.getColumnIndexOrThrow(TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE)).toInt()
249+
val allowCelluar = cursor.getShort(cursor.getColumnIndexOrThrow(TaskEntry.COLUMN_ALLOW_CELLULAR)).toInt()
246250
return DownloadTask(
247251
primaryId,
248252
taskId,
@@ -257,7 +261,8 @@ class TaskDao(private val dbHelper: TaskDbHelper) {
257261
showNotification == 1,
258262
clickToOpenDownloadedFile == 1,
259263
timeCreated,
260-
saveInPublicStorage == 1
264+
saveInPublicStorage == 1,
265+
allowCellular = allowCelluar == 1
261266
)
262267
}
263268
}

android/src/main/kotlin/vn/hunghd/flutterdownloader/TaskDbHelper.kt

+8-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ class TaskDbHelper private constructor(context: Context) :
1212
}
1313

1414
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
15-
if (oldVersion == 2 && newVersion == 3) {
15+
if(newVersion == 4) {
16+
db.execSQL("ALTER TABLE ${TaskEntry.TABLE_NAME} ADD COLUMN ${TaskEntry.COLUMN_ALLOW_CELLULAR} TINYINT DEFAULT 1")
17+
}
18+
else if (oldVersion == 2 && newVersion == 3) {
1619
db.execSQL("ALTER TABLE " + TaskEntry.TABLE_NAME + " ADD COLUMN " + TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE + " TINYINT DEFAULT 0")
1720
} else {
1821
db.execSQL(SQL_DELETE_ENTRIES)
@@ -25,7 +28,7 @@ class TaskDbHelper private constructor(context: Context) :
2528
}
2629

2730
companion object {
28-
const val DATABASE_VERSION = 3
31+
const val DATABASE_VERSION = 4
2932
const val DATABASE_NAME = "download_tasks.db"
3033
private var instance: TaskDbHelper? = null
3134
private const val SQL_CREATE_ENTRIES = (
@@ -43,10 +46,11 @@ class TaskDbHelper private constructor(context: Context) :
4346
TaskEntry.COLUMN_NAME_SHOW_NOTIFICATION + " TINYINT DEFAULT 0, " +
4447
TaskEntry.COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION + " TINYINT DEFAULT 0, " +
4548
TaskEntry.COLUMN_NAME_TIME_CREATED + " INTEGER DEFAULT 0, " +
46-
TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE + " TINYINT DEFAULT 0" +
49+
TaskEntry.COLUMN_SAVE_IN_PUBLIC_STORAGE + " TINYINT DEFAULT 0, " +
50+
TaskEntry.COLUMN_ALLOW_CELLULAR + " TINYINT DEFAULT 1" +
4751
")"
4852
)
49-
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TaskEntry.TABLE_NAME
53+
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${TaskEntry.TABLE_NAME}"
5054

5155
fun getInstance(ctx: Context?): TaskDbHelper {
5256
// Use the application context, which will ensure that you

android/src/main/kotlin/vn/hunghd/flutterdownloader/TaskEntry.kt

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ object TaskEntry : BaseColumns {
1717
const val COLUMN_NAME_OPEN_FILE_FROM_NOTIFICATION = "open_file_from_notification"
1818
const val COLUMN_NAME_TIME_CREATED = "time_created"
1919
const val COLUMN_SAVE_IN_PUBLIC_STORAGE = "save_in_public_storage"
20+
const val COLUMN_ALLOW_CELLULAR = "allow_cellular"
2021
}

lib/src/downloader.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'package:flutter_downloader/src/exceptions.dart';
1010
import 'callback_dispatcher.dart';
1111
import 'models.dart';
1212

13-
/// Singature for a function which is called when the download state of a task
13+
/// Signature for a function which is called when the download state of a task
1414
/// with [id] changes.
1515
typedef DownloadCallback = void Function(
1616
String id,
@@ -101,6 +101,7 @@ class FlutterDownloader {
101101
bool openFileFromNotification = true,
102102
bool requiresStorageNotLow = true,
103103
bool saveInPublicStorage = false,
104+
bool allowCellular = true,
104105
int timeout = 15000,
105106
}) async {
106107
assert(_initialized, 'plugin flutter_downloader is not initialized');
@@ -117,6 +118,7 @@ class FlutterDownloader {
117118
'requires_storage_not_low': requiresStorageNotLow,
118119
'save_in_public_storage': saveInPublicStorage,
119120
'timeout': timeout,
121+
'allow_cellular': allowCellular,
120122
});
121123

122124
if (taskId == null) {
@@ -160,6 +162,7 @@ class FlutterDownloader {
160162
filename: item['file_name'] as String?,
161163
savedDir: item['saved_dir'] as String,
162164
timeCreated: item['time_created'] as int,
165+
allowCellular: item['allow_cellular'] as bool,
163166
);
164167
},
165168
).toList();
@@ -217,6 +220,7 @@ class FlutterDownloader {
217220
filename: item['file_name'] as String?,
218221
savedDir: item['saved_dir'] as String,
219222
timeCreated: item['time_created'] as int,
223+
allowCellular: item['allow_cellular'] as bool,
220224
);
221225
},
222226
).toList();

0 commit comments

Comments
 (0)