diff --git a/app/build.gradle b/app/build.gradle index 69bf5cd0..a6212aa7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,8 +3,8 @@ plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'com.google.dagger.hilt.android' + id 'realm-android' } - android { namespace 'com.navinfo.omqs' compileSdk 33 @@ -26,8 +26,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = '1.8' @@ -50,8 +50,12 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - //权限管理 - implementation 'com.github.getActivity:XXPermissions:16.5' + //权限管理 https://github.com/getActivity/XXPermissions + implementation 'com.github.getActivity:XXPermissions:16.8' + // 文件管理 https://github.com/K1rakishou/Fuck-Storage-Access-Framework + implementation 'com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.1.3' + // Android工具类库 https://blankj.com/2016/07/31/android-utils-code/ + implementation 'com.blankj:utilcodex:1.30.1' //依赖注入 //hilt implementation "com.google.dagger:hilt-android:2.44" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index db80cb12..99b60c14 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ + + android:requestLegacyExternalStorage="true"> + \ No newline at end of file diff --git a/app/src/main/assets/MergeOMDB.py b/app/src/main/assets/MergeOMDB.py new file mode 100644 index 00000000..401524b1 --- /dev/null +++ b/app/src/main/assets/MergeOMDB.py @@ -0,0 +1,95 @@ +# coding:utf-8 +# 合并指定目录下的omdb(sqlite)数据 + +import os +import sys +import json +import sqlite3 + + +# 定义遍历目录的函数 +def traverse_dir(path): + fileList = list() + for root, dirs, files in os.walk(path): + for file in files: + if str(file).endswith(".omdb"): + # 文件的完整路径 + file_path = os.path.join(root, file) + # 处理文件,例如读取文件内容等 + print(file_path) + fileList.append(file_path) + return fileList + + +# 打开配置文件,读取用户配置的 +def openConfigJson(path): + # 读取json配置,获取要抽取的表名 + with open(path, "r") as f: + configMap = json.load(f) + return configMap + + +# 按照tableList中指定的表名合并多个源数据库到指定目标数据库中 +def mergeSqliteData(originSqliteList, destSqlite, tableList): + destConn = sqlite3.connect(destSqlite) + destCursor = destConn.cursor() + + for originSqlite in originSqliteList: + originConn = sqlite3.connect(originSqlite) + originCursor = originConn.cursor() + # 从源数据库中遍历取出表list中的数据 + for table in tableList: + # 检查目标数据库中是否存在指定的表 + containsTable = destCursor.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'" % (table)).fetchall() + if not containsTable or len(containsTable) <= 0: + # 复制表结构 + originCursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'" % (table)) + createTableSql = originCursor.fetchone()[0] + destCursor.execute(createTableSql) + destConn.commit() + + originCursor.execute("Select * From " + table) + # 获取到源数据库中该表的所有数据 + originData = originCursor.fetchall() + # 获取一行数据中包含多少列,以此动态设置sql语句中的?个数 + if originData and len(originData)>0: + num_cols = len(originData[0]) + placeholders = ",".join(["?"] * num_cols) + for row in originData: + destCursor.execute("INSERT INTO "+table+" VALUES ({})".format(placeholders), row) + + print("{}数据已导入!".format(originSqlite)) + originCursor.close() + originConn.close() + destConn.commit() + destCursor.close() + destConn.close() + + +if __name__ == '__main__': + params = sys.argv[1:] # 截取参数 + if params: + if not params[0]: + print("请输入要合并的omdb数据的文件夹") + raise AttributeError("请输入要合并的omdb数据的文件夹") + # 获取导出文件的表配置 + jsonPath = params[0] + "/config.json" + if not os.path.exists(jsonPath): + raise AttributeError("指定目录下缺少config.json配置文件") + omdbDir = params[0] + originSqliteList = traverse_dir(omdbDir) # 获取到所有的omdb数据库的路径 + + tableNameList = list() + configMap = openConfigJson(jsonPath) + if configMap["tables"] and len(configMap["tables"]) > 0: + for tableName in set(configMap["tables"]): + tableNameList.append(tableName) + print(tableNameList) + else: + raise AttributeError("config.json文件中没有配置抽取数据的表名") + + # 开始分别连接Sqlite数据库,按照指定表名合并数据 + mergeSqliteData(originSqliteList, params[0]+"/output.sqlite", tableNameList) + else: + raise AttributeError("缺少参数:请输入要合并的omdb数据的文件夹") diff --git a/app/src/main/java/com/navinfo/omqs/Constant.kt b/app/src/main/java/com/navinfo/omqs/Constant.kt index 92d50246..306ff4e3 100644 --- a/app/src/main/java/com/navinfo/omqs/Constant.kt +++ b/app/src/main/java/com/navinfo/omqs/Constant.kt @@ -1,13 +1,29 @@ package com.navinfo.omqs +import io.realm.Realm + class Constant { companion object { /** * sd卡根目录 */ lateinit var ROOT_PATH: String + + /** + * 地图目录 + */ lateinit var MAP_PATH: String + /** + * 数据目录 + */ + lateinit var DATA_PATH: String + + /** + * 离线地图目录 + */ + lateinit var OFFLINE_MAP_PATH: String + /** * 服务器地址 */ @@ -20,7 +36,7 @@ class Constant { const val message_version_right_off = "1" //立即发送 const val MESSAGE_PAGE_SIZE = 30 //消息列表一页最多数量 - + lateinit var realm: Realm } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/OMQSApplication.kt b/app/src/main/java/com/navinfo/omqs/OMQSApplication.kt index 7e85a078..27e26fa3 100644 --- a/app/src/main/java/com/navinfo/omqs/OMQSApplication.kt +++ b/app/src/main/java/com/navinfo/omqs/OMQSApplication.kt @@ -1,8 +1,18 @@ package com.navinfo.omqs import android.app.Application +import android.util.Log +import com.navinfo.omqs.tools.FileManager import dagger.hilt.android.HiltAndroidApp +import io.realm.Realm +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.File @HiltAndroidApp class OMQSApplication : Application() { + override fun onCreate() { + FileManager.initRootDir(this) + super.onCreate() + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/bean/OfflineMapCityBean.kt b/app/src/main/java/com/navinfo/omqs/bean/OfflineMapCityBean.kt deleted file mode 100644 index ea294e98..00000000 --- a/app/src/main/java/com/navinfo/omqs/bean/OfflineMapCityBean.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.navinfo.omqs.bean - -data class OfflineMapCityBean( - val id: String, - val fileName: String, - val name: String, - val url: String, - val version: Long, - val fileSize: Long, - var currentSize:Long = 0, - var status:Int = NONE -) { - companion object Status{ - const val NONE = 0 //无状态 - const val WAITING = 1 //等待中 - const val LOADING = 2 //下载中 - const val PAUSE = 3 //暂停 - const val ERROR = 4 //错误 - const val DONE = 5 //完成 - const val UPDATE = 6 //有新版本要更新 - } - fun getFileSizeText(): String { - return if (fileSize < 1024.0) - "$fileSize B" - else if (fileSize < 1048576.0) - "%.2f K".format(fileSize / 1024.0) - else if (fileSize < 1073741824.0) - "%.2f M".format(fileSize / 1048576.0) - else - "%.2f M".format(fileSize / 1073741824.0) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/data/process/DataEngine.kt b/app/src/main/java/com/navinfo/omqs/data/process/DataEngine.kt new file mode 100644 index 00000000..581343c3 --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/data/process/DataEngine.kt @@ -0,0 +1,8 @@ +package com.navinfo.omqs.data.process + +/** + * 数据处理引擎,数据导入、导出及转换处理 + * */ +class DataEngine { + +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/hilt/GlobalModule.kt b/app/src/main/java/com/navinfo/omqs/hilt/GlobalModule.kt index 90cac2bb..4ace2b64 100644 --- a/app/src/main/java/com/navinfo/omqs/hilt/GlobalModule.kt +++ b/app/src/main/java/com/navinfo/omqs/hilt/GlobalModule.kt @@ -6,11 +6,13 @@ import com.google.gson.Gson import com.navinfo.omqs.Constant import com.navinfo.omqs.OMQSApplication import com.navinfo.omqs.http.RetrofitNetworkServiceAPI +import com.navinfo.omqs.tools.RealmCoroutineScope import dagger.Lazy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.* import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -25,11 +27,11 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class GlobalModule { -// @Singleton -// @Provides -// fun provideApplication(application: Application): OMQSApplication { -// return application as OMQSApplication -// } + @Singleton + @Provides + fun provideApplication(application: Application): OMQSApplication { + return application as OMQSApplication + } /** * 注入 网络OKHttp 对象 @@ -54,7 +56,8 @@ class GlobalModule { } }.apply { level = if (Constant.DEBUG) { - HttpLoggingInterceptor.Level.BODY + //坑 !!!! 下载文件时打印log 内存不足 + HttpLoggingInterceptor.Level.HEADERS } else { HttpLoggingInterceptor.Level.NONE } @@ -91,4 +94,13 @@ class GlobalModule { fun provideNetworkService(retrofit: Retrofit): RetrofitNetworkServiceAPI { return retrofit.create(RetrofitNetworkServiceAPI::class.java) } + + /** + * realm 注册 + */ + @Provides + @Singleton + fun provideRealmService(context: Application): RealmCoroutineScope { + return RealmCoroutineScope(context) + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/hilt/MainActivityModule.kt b/app/src/main/java/com/navinfo/omqs/hilt/MainActivityModule.kt index 92dd5207..8560c38b 100644 --- a/app/src/main/java/com/navinfo/omqs/hilt/MainActivityModule.kt +++ b/app/src/main/java/com/navinfo/omqs/hilt/MainActivityModule.kt @@ -1,13 +1,13 @@ package com.navinfo.omqs.hilt -import android.content.Context import com.navinfo.collect.library.map.NIMapController +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager +import com.navinfo.omqs.tools.RealmCoroutineScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityRetainedScoped @InstallIn(ActivityRetainedComponent::class) @@ -26,8 +26,11 @@ class MainActivityModule { */ @ActivityRetainedScoped @Provides - fun providesOfflineMapDownloadManager(@ActivityContext context: Context): OfflineMapDownloadManager = - OfflineMapDownloadManager(context) + fun providesOfflineMapDownloadManager( + networkServiceAPI: RetrofitNetworkServiceAPI, + realmManager: RealmCoroutineScope + ): OfflineMapDownloadManager = + OfflineMapDownloadManager(networkServiceAPI, realmManager) /** * 实验失败,这样创建,viewmodel不会在activity销毁的时候同时销毁 @@ -35,7 +38,6 @@ class MainActivityModule { // @ActivityRetainedScoped // @Provides // fun providesMainViewModel(mapController: NIMapController): MainViewModel { -// Log.e("jingo", "MainViewModel 被创建") // return MainViewModel(mapController) // } diff --git a/app/src/main/java/com/navinfo/omqs/http/NetworkService.kt b/app/src/main/java/com/navinfo/omqs/http/NetworkService.kt index 8ed34846..eb4a6103 100644 --- a/app/src/main/java/com/navinfo/omqs/http/NetworkService.kt +++ b/app/src/main/java/com/navinfo/omqs/http/NetworkService.kt @@ -1,6 +1,7 @@ package com.navinfo.omqs.http -import com.navinfo.omqs.bean.OfflineMapCityBean +import com.navinfo.collect.library.data.entity.OfflineMapCityBean + /** * 网络访问 业务接口 diff --git a/app/src/main/java/com/navinfo/omqs/http/NetworkServiceImpl.kt b/app/src/main/java/com/navinfo/omqs/http/NetworkServiceImpl.kt index 4a67bf83..bbcafc3e 100644 --- a/app/src/main/java/com/navinfo/omqs/http/NetworkServiceImpl.kt +++ b/app/src/main/java/com/navinfo/omqs/http/NetworkServiceImpl.kt @@ -1,6 +1,6 @@ package com.navinfo.omqs.http -import com.navinfo.omqs.bean.OfflineMapCityBean +import com.navinfo.collect.library.data.entity.OfflineMapCityBean import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject diff --git a/app/src/main/java/com/navinfo/omqs/http/RetrofitNetworkServiceAPI.kt b/app/src/main/java/com/navinfo/omqs/http/RetrofitNetworkServiceAPI.kt index 5267cc8c..4bea1858 100644 --- a/app/src/main/java/com/navinfo/omqs/http/RetrofitNetworkServiceAPI.kt +++ b/app/src/main/java/com/navinfo/omqs/http/RetrofitNetworkServiceAPI.kt @@ -1,13 +1,12 @@ package com.navinfo.omqs.http -import androidx.lifecycle.LiveData -import com.navinfo.omqs.bean.OfflineMapCityBean +import com.navinfo.collect.library.data.entity.OfflineMapCityBean import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Streaming import retrofit2.http.Url -import java.util.concurrent.Flow /** * retrofit2 网络请求接口 @@ -47,7 +46,7 @@ interface RetrofitNetworkServiceAPI { */ @Streaming @GET - suspend fun retrofitDownLoadFile(@Url url: String):Response + suspend fun retrofitDownLoadFile(@Header("RANGE") start: String? = "0", @Url url: String):Response /** diff --git a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt index cc720498..e4aa57e1 100644 --- a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt +++ b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadManager.kt @@ -1,48 +1,114 @@ package com.navinfo.omqs.http.offlinemapdownload -import android.content.Context -import android.os.Environment -import android.text.TextUtils -import com.navinfo.omqs.Constant -import com.navinfo.omqs.bean.OfflineMapCityBean -import dagger.hilt.android.qualifiers.ActivityContext -import java.io.Serializable +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import com.navinfo.collect.library.data.entity.OfflineMapCityBean +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI +import com.navinfo.omqs.tools.RealmCoroutineScope import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject /** * 管理离线地图下载 */ -class OfflineMapDownloadManager @Inject constructor(@ActivityContext context: Context) { +class OfflineMapDownloadManager( + val netApi: RetrofitNetworkServiceAPI, val realmManager: RealmCoroutineScope +) { /** * 最多同时下载数量 */ - private val MAX_SCOPE = 5 + private val MAX_SCOPE = 3 /** - * 存储有哪些城市需要下载 + * 存储有哪些城市需要下载的队列 */ private val scopeMap: ConcurrentHashMap by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentHashMap() } - val downloadFolder: String? by lazy { - Constant.MAP_PATH + "/offline/" + /** + * 存储正在下载的城市队列 + */ + private val taskScopeMap: ConcurrentHashMap by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + ConcurrentHashMap() } /** + * 启动下载任务 + * 请不要直接使用此方法启动下载任务,它是交由[OfflineMapDownloadScope]进行调用 + */ + fun launchScope(scope: OfflineMapDownloadScope) { + if (taskScopeMap.size >= MAX_SCOPE) { + return + } + if (taskScopeMap.contains(scope.cityBean.id)) { + return + } + taskScopeMap[scope.cityBean.id] = scope + scope.launch() + } + + /** + * 启动下一个任务,如果有正在等待中的任务的话 + * 请不要直接使用此方法启动下载任务,它是交由[OfflineMapDownloadScope]进行调用 + * @param previousUrl 上一个下载任务的下载连接 + */ + fun launchNext(previousUrl: String) { + taskScopeMap.remove(previousUrl) + for (entrySet in scopeMap) { + val downloadScope = entrySet.value + if (downloadScope.isWaiting()) { + launchScope(downloadScope) + break + } + } + } + + /** + * 暂停任务 + * 只有等待中的任务和正在下载中的任务才可以进行暂停操作 + */ + fun pause(id: String) { + if (taskScopeMap.containsKey(id)) { + val downloadScope = taskScopeMap[id] + downloadScope?.let { + downloadScope.pause() + } + launchNext(id) + } + + } + + /** + * 将下载任务加入到协程作用域的下载队列里 * 请求一个下载任务[OfflineMapDownloadScope] * 这是创建[OfflineMapDownloadScope]的唯一途径,请不要通过其他方式创建[OfflineMapDownloadScope] */ - fun request(cityBean: OfflineMapCityBean): OfflineMapDownloadScope? { - //没有下载连接的不能下载 - if (TextUtils.isEmpty(cityBean.url)) return null -// if(scopeMap.containsKey()) - var downloadScope = scopeMap[cityBean.id] - if (downloadScope == null) { - scopeMap[cityBean.id] = OfflineMapDownloadScope(cityBean) - } - return downloadScope + fun start(id: String) { + scopeMap[id]?.start() } + + + fun addTask(cityBean: OfflineMapCityBean) { + if (!scopeMap.containsKey(cityBean.id)) { + scopeMap[cityBean.id] = OfflineMapDownloadScope(this, cityBean) + } + } + + + fun observer( + id: String, lifecycleOwner: LifecycleOwner, observer: Observer + ) { + if (scopeMap.containsKey(id)) { + scopeMap[id]!!.observer(lifecycleOwner, observer) + } + } + + fun removeObserver(id: String) { + if (scopeMap.containsKey(id)) { + scopeMap[id]!!.removeObserver() + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt index a51f93d5..0821f8c0 100644 --- a/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt +++ b/app/src/main/java/com/navinfo/omqs/http/offlinemapdownload/OfflineMapDownloadScope.kt @@ -1,26 +1,176 @@ package com.navinfo.omqs.http.offlinemapdownload +import android.util.Log +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData -import com.navinfo.omqs.bean.OfflineMapCityBean -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlin.coroutines.EmptyCoroutineContext +import androidx.lifecycle.Observer +import com.navinfo.collect.library.data.entity.OfflineMapCityBean +import com.navinfo.omqs.Constant +import kotlinx.coroutines.* +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.RandomAccessFile /** * 代表一个下载任务 * [OfflineMapCityBean.id]将做为下载任务的唯一标识 - * 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.download]获取此对象 + * 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.request]获取此对象 * 这是一个协程作用域, * EmptyCoroutineContext 表示一个不包含任何元素的协程上下文,它通常用于创建新的协程上下文,或者作为协程上下文的基础。 */ -class OfflineMapDownloadScope(cityBean: OfflineMapCityBean) : CoroutineScope by CoroutineScope(EmptyCoroutineContext) { +class OfflineMapDownloadScope( + private val downloadManager: OfflineMapDownloadManager, + val cityBean: OfflineMapCityBean, +) : + CoroutineScope by CoroutineScope(Dispatchers.IO) { /** - * + *下载任务,用来取消的 */ private var downloadJob: Job? = null /** - * + * 管理观察者,同时只有一个就行了 + */ +// private var observer: Observer? = null + private var lifecycleOwner: LifecycleOwner? = null + + /** + *通知UI更新 */ private val downloadData = MutableLiveData() + + init { + downloadData.value = cityBean + } + + //改进的代码 + fun start() { + change(OfflineMapCityBean.WAITING) + downloadManager.launchScope(this@OfflineMapDownloadScope) + } + + /** + * 暂停任务 + * 其实就是取消任务,移除监听 + */ + fun pause() { + downloadJob?.cancel("pause") + change(OfflineMapCityBean.PAUSE) + } + + /** + * 启动协程进行下载 + * 请不要尝试在外部调用此方法,那样会脱离[OfflineMapDownloadManager]的管理 + */ + fun launch() { + downloadJob = launch() { + Log.e("jingo", "启动下载1") + download() + Log.e("jingo", "启动下载2") + downloadManager.launchNext(cityBean.id) + Log.e("jingo", "启动下载3") + } + } + + + /** + * 是否是等待任务 + */ + fun isWaiting(): Boolean { + return cityBean.status == OfflineMapCityBean.WAITING + } + + /** + * 更新任务 + * @param status [OfflineMapCityBean.Status] + */ + private fun change(status: Int) { + if (cityBean.status != status || status == OfflineMapCityBean.LOADING) { + cityBean.status = status + downloadData.postValue(cityBean) + + downloadManager.realmManager.launch { + downloadManager.realmManager.insertOrUpdate(cityBean) + } + } + } + + /** + * 添加下载任务观察者 + */ + fun observer(owner: LifecycleOwner, ob: Observer) { + removeObserver() + this.lifecycleOwner = owner + downloadData.observe(owner, ob) + } + + /** + * 下载文件 + */ + private suspend fun download() { + var inputStream: InputStream? = null + var randomAccessFile: RandomAccessFile? = null + try { + //创建离线地图 下载文件夹,.map文件夹的下一级 + val fileDir = File("${Constant.OFFLINE_MAP_PATH}download") + if (!fileDir.exists()) { + fileDir.mkdirs() + } + + val fileTemp = + File("${Constant.OFFLINE_MAP_PATH}download/${cityBean.id}_${cityBean.version}") + val startPosition = cityBean.currentSize + //验证断点有效性 + if (startPosition < 0) throw IOException("jingo Start position less than zero") + val response = downloadManager.netApi.retrofitDownLoadFile( + start = "bytes=$startPosition-", + url = cityBean.url + ) + val responseBody = response.body() + change(OfflineMapCityBean.LOADING) + responseBody ?: throw IOException("jingo ResponseBody is null") + //写入文件 + randomAccessFile = RandomAccessFile(fileTemp, "rwd") + randomAccessFile.seek(startPosition) + cityBean.currentSize = startPosition + inputStream = responseBody.byteStream() + val bufferSize = 1024 * 2 + val buffer = ByteArray(bufferSize) + + var readLength = 0 + while (downloadJob?.isActive == true) { + readLength = inputStream.read(buffer) + if (readLength != -1) { + randomAccessFile.write(buffer, 0, readLength) + cityBean.currentSize += readLength + change(OfflineMapCityBean.LOADING) + } else { + break + } + } + + Log.e("jingo", "文件下载完成 ${cityBean.currentSize} == ${cityBean.fileSize}") + if (cityBean.currentSize == cityBean.fileSize) { + val res = + fileTemp.renameTo(File("${Constant.OFFLINE_MAP_PATH}${cityBean.fileName}")) + Log.e("jingo", "文件下载完成 修改文件 $res") + change(OfflineMapCityBean.DONE) + } else { + change(OfflineMapCityBean.PAUSE) + } + } catch (e: Throwable) { + change(OfflineMapCityBean.ERROR) + } finally { + inputStream?.close() + randomAccessFile?.close() + } + } + + fun removeObserver() { + lifecycleOwner?.let { + downloadData.removeObservers(it) + null + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/tools/FileManager.kt b/app/src/main/java/com/navinfo/omqs/tools/FileManager.kt new file mode 100644 index 00000000..e40972ee --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/tools/FileManager.kt @@ -0,0 +1,95 @@ +package com.navinfo.omqs.tools + +import android.content.Context +import android.util.Log +import com.navinfo.collect.library.data.entity.OfflineMapCityBean +import com.navinfo.omqs.Constant +import java.io.File + +class FileManager { + companion object { + //初始化数据文件夹 + fun initRootDir(context:Context){ + // 在SD卡创建项目目录 + val sdCardPath = context.getExternalFilesDir(null) + sdCardPath?.let { + Constant.ROOT_PATH = sdCardPath.absolutePath + Constant.MAP_PATH = Constant.ROOT_PATH + "/map/" + Constant.OFFLINE_MAP_PATH = Constant.MAP_PATH + "offline/" + val file = File(Constant.MAP_PATH) + if (!file.exists()) { + file.mkdirs() + Constant.DATA_PATH = Constant.ROOT_PATH + "/data/" + with(File(Constant.MAP_PATH)) { + if (!this.exists()) this.mkdirs() + } + with(File(Constant.DATA_PATH)) { + if (!this.exists()) this.mkdirs() + } + }else{ + Constant.DATA_PATH = Constant.ROOT_PATH + "/data/" + } + } + } + + /** + * 检查离线地图文件 + */ + suspend fun checkOfflineMapFileInfo(cityBean: OfflineMapCityBean) { + //访问离线地图文件夹 + val fileDir = File("${Constant.OFFLINE_MAP_PATH}") + //如果连本地文件夹还没有,就不用修改任何数据了 + if (!fileDir.exists()) { + return + } + //访问离线地图临时下载文件夹 + val fileTempDir = File("${Constant.OFFLINE_MAP_PATH}download/") + //是否有一份.map文件了 + var mapFile: File? = null + //文件夹里文件挨个访问 + for (item in fileDir.listFiles()) { + //先找到对应的省市文件,例如:540000_西藏自治区_20230401195018.map",以id开头 + if (item.isFile && item.name.startsWith(cityBean.id)) { + //如果本地文件与从网络获取到版本号一致,表示这个文件已经下载完毕,不用处理了 + if (item.name.contains("_${cityBean.version}.map")) { + cityBean.status = OfflineMapCityBean.DONE + return + } + //文件存在,版本号不对应,留给下面流程处理 + mapFile = item + break + } + } + //临时下载文件夹 + if (fileTempDir.exists()) { + for (item in fileTempDir.listFiles()) { + //先找到对应的省市文件,例如:540000_20230401195018",以id开头 + if (item.isFile && item.name.startsWith(cityBean.id)) { + //如果本地文件与从网络获取到版本号一致,表示这个文件已经在下载列表中 + if (item.name == "${cityBean.id}_${cityBean.version}") { + //如果这个临时文件的大小和下载大小是一致的,说明已经下载完了,但是在下载环节没有更名移动成功,需要重命名和移动文件夹 + if (item.length() == cityBean.fileSize) { + //移动更名文件后删除旧数据,修改状态 + if (item.renameTo(File("${Constant.OFFLINE_MAP_PATH}${cityBean.fileName}"))) { + //删除旧版本数据 + mapFile?.delete() + cityBean.status = OfflineMapCityBean.DONE + return + } + } else { // 临时文件大小和目标不一致,说明下载了一半 + cityBean.status = OfflineMapCityBean.PAUSE + cityBean.currentSize = item.length() + return + } + } else { //虽然省市id开头一致,但是版本号不一致,说明之前版本下载了一部分,现在要更新了,原来下载的文件直接删除 + cityBean.status = OfflineMapCityBean.UPDATE + item.delete() + return + } + break + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/tools/RealmCoroutineScope.kt b/app/src/main/java/com/navinfo/omqs/tools/RealmCoroutineScope.kt new file mode 100644 index 00000000..70880896 --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/tools/RealmCoroutineScope.kt @@ -0,0 +1,57 @@ +package com.navinfo.omqs.tools + +import android.app.Application +import com.navinfo.collect.library.data.entity.OfflineMapCityBean +import com.navinfo.omqs.Constant +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmModel +import io.realm.Sort +import io.realm.kotlin.where +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext +import java.io.File + +class RealmCoroutineScope(context: Application) : + CoroutineScope by CoroutineScope(newSingleThreadContext("RealmThread")) { + lateinit var realm: Realm + + init { + launch { + Realm.init(context) + val password = "password".encodeToByteArray().copyInto(ByteArray(64)) + // 1110000011000010111001101110011011101110110111101110010011001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// Log.d("", "密码是: ${BigInteger(1, password).toString(2).padStart(64, '0')}") + val config = RealmConfiguration.Builder() + .directory(File(Constant.DATA_PATH)) + .name("HDData") +// .encryptionKey(password) + .build() + Realm.setDefaultConfiguration(config) + realm = Realm.getDefaultInstance() + } + } + + suspend fun getOfflineCityList(): List { + var list: List = mutableListOf() + realm.executeTransaction { + val objects = realm.where().findAll().sort("id", Sort.ASCENDING) + list = realm.copyFromRealm(objects) + } + return list + } + + suspend fun insertOrUpdate(objects: Collection?) { + realm.executeTransaction { + realm.insertOrUpdate(objects) + } + } + + suspend fun insertOrUpdate(realmModel: RealmModel?) { + realm.executeTransaction { + realm.insertOrUpdate(realmModel) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/MainActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/MainActivity.kt new file mode 100644 index 00000000..6bad71e2 --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/ui/MainActivity.kt @@ -0,0 +1,84 @@ +package com.navinfo.omqs.ui + +import android.content.Intent +import android.os.Bundle +import androidx.core.view.WindowCompat +import androidx.navigation.ui.AppBarConfiguration +import com.github.k1rakishou.fsaf.FileChooser +import com.github.k1rakishou.fsaf.callback.FSAFActivityCallbacks +import com.navinfo.omqs.databinding.ActivityMainBinding +import com.navinfo.omqs.ui.activity.PermissionsActivity + +class MainActivity : PermissionsActivity(), FSAFActivityCallbacks { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + private val fileChooser by lazy { FileChooser(this@MainActivity) } + + override fun onCreate(savedInstanceState: Bundle?) { + WindowCompat.setDecorFitsSystemWindows(window, false) + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + +// val navController = findNavController(R.id.nav_host_fragment_content_main) +// appBarConfiguration = AppBarConfiguration(navController.graph) +// setupActionBarWithNavController(navController, appBarConfiguration) + + fileChooser.setCallbacks(this@MainActivity) +// binding.fab.setOnClickListener { view -> +// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) +// .setAnchorView(R.id.fab) +// .setAction("Action", null).show() +// // 开始数据导入功能 +// fileChooser.openChooseFileDialog(object: FileChooserCallback() { +// override fun onCancel(reason: String) { +// } +// +// override fun onResult(uri: Uri) { +// val file = UriUtils.uri2File(uri) +// Snackbar.make(view, "文件大小为:${file.length()}", Snackbar.LENGTH_LONG) +// .show() +// } +// }) +// } + } + + override fun onPermissionsGranted() { + } + + override fun onPermissionsDenied() { + } + +// override fun onCreateOptionsMenu(menu: Menu): Boolean { +// // Inflate the menu; this adds items to the action bar if it is present. +// menuInflater.inflate(R.menu.menu_main, menu) +// return true +// } + +// override fun onOptionsItemSelected(item: MenuItem): Boolean { +// // Handle action bar item clicks here. The action bar will +// // automatically handle clicks on the Home/Up button, so long +// // as you specify a parent activity in AndroidManifest.xml. +// return when (item.itemId) { +// R.id.action_settings -> true +// else -> super.onOptionsItemSelected(item) +// } +// } +// +// override fun onSupportNavigateUp(): Boolean { +// val navController = findNavController(R.id.nav_host_fragment_content_main) +// return navController.navigateUp(appBarConfiguration) +// || super.onSupportNavigateUp() +// } + + override fun fsafStartActivityForResult(intent: Intent, requestCode: Int) { + startActivityForResult(intent, requestCode) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + fileChooser.onActivityResult(requestCode, resultCode, data) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/PermissionsActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/PermissionsActivity.kt index 94b8ab90..cb8bb99c 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/PermissionsActivity.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/PermissionsActivity.kt @@ -16,9 +16,10 @@ open class PermissionsActivity : BaseActivity() { val permissionList = mutableListOf() if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU) { //文件读写 - permissionList.add(Permission.READ_MEDIA_IMAGES) - permissionList.add(Permission.READ_MEDIA_AUDIO) - permissionList.add(Permission.READ_MEDIA_VIDEO) +// permissionList.add(Permission.READ_MEDIA_IMAGES) +// permissionList.add(Permission.READ_MEDIA_AUDIO) +// permissionList.add(Permission.READ_MEDIA_VIDEO) + permissionList.add(Permission.MANAGE_EXTERNAL_STORAGE) } else { //文件读写 permissionList.add(Permission.WRITE_EXTERNAL_STORAGE) diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt index f4fc2611..061fe8dc 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginActivity.kt @@ -7,15 +7,19 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.navinfo.omqs.R import com.navinfo.omqs.databinding.ActivityLoginBinding import com.navinfo.omqs.ui.activity.PermissionsActivity import com.navinfo.omqs.ui.activity.map.MainActivity +import dagger.hilt.android.AndroidEntryPoint /** * 登陆页面 */ + +@AndroidEntryPoint class LoginActivity : PermissionsActivity() { private lateinit var binding: ActivityLoginBinding @@ -30,41 +34,52 @@ class LoginActivity : PermissionsActivity() { initView() } - private fun initView() { - //登录校验,初始化成功 - viewModel.loginStatus.observe(this) { - when (it) { - LoginStatus.LOGIN_STATUS_NET_LOADING -> { - loginDialog("验证用户信息...") - } - LoginStatus.LOGIN_STATUS_FOLDER_INIT -> { - loginDialog("检查本地数据...") - } - LoginStatus.LOGIN_STATUS_FOLDER_FAILURE -> { - Toast.makeText(this, "文件夹初始化失败", Toast.LENGTH_SHORT).show() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_NET_FAILURE -> { - Toast.makeText(this, "网络访问失败", Toast.LENGTH_SHORT).show() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_SUCCESS -> { - val intent = Intent(this@LoginActivity, MainActivity::class.java) - startActivity(intent) + /** + * 观察登录状态,把Observer提出来是为了防止每次数据变化都会有新的observer创建 + * 还有为了方便释放(需不需要手动释放?不清楚)不需要释放,当viewmodel观察到activity/fragment 的生命周期时会自动释放 + * PS:不要在 observer 中修改 LiveData 的值的数据,会影响其他 observer + */ + private val loginObserve = Observer { + when (it) { + LoginStatus.LOGIN_STATUS_NET_LOADING -> { + loginDialog("验证用户信息...") + } + LoginStatus.LOGIN_STATUS_FOLDER_INIT -> { + loginDialog("检查本地数据...") + } + LoginStatus.LOGIN_STATUS_FOLDER_FAILURE -> { + Toast.makeText(this, "文件夹初始化失败", Toast.LENGTH_SHORT).show() + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_NET_FAILURE -> { + Toast.makeText(this, "网络访问失败", Toast.LENGTH_SHORT).show() + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_SUCCESS -> { + val intent = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intent) // finish() - loginDialog?.dismiss() - loginDialog = null - } - LoginStatus.LOGIN_STATUS_CANCEL -> { - loginDialog?.dismiss() - loginDialog = null - } + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_CANCEL -> { + loginDialog?.dismiss() + loginDialog = null + } + LoginStatus.LOGIN_STATUS_NET_OFFLINE_MAP -> { + loginDialog("检查离线地图...") } } } + private fun initView() { + //登录校验,初始化成功 + viewModel.loginStatus.observe(this, loginObserve) + + } + /** * 登录dialog */ @@ -73,7 +88,7 @@ class LoginActivity : PermissionsActivity() { loginDialog = MaterialAlertDialogBuilder( this, com.google.android.material.R.style.MaterialAlertDialog_Material3 ).setTitle("登录").setMessage(message).show() - loginDialog!!.setCanceledOnTouchOutside(true) + loginDialog!!.setCanceledOnTouchOutside(false) loginDialog!!.setOnCancelListener { viewModel.cancelLogin() } @@ -84,13 +99,17 @@ class LoginActivity : PermissionsActivity() { //进应用根本不调用,待查 override fun onPermissionsGranted() { - Log.e("jingo","调用了吗") + Log.e("jingo", "调用了吗") } override fun onPermissionsDenied() { } + override fun onDestroy() { + super.onDestroy() + } + /** * 处理登录按钮 */ diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt index 2d6f29b6..76496b0d 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/login/LoginViewModel.kt @@ -7,11 +7,15 @@ import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.navinfo.omqs.Constant import com.navinfo.omqs.bean.LoginUserBean +import com.navinfo.omqs.http.NetResult +import com.navinfo.omqs.http.NetworkService +import com.navinfo.omqs.tools.FileManager +import com.navinfo.omqs.tools.RealmCoroutineScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import okio.IOException -import java.io.File +import javax.inject.Inject enum class LoginStatus { /** @@ -19,6 +23,11 @@ enum class LoginStatus { */ LOGIN_STATUS_NET_LOADING, + /** + * 访问离线地图列表 + */ + LOGIN_STATUS_NET_OFFLINE_MAP, + /** * 初始化文件夹 */ @@ -45,7 +54,10 @@ enum class LoginStatus { LOGIN_STATUS_CANCEL, } -class LoginViewModel( +@HiltViewModel +class LoginViewModel @Inject constructor( + private val networkService: NetworkService, + private val realmManager: RealmCoroutineScope ) : ViewModel() { //用户信息 val loginUser: MutableLiveData = MutableLiveData() @@ -65,7 +77,7 @@ class LoginViewModel( */ fun onClick(view: View) { loginUser.value!!.username = "admin2" - loginUser.postValue(loginUser.value) + loginUser.value = loginUser.value } /** @@ -81,7 +93,6 @@ class LoginViewModel( //不指定IO,会在主线程里运行 jobLogin = viewModelScope.launch(Dispatchers.IO) { loginCheck(context, userName, password) - Log.e("jingo", "运行完了1?${Thread.currentThread().name}") } } @@ -90,52 +101,68 @@ class LoginViewModel( */ private suspend fun loginCheck(context: Context, userName: String, password: String) { - Log.e("jingo", "我在哪个线程里?${Thread.currentThread().name}") //上面调用了线程切换,这里不用调用,即使调用了还是在同一个线程中,除非自定义协程域?(待验证) // withContext(Dispatchers.IO) { - Log.e("jingo", "delay之前?${Thread.currentThread().name}") //网络访问 loginStatus.postValue(LoginStatus.LOGIN_STATUS_NET_LOADING) - //假装网络访问,等待3秒 - delay(3000) + //假装网络访问,等待2秒 + delay(1000) //文件夹初始化 try { loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_INIT) - createRootFolder(context) + createUserFolder(context) + // 初始化Realm } catch (e: IOException) { loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_FAILURE) } + //假装解压文件等 delay(1000) - loginStatus.postValue(LoginStatus.LOGIN_STATUS_SUCCESS) - Log.e("jingo", "delay之后?${Thread.currentThread().name}") + loginStatus.postValue(LoginStatus.LOGIN_STATUS_NET_OFFLINE_MAP) + when (val result = networkService.getOfflineMapCityList()) { + is NetResult.Success -> { -// } + if (result.data != null) { + for (cityBean in result.data) { + FileManager.checkOfflineMapFileInfo(cityBean) + } + realmManager.launch { + realmManager.insertOrUpdate(result.data) + } + } + } + is NetResult.Error -> { + withContext(Dispatchers.Main) { + Toast.makeText(context, "${result.exception.message}", Toast.LENGTH_SHORT) + .show() + } + } + is NetResult.Failure -> { + withContext(Dispatchers.Main) { + Toast.makeText(context, "${result.code}:${result.msg}", Toast.LENGTH_SHORT) + .show() + } + } + NetResult.Loading -> {} + } + loginStatus.postValue(LoginStatus.LOGIN_STATUS_SUCCESS) } - + /** + * 创建用户目录 + */ @Throws(IOException::class) - private fun createRootFolder(context: Context) { - // 在SD卡创建项目目录 - val sdCardPath = context.getExternalFilesDir(null) - sdCardPath?.let { - Constant.ROOT_PATH = sdCardPath.absolutePath - Constant.MAP_PATH = Constant.ROOT_PATH + "/map/" - val file = File(Constant.MAP_PATH) - if (!file.exists()) { - file.mkdirs() - } - } + private fun createUserFolder(context: Context) { + // 在SD卡创建用户目录,解压资源等 } /** * 取消登录 */ fun cancelLogin() { - Log.e("jingo", "取消了?${Thread.currentThread().name}") jobLogin?.let { it.cancel() - loginStatus.postValue(LoginStatus.LOGIN_STATUS_CANCEL) + loginStatus.value = LoginStatus.LOGIN_STATUS_CANCEL } } @@ -143,6 +170,4 @@ class LoginViewModel( super.onCleared() cancelLogin() } - - } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt index cb03179e..4126123b 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainActivity.kt @@ -11,6 +11,7 @@ import com.navinfo.collect.library.map.NIMapController import com.navinfo.omqs.Constant import com.navinfo.omqs.R import com.navinfo.omqs.databinding.ActivityMainBinding +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import com.navinfo.omqs.ui.activity.BaseActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -27,6 +28,8 @@ class MainActivity : BaseActivity() { //注入地图控制器 @Inject lateinit var mapController: NIMapController + @Inject + lateinit var offlineMapDownloadManager: OfflineMapDownloadManager override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -66,7 +69,6 @@ class MainActivity : BaseActivity() { super.onDestroy() mapController.mMapView.onDestroy() mapController.locationLayerHandler.stopLocation() - Log.e("jingo", "MainActivity 销毁") } override fun onResume() { diff --git a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainViewModel.kt index 722baf7a..c052a0f7 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainViewModel.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/activity/map/MainViewModel.kt @@ -25,7 +25,6 @@ class MainViewModel @Inject constructor( } override fun onCleared() { - Log.e("jingo","MainViewModel 被释放了") super.onCleared() } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt index 4c1e6786..f3c863a4 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapAdapter.kt @@ -3,6 +3,7 @@ package com.navinfo.omqs.ui.fragment.offlinemap import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter +import dagger.hilt.EntryPoint /** * 离线地图主页面,viewpage适配器 diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt index 8f94464c..d27594bd 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListAdapter.kt @@ -1,33 +1,134 @@ package com.navinfo.omqs.ui.fragment.offlinemap -import androidx.databinding.ViewDataBinding +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import com.navinfo.collect.library.data.entity.OfflineMapCityBean import com.navinfo.omqs.R -import com.navinfo.omqs.BR -import com.navinfo.omqs.bean.OfflineMapCityBean import com.navinfo.omqs.databinding.AdapterOfflineMapCityBinding +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import com.navinfo.omqs.ui.other.BaseRecyclerViewAdapter import com.navinfo.omqs.ui.other.BaseViewHolder import javax.inject.Inject /** * 离线地图城市列表 RecyclerView 适配器 + * + * 在 RecycleView 的 ViewHolder 中监听 ViewModel 的 LiveData,然后此时传递的 lifecycleOwner 是对应的 Fragment。由于 ViewHolder 的生命周期是比 Fragment 短的,所以当 ViewHolder 销毁时,由于 Fragment 的 Lifecycle 还没有结束,此时 ViewHolder 会发生内存泄露(监听的 LiveData 没有解绑) + * 这种场景下有两种解决办法: + *使用 LiveData 的 observeForever 然后在 ViewHolder 销毁前手动调用 removeObserver + *使用 LifecycleRegistry 给 ViewHolder 分发生命周期(这里使用了这个) */ +class OfflineMapCityListAdapter @Inject constructor( + private val downloadManager: OfflineMapDownloadManager, private val context: Context +) : BaseRecyclerViewAdapter() { -class OfflineMapCityListAdapter @Inject constructor() : - BaseRecyclerViewAdapter() { - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - var binding: ViewDataBinding = holder.dataBinding - //立刻刷新UI,解决闪烁 -// binding.executePendingBindings() - binding.setVariable(BR.cityBean, data[position]) - (binding as AdapterOfflineMapCityBinding).offlineMapDownloadBtn.setOnClickListener { + private val downloadBtnClick = View.OnClickListener() { + if (it.tag != null) { + val cityBean = data[it.tag as Int] + when (cityBean.status) { + OfflineMapCityBean.NONE, OfflineMapCityBean.UPDATE, OfflineMapCityBean.PAUSE, OfflineMapCityBean.ERROR -> { + Log.e("jingo", "开始下载 ${cityBean.status}") + downloadManager.start(cityBean.id) + } + OfflineMapCityBean.LOADING, OfflineMapCityBean.WAITING -> { + Log.e("jingo", "暂停 ${cityBean.status}") + downloadManager.pause(cityBean.id) + } + else -> { + Log.e("jingo", "暂停 ${cityBean.status}") + } + } } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + val viewBinding = + AdapterOfflineMapCityBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BaseViewHolder(viewBinding) + } + + override fun onViewRecycled(holder: BaseViewHolder) { + super.onViewRecycled(holder) + //页面滑动时会用holder重构页面,但是对进度条的监听回调会一直返回,扰乱UI,所以当当前holder去重构的时候,移除监听 + downloadManager.removeObserver(holder.tag) + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + val binding: AdapterOfflineMapCityBinding = + holder.viewBinding as AdapterOfflineMapCityBinding + //牺牲性能立刻刷新UI,解决闪烁 这里不用 +// binding.executePendingBindings() + val cityBean = data[position] + //tag 方便onclick里拿到数据 + holder.tag = cityBean.id + changeViews(binding, cityBean) + downloadManager.addTask(cityBean) + downloadManager.observer(cityBean.id, holder, DownloadObserver(cityBean.id, binding)) + binding.offlineMapDownloadBtn.tag = position + binding.offlineMapDownloadBtn.setOnClickListener(downloadBtnClick) + binding.offlineMapCityName.text = cityBean.name + binding.offlineMapCitySize.text = cityBean.getFileSizeText() + } + + inner class DownloadObserver(val id: String, val binding: AdapterOfflineMapCityBinding) : + Observer { + override fun onChanged(t: OfflineMapCityBean?) { + if (id == t?.id) + changeViews(binding, t) + } + } + + + private fun changeViews(binding: AdapterOfflineMapCityBinding, cityBean: OfflineMapCityBean) { + binding.offlineMapProgress.progress = + (cityBean.currentSize * 100 / cityBean.fileSize).toInt() + when (cityBean.status) { + OfflineMapCityBean.NONE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "下载" + } + OfflineMapCityBean.WAITING -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "等待中" + } + OfflineMapCityBean.LOADING -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "暂停" + } + OfflineMapCityBean.PAUSE -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "继续" + } + OfflineMapCityBean.ERROR -> { + if (binding.offlineMapProgress.visibility != View.VISIBLE) binding.offlineMapProgress.visibility = + View.VISIBLE + binding.offlineMapDownloadBtn.text = "重试" + } + OfflineMapCityBean.DONE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "已完成" + } + OfflineMapCityBean.UPDATE -> { + if (binding.offlineMapProgress.visibility == View.VISIBLE) binding.offlineMapProgress.visibility = + View.INVISIBLE + binding.offlineMapDownloadBtn.text = "更新" + } + } } override fun getItemViewType(position: Int): Int { return R.layout.adapter_offline_map_city } +} + -} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt index 23b74da5..5d64468d 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListFragment.kt @@ -10,17 +10,28 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.navinfo.omqs.databinding.FragmentOfflineMapCityListBinding +import com.navinfo.omqs.http.RetrofitNetworkServiceAPI +import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** * 离线地图城市列表 */ @AndroidEntryPoint class OfflineMapCityListFragment : Fragment() { + @Inject + lateinit var downloadManager: OfflineMapDownloadManager private var _binding: FragmentOfflineMapCityListBinding? = null private val viewModel by viewModels() private val binding get() = _binding!! - private val adapter: OfflineMapCityListAdapter by lazy { OfflineMapCityListAdapter() } + private val adapter: OfflineMapCityListAdapter by lazy { + OfflineMapCityListAdapter( + downloadManager, + requireContext() + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -33,8 +44,10 @@ class OfflineMapCityListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val layoutManager = LinearLayoutManager(context) - _binding!!.offlineMapCityListRecyclerview.layoutManager = layoutManager - _binding!!.offlineMapCityListRecyclerview.adapter = adapter + //// 设置 RecyclerView 的固定大小,避免在滚动时重新计算视图大小和布局,提高性能 + binding.offlineMapCityListRecyclerview.setHasFixedSize(true) + binding.offlineMapCityListRecyclerview.layoutManager = layoutManager + binding.offlineMapCityListRecyclerview.adapter = adapter viewModel.cityListLiveData.observe(viewLifecycleOwner) { adapter.refreshData(it) } @@ -44,6 +57,5 @@ class OfflineMapCityListFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() _binding = null - Log.e("jingo","OfflineMapCityListFragment onDestroyView") } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt index 4eba17ae..1dbd04f0 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapCityListViewModel.kt @@ -1,16 +1,18 @@ package com.navinfo.omqs.ui.fragment.offlinemap -import android.app.Application import android.content.Context -import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.navinfo.omqs.http.NetResult -import com.navinfo.omqs.http.NetworkService -import com.navinfo.omqs.bean.OfflineMapCityBean +import com.navinfo.collect.library.data.entity.OfflineMapCityBean +import com.navinfo.omqs.tools.FileManager +import com.navinfo.omqs.tools.RealmCoroutineScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.where +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,8 +21,7 @@ import javax.inject.Inject */ @HiltViewModel class OfflineMapCityListViewModel @Inject constructor( - private val networkService: NetworkService, - @ApplicationContext val context: Context + @ApplicationContext val context: Context, ) : ViewModel() { val cityListLiveData = MutableLiveData>() @@ -29,21 +30,15 @@ class OfflineMapCityListViewModel @Inject constructor( * 去获取离线地图列表 */ fun getCityList() { - viewModelScope.launch { - when (val result = networkService.getOfflineMapCityList()) { - is NetResult.Success -> { - cityListLiveData.postValue(result.data!!) - } - is NetResult.Error -> { - Toast.makeText(context, "${result.exception.message}", Toast.LENGTH_SHORT) - .show() - } - is NetResult.Failure -> { - Toast.makeText(context, "${result.code}:${result.msg}", Toast.LENGTH_SHORT) - .show() - } - NetResult.Loading -> {} + viewModelScope.launch(Dispatchers.IO) { + val realm = Realm.getDefaultInstance() + val objects = realm.where().findAll().sort("id", Sort.ASCENDING) + val list = realm.copyFromRealm(objects) + realm.close() + for (item in list) { + FileManager.checkOfflineMapFileInfo(item) } + cityListLiveData.postValue(list) } } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapFragment.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapFragment.kt index ee60b969..8f9da1e1 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapFragment.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapFragment.kt @@ -61,6 +61,5 @@ class OfflineMapFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() _binding = null - Log.e("jingo","OfflineMapFragment onDestroyView") } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapStateListFragment.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapStateListFragment.kt index 59285dcf..b4ae37f7 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapStateListFragment.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapStateListFragment.kt @@ -33,6 +33,5 @@ class OfflineMapStateListFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() _binding = null - Log.e("jingo","OfflineMapStateListFragment onDestroyView") } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt new file mode 100644 index 00000000..02a03583 --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/offlinemap/OfflineMapViewHolder.kt @@ -0,0 +1,10 @@ +package com.navinfo.omqs.ui.fragment.offlinemap + +import com.navinfo.omqs.databinding.AdapterOfflineMapCityBinding +import com.navinfo.omqs.ui.other.BaseViewHolder + +class OfflineMapViewHolder(dataBinding: AdapterOfflineMapCityBinding) : BaseViewHolder(dataBinding) { + init{ + dataBinding.offlineMapDownloadBtn + } +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterFragment.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterFragment.kt index b9b9992d..e0d21450 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterFragment.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterFragment.kt @@ -1,22 +1,30 @@ package com.navinfo.omqs.ui.fragment.personalcenter +import android.content.Intent +import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.blankj.utilcode.util.UriUtils +import com.github.k1rakishou.fsaf.FileChooser +import com.github.k1rakishou.fsaf.callback.FSAFActivityCallbacks +import com.github.k1rakishou.fsaf.callback.FileChooserCallback import com.navinfo.omqs.R import com.navinfo.omqs.databinding.FragmentPersonalCenterBinding /** * 个人中心 */ -class PersonalCenterFragment : Fragment() { +class PersonalCenterFragment : Fragment(), FSAFActivityCallbacks { private var _binding: FragmentPersonalCenterBinding? = null private val binding get() = _binding!! + private val fileChooser by lazy { FileChooser(requireContext()) } + private val viewModel by lazy { viewModels().value } override fun onCreateView( @@ -25,23 +33,45 @@ class PersonalCenterFragment : Fragment() { ): View { _binding = FragmentPersonalCenterBinding.inflate(inflater, container, false) return binding.root - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Log.e("jingo", "NIMapController PersonalCenterFragment onViewCreated") binding.root.setNavigationItemSelectedListener { when (it.itemId) { R.id.personal_center_menu_offline_map -> findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + R.id.personal_center_menu_import_data -> { + // 用户选中导入数据,打开文件选择器,用户选择导入的数据文件目录 + fileChooser.openChooseFileDialog(object: FileChooserCallback() { + override fun onCancel(reason: String) { + } + + override fun onResult(uri: Uri) { + val file = UriUtils.uri2File(uri) + // 开始导入数据 + viewModel.importOmdbData(file) + } + }) + } } true } + + fileChooser.setCallbacks(this@PersonalCenterFragment) } override fun onDestroyView() { super.onDestroyView() _binding = null } + + override fun fsafStartActivityForResult(intent: Intent, requestCode: Int) { + startActivityForResult(intent, requestCode) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + fileChooser.onActivityResult(requestCode, resultCode, data) + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterViewModel.kt b/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterViewModel.kt new file mode 100644 index 00000000..a0217e7d --- /dev/null +++ b/app/src/main/java/com/navinfo/omqs/ui/fragment/personalcenter/PersonalCenterViewModel.kt @@ -0,0 +1,16 @@ +package com.navinfo.omqs.ui.fragment.personalcenter + +import androidx.lifecycle.ViewModel +import java.io.File + +class PersonalCenterViewModel: ViewModel() { + fun importOmdbData(omdbFile: File) { + // 检查File是否为sqlite数据库 + if (omdbFile == null || omdbFile.exists()) { + throw Exception("文件不存在") + } + if (!omdbFile.name.endsWith(".sqlite") and !omdbFile.name.endsWith("db")) { + throw Exception("文件不存在") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt b/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt index 33747c57..fa964626 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/other/BaseRecyclerViewAdapter.kt @@ -1,6 +1,7 @@ package com.navinfo.omqs.ui.other import android.view.LayoutInflater +import android.view.View.OnClickListener import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView @@ -10,23 +11,51 @@ import androidx.recyclerview.widget.RecyclerView */ abstract class BaseRecyclerViewAdapter(var data: List = listOf()) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return BaseViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - viewType, - parent, - false - ) - ) - } + // private var recyclerView: RecyclerView? = null +// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { +// +// +// +// return BaseViewHolder( +// DataBindingUtil.inflate( +// LayoutInflater.from(parent.context), +// viewType, +// parent, +// false +// ) +// ) +// } + override fun getItemCount(): Int { return data.size } - fun refreshData(newData:List){ + fun refreshData(newData: List) { this.data = newData this.notifyDataSetChanged() } + + + override fun onViewAttachedToWindow(holder: BaseViewHolder) { + super.onViewAttachedToWindow(holder) + holder.onStart() + } + + override fun onViewDetachedFromWindow(holder: BaseViewHolder) { + super.onViewDetachedFromWindow(holder) + holder.apply { + onStop() + } + } +// +// override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { +// super.onAttachedToRecyclerView(recyclerView) +// this.recyclerView = recyclerView +// } +// +// override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { +// super.onDetachedFromRecyclerView(recyclerView) +// this.recyclerView = null +// } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt b/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt index a06b61ca..2544a7c0 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/other/BaseViewHolder.kt @@ -1,11 +1,55 @@ package com.navinfo.omqs.ui.other -import androidx.databinding.ViewDataBinding +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding /** * dataBinding viewHolder 基类 + * LifecycleRegistry 这是一个生命周期注册器,继承自 Lifecycle,LifecycleOwner 通过这个类来分发生命周期事件,并在 getLifecycle() 中返回 */ -open class BaseViewHolder(var dataBinding: ViewDataBinding) : - RecyclerView.ViewHolder(dataBinding.root) { +open class BaseViewHolder(val viewBinding: ViewBinding) : + RecyclerView.ViewHolder(viewBinding.root), LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + var tag = "" + + init { +// dataBinding.lifecycleOwner = this + lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED + lifecycleRegistry.currentState = Lifecycle.State.CREATED + itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + // View onDetached 的时候回调 onDestroy() + override fun onViewDetachedFromWindow(v: View) { + itemView.removeOnAttachStateChangeListener(this) + onDestroy() + } + + // View onAttached 的时候回调 onCreate() + override fun onViewAttachedToWindow(v: View) { + onStart() + } + }) + } + + fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED // + lifecycleRegistry.currentState = Lifecycle.State.RESUMED // ON_RESUME EVENT + } + + fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED // + lifecycleRegistry.currentState = Lifecycle.State.CREATED // ON_STOP EVENT + } + + fun onDestroy() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED /// ON_DESTROY EVENT + } + + + override fun getLifecycle(): Lifecycle { + return lifecycleRegistry + } } \ No newline at end of file diff --git a/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt b/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt index 0f1180a6..b4a9fd7f 100644 --- a/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt +++ b/app/src/main/java/com/navinfo/omqs/ui/widget/MyProgressBar.kt @@ -5,10 +5,13 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect -import android.opengl.ETC1.getHeight -import android.opengl.ETC1.getWidth import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout import android.widget.ProgressBar +import com.navinfo.omqs.R /** @@ -18,8 +21,13 @@ class MyProgressBar : ProgressBar { private lateinit var mPaint: Paint private var text: String = "" private var rate = 0f + private lateinit var bar: ProgressBar constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { +// LayoutInflater.from(context).inflate( +// R.layout.my_projressbar, this, +// true +// ); initView() } @@ -33,6 +41,7 @@ class MyProgressBar : ProgressBar { mPaint.color = Color.BLUE } + @Synchronized override fun setProgress(progress: Int) { setText(progress) @@ -40,7 +49,7 @@ class MyProgressBar : ProgressBar { } private fun setText(progress: Int) { - rate = progress * 1.0f / this.getMax() + rate = progress * 1.0f / this.max val i = (rate * 100).toInt() text = "$i%" } @@ -53,12 +62,14 @@ class MyProgressBar : ProgressBar { // int x = (getWidth()/2) - rect.centerX(); // int y = (getHeight()/2) - rect.centerY(); var x = (width * rate).toInt() - if (x == width) { + val dx = width - rect.right + if (x > dx) { // 如果为百分之百则在左边绘制。 - x = width - rect.right + x = dx } - val y: Int = 0 - rect.top - mPaint.textSize = 22f + mPaint.textSize = 24f + val y: Int = 10 - rect.top + canvas.drawText(text, x.toFloat(), y.toFloat(), mPaint) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_import_export_24.xml b/app/src/main/res/drawable/ic_baseline_import_export_24.xml new file mode 100644 index 00000000..0c6ecd72 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_import_export_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/progress_bg.xml b/app/src/main/res/drawable/progress_bg.xml new file mode 100644 index 00000000..244083fc --- /dev/null +++ b/app/src/main/res/drawable/progress_bg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index d1405c56..1d88e9f5 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -17,8 +17,7 @@ + android:layout_height="match_parent"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_offline_map_city.xml b/app/src/main/res/layout/adapter_offline_map_city.xml index 3bad8ddd..3f9f5700 100644 --- a/app/src/main/res/layout/adapter_offline_map_city.xml +++ b/app/src/main/res/layout/adapter_offline_map_city.xml @@ -1,82 +1,67 @@ - - - - - - - - - + android:text="省市名称" + android:textColor="@color/white" + android:textSize="@dimen/default_font_size" /> - + - - + - + - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/menu/personal_center_menu.xml b/app/src/main/res/menu/personal_center_menu.xml index e66e3705..4e3502df 100644 --- a/app/src/main/res/menu/personal_center_menu.xml +++ b/app/src/main/res/menu/personal_center_menu.xml @@ -11,9 +11,9 @@ android:icon="@drawable/baseline_map_24" android:title="离线地图" /> + android:id="@+id/personal_center_menu_import_data" + android:icon="@drawable/ic_baseline_import_export_24" + android:title="导入数据" />