Skip to content

Commit ff1ab06

Browse files
committed
feat(assists): 添加文件下载功能支持
- 新增 FileDownloadUtil 工具类,支持文件下载到应用内部存储目录 - 实现 downloadFile、downloadFileWithResume 和 downloadFileToCache 方法 - 支持下载进度回调和断点续传功能 - 在 ASJavascriptInterfaceAsync 中添加 download 方法支持 - 引入 OkHttp 网络库依赖,版本为 4.10.0 - 添加 assistsime 模块及相关配置文件和代码 - 实现输入法相关功能,包括事件处理和键盘布局等 - 添加 ProGuard 规则和 .gitignore 文件配置
1 parent 0063b99 commit ff1ab06

File tree

907 files changed

+60421
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

907 files changed

+60421
-7
lines changed

assists-web/src/main/java/com/ven/assists/web/ASJavascriptInterfaceAsync.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import java.nio.charset.StandardCharsets
6666
import java.util.concurrent.TimeUnit
6767
import androidx.core.net.toUri
6868
import com.blankj.utilcode.util.ActivityUtils
69+
import com.ven.assists.utils.FileDownloadUtil
6970

7071
class ASJavascriptInterfaceAsync(val webView: WebView) {
7172
var callIntercept: ((json: String) -> CallInterceptResult)? = null
@@ -116,6 +117,32 @@ class ASJavascriptInterfaceAsync(val webView: WebView) {
116117
val request = GsonUtils.fromJson<CallRequest<JsonObject>>(requestJson, object : TypeToken<CallRequest<JsonObject>>() {}.type)
117118
runCatching {
118119
val response = when (request.method) {
120+
CallMethod.download -> {
121+
val url = request.arguments?.get("url")?.asString ?: ""
122+
AssistsService.instance?.let {
123+
val result = FileDownloadUtil.downloadFile(it, url)
124+
when (result) {
125+
is FileDownloadUtil.DownloadResult.Error -> {
126+
val response = request.createResponse(-1, data = result.exception.message)
127+
response
128+
}
129+
130+
is FileDownloadUtil.DownloadResult.Success -> {
131+
val response = request.createResponse(0, data = result.file.path)
132+
response
133+
}
134+
135+
else -> {
136+
val response = request.createResponse(-1, data = null)
137+
response
138+
}
139+
}
140+
} ?: let {
141+
val response = request.createResponse(-1, data = null)
142+
response
143+
}
144+
}
145+
119146
CallMethod.getNetworkType -> {
120147
val networkType = NetworkUtils.getNetworkType()
121148
var networkTypeValue = ""

assists/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ dependencies {
3838
implementation "androidx.appcompat:appcompat:${rootProject.ext.appcompatVersion}"
3939
implementation platform("org.jetbrains.kotlin:kotlin-bom:${rootProject.ext.kotlinBomVersion}")
4040
api "com.blankj:utilcodex:${rootProject.ext.utilcodexVersion}"
41+
42+
// OkHttp 网络库
43+
api "com.squareup.okhttp3:okhttp:4.10.0"
4144
}
4245

4346
dokkaHtml {
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package com.ven.assists.utils
2+
3+
import android.content.Context
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.withContext
6+
import okhttp3.OkHttpClient
7+
import okhttp3.Request
8+
import okhttp3.Response
9+
import java.io.File
10+
import java.io.FileOutputStream
11+
import java.io.IOException
12+
import java.io.InputStream
13+
import java.util.concurrent.TimeUnit
14+
15+
/**
16+
* 文件下载工具类
17+
* 使用 OkHttp 实现文件下载到应用内部存储目录
18+
*/
19+
object FileDownloadUtil {
20+
21+
private val client: OkHttpClient by lazy {
22+
OkHttpClient.Builder()
23+
.connectTimeout(30, TimeUnit.SECONDS)
24+
.readTimeout(30, TimeUnit.SECONDS)
25+
.writeTimeout(30, TimeUnit.SECONDS)
26+
.retryOnConnectionFailure(true)
27+
.build()
28+
}
29+
30+
/**
31+
* 下载结果
32+
*/
33+
sealed class DownloadResult {
34+
data class Success(val file: File) : DownloadResult()
35+
data class Error(val exception: Exception) : DownloadResult()
36+
data class Progress(val current: Long, val total: Long, val percent: Int) : DownloadResult()
37+
}
38+
39+
/**
40+
* 下载文件到应用内部存储目录
41+
*
42+
* @param context 上下文
43+
* @param url 下载地址
44+
* @param fileName 保存的文件名,如果为空则从 URL 中提取
45+
* @param subDir 子目录名称,如果为空则保存在根目录
46+
* @param callback 下载进度回调
47+
* @return 下载结果
48+
*/
49+
suspend fun downloadFile(
50+
context: Context,
51+
url: String,
52+
fileName: String? = null,
53+
subDir: String? = null,
54+
callback: ((DownloadResult) -> Unit)? = null
55+
): DownloadResult = withContext(Dispatchers.IO) {
56+
try {
57+
// 确定保存目录
58+
val saveDir = if (subDir.isNullOrEmpty()) {
59+
context.filesDir
60+
} else {
61+
File(context.filesDir, subDir).apply {
62+
if (!exists()) mkdirs()
63+
}
64+
}
65+
66+
// 确定文件名
67+
val finalFileName = fileName ?: url.substringAfterLast('/').ifEmpty { "download_${System.currentTimeMillis()}" }
68+
val saveFile = File(saveDir, finalFileName)
69+
70+
// 如果文件已存在,先删除
71+
if (saveFile.exists()) {
72+
saveFile.delete()
73+
}
74+
75+
// 创建请求
76+
val request = Request.Builder()
77+
.url(url)
78+
.get()
79+
.build()
80+
81+
// 执行请求
82+
val response: Response = client.newCall(request).execute()
83+
84+
if (!response.isSuccessful) {
85+
throw IOException("Download failed: ${response.code}")
86+
}
87+
88+
val body = response.body ?: throw IOException("Response body is null")
89+
val contentLength = body.contentLength()
90+
val inputStream: InputStream = body.byteStream()
91+
val outputStream = FileOutputStream(saveFile)
92+
93+
try {
94+
val buffer = ByteArray(8192)
95+
var bytesRead: Int
96+
var totalBytesRead: Long = 0
97+
98+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
99+
outputStream.write(buffer, 0, bytesRead)
100+
totalBytesRead += bytesRead
101+
102+
// 计算进度
103+
if (contentLength > 0) {
104+
val percent = ((totalBytesRead * 100) / contentLength).toInt()
105+
withContext(Dispatchers.Main) {
106+
callback?.invoke(DownloadResult.Progress(totalBytesRead, contentLength, percent))
107+
}
108+
}
109+
}
110+
111+
outputStream.flush()
112+
DownloadResult.Success(saveFile)
113+
} finally {
114+
inputStream.close()
115+
outputStream.close()
116+
}
117+
} catch (e: Exception) {
118+
e.printStackTrace()
119+
DownloadResult.Error(e)
120+
}
121+
}
122+
123+
/**
124+
* 下载文件到应用内部存储目录(支持断点续传)
125+
*
126+
* @param context 上下文
127+
* @param url 下载地址
128+
* @param fileName 保存的文件名,如果为空则从 URL 中提取
129+
* @param subDir 子目录名称,如果为空则保存在根目录
130+
* @param callback 下载进度回调
131+
* @return 下载结果
132+
*/
133+
suspend fun downloadFileWithResume(
134+
context: Context,
135+
url: String,
136+
fileName: String? = null,
137+
subDir: String? = null,
138+
callback: ((DownloadResult) -> Unit)? = null
139+
): DownloadResult = withContext(Dispatchers.IO) {
140+
try {
141+
// 确定保存目录
142+
val saveDir = if (subDir.isNullOrEmpty()) {
143+
context.filesDir
144+
} else {
145+
File(context.filesDir, subDir).apply {
146+
if (!exists()) mkdirs()
147+
}
148+
}
149+
150+
// 确定文件名
151+
val finalFileName = fileName ?: url.substringAfterLast('/').ifEmpty { "download_${System.currentTimeMillis()}" }
152+
val saveFile = File(saveDir, finalFileName)
153+
154+
// 获取已下载的文件大小
155+
val downloadedLength = if (saveFile.exists()) saveFile.length() else 0L
156+
157+
// 创建请求,支持断点续传
158+
val requestBuilder = Request.Builder()
159+
.url(url)
160+
.get()
161+
162+
if (downloadedLength > 0) {
163+
requestBuilder.addHeader("Range", "bytes=$downloadedLength-")
164+
}
165+
166+
val request = requestBuilder.build()
167+
168+
// 执行请求
169+
val response: Response = client.newCall(request).execute()
170+
171+
if (!response.isSuccessful && response.code != 206) {
172+
throw IOException("Download failed: ${response.code}")
173+
}
174+
175+
val body = response.body ?: throw IOException("Response body is null")
176+
val contentLength = body.contentLength()
177+
val totalLength = if (response.code == 206) {
178+
contentLength + downloadedLength
179+
} else {
180+
contentLength
181+
}
182+
183+
val inputStream: InputStream = body.byteStream()
184+
val outputStream = FileOutputStream(saveFile, response.code == 206)
185+
186+
try {
187+
val buffer = ByteArray(8192)
188+
var bytesRead: Int
189+
var totalBytesRead: Long = downloadedLength
190+
191+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
192+
outputStream.write(buffer, 0, bytesRead)
193+
totalBytesRead += bytesRead
194+
195+
// 计算进度
196+
if (totalLength > 0) {
197+
val percent = ((totalBytesRead * 100) / totalLength).toInt()
198+
withContext(Dispatchers.Main) {
199+
callback?.invoke(DownloadResult.Progress(totalBytesRead, totalLength, percent))
200+
}
201+
}
202+
}
203+
204+
outputStream.flush()
205+
DownloadResult.Success(saveFile)
206+
} finally {
207+
inputStream.close()
208+
outputStream.close()
209+
}
210+
} catch (e: Exception) {
211+
e.printStackTrace()
212+
DownloadResult.Error(e)
213+
}
214+
}
215+
216+
/**
217+
* 下载文件到应用缓存目录
218+
*
219+
* @param context 上下文
220+
* @param url 下载地址
221+
* @param fileName 保存的文件名,如果为空则从 URL 中提取
222+
* @param callback 下载进度回调
223+
* @return 下载结果
224+
*/
225+
suspend fun downloadFileToCache(
226+
context: Context,
227+
url: String,
228+
fileName: String? = null,
229+
callback: ((DownloadResult) -> Unit)? = null
230+
): DownloadResult = withContext(Dispatchers.IO) {
231+
try {
232+
val cacheDir = context.cacheDir
233+
val finalFileName = fileName ?: url.substringAfterLast('/').ifEmpty { "cache_${System.currentTimeMillis()}" }
234+
val cacheFile = File(cacheDir, finalFileName)
235+
236+
// 如果文件已存在,先删除
237+
if (cacheFile.exists()) {
238+
cacheFile.delete()
239+
}
240+
241+
// 创建请求
242+
val request = Request.Builder()
243+
.url(url)
244+
.get()
245+
.build()
246+
247+
// 执行请求
248+
val response: Response = client.newCall(request).execute()
249+
250+
if (!response.isSuccessful) {
251+
throw IOException("Download failed: ${response.code}")
252+
}
253+
254+
val body = response.body ?: throw IOException("Response body is null")
255+
val contentLength = body.contentLength()
256+
val inputStream: InputStream = body.byteStream()
257+
val outputStream = FileOutputStream(cacheFile)
258+
259+
try {
260+
val buffer = ByteArray(8192)
261+
var bytesRead: Int
262+
var totalBytesRead: Long = 0
263+
264+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
265+
outputStream.write(buffer, 0, bytesRead)
266+
totalBytesRead += bytesRead
267+
268+
// 计算进度
269+
if (contentLength > 0) {
270+
val percent = ((totalBytesRead * 100) / contentLength).toInt()
271+
withContext(Dispatchers.Main) {
272+
callback?.invoke(DownloadResult.Progress(totalBytesRead, contentLength, percent))
273+
}
274+
}
275+
}
276+
277+
outputStream.flush()
278+
DownloadResult.Success(cacheFile)
279+
} finally {
280+
inputStream.close()
281+
outputStream.close()
282+
}
283+
} catch (e: Exception) {
284+
e.printStackTrace()
285+
DownloadResult.Error(e)
286+
}
287+
}
288+
289+
/**
290+
* 取消所有下载任务
291+
*/
292+
fun cancelAll() {
293+
client.dispatcher.cancelAll()
294+
}
295+
296+
/**
297+
* 获取文件大小(字节)
298+
*/
299+
suspend fun getFileSize(url: String): Long = withContext(Dispatchers.IO) {
300+
try {
301+
val request = Request.Builder()
302+
.url(url)
303+
.head()
304+
.build()
305+
306+
val response = client.newCall(request).execute()
307+
response.body?.contentLength() ?: -1L
308+
} catch (e: Exception) {
309+
e.printStackTrace()
310+
-1L
311+
}
312+
}
313+
}
314+

assistsime/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

assistsime/build.gradle

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apply plugin: 'com.android.library'
2+
3+
android {
4+
defaultConfig {
5+
compileSdk 36
6+
minSdkVersion 23
7+
targetSdkVersion 36
8+
}
9+
buildTypes {
10+
release {
11+
minifyEnabled true
12+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
13+
}
14+
}
15+
namespace 'rkr.simplekeyboard.inputmethod'
16+
}
17+
18+
dependencies {
19+
}

0 commit comments

Comments
 (0)