Merge branch 'master' into 'master'

Master

See merge request qiji4215/OneMapQS!1
This commit is contained in:
齐济04215
2023-04-06 10:49:38 +08:00
46 changed files with 1275 additions and 301 deletions

View File

@@ -3,8 +3,8 @@ plugins {
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'com.google.dagger.hilt.android' id 'com.google.dagger.hilt.android'
id 'realm-android'
} }
android { android {
namespace 'com.navinfo.omqs' namespace 'com.navinfo.omqs'
compileSdk 33 compileSdk 33
@@ -26,8 +26,8 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
@@ -50,8 +50,12 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//权限管理 //权限管理 https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:16.5' 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 //hilt
implementation "com.google.dagger:hilt-android:2.44" implementation "com.google.dagger:hilt-android:2.44"

View File

@@ -26,6 +26,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 读取缓存数据 --> <!-- 读取缓存数据 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!--android:largeHeap="true" 大内存 128M -->
<application <application
android:name=".OMQSApplication" android:name=".OMQSApplication"
android:allowBackup="true" android:allowBackup="true"
@@ -33,10 +34,10 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/Theme.OMQualityInspection" android:theme="@style/Theme.OMQualityInspection"
tools:targetApi="31"> android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".ui.activity.login.LoginActivity" android:name=".ui.activity.login.LoginActivity"
android:exported="true" android:exported="true"
@@ -56,6 +57,7 @@
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:theme="@style/Theme.OMQualityInspection" /> android:theme="@style/Theme.OMQualityInspection" />
<meta-data android:name="ScopedStorage" android:value="true" />
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,95 @@
# coding:utf-8
# 合并指定目录下的omdbsqlite数据
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数据的文件夹")

View File

@@ -1,13 +1,29 @@
package com.navinfo.omqs package com.navinfo.omqs
import io.realm.Realm
class Constant { class Constant {
companion object { companion object {
/** /**
* sd卡根目录 * sd卡根目录
*/ */
lateinit var ROOT_PATH: String lateinit var ROOT_PATH: String
/**
* 地图目录
*/
lateinit var MAP_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_version_right_off = "1" //立即发送
const val MESSAGE_PAGE_SIZE = 30 //消息列表一页最多数量 const val MESSAGE_PAGE_SIZE = 30 //消息列表一页最多数量
lateinit var realm: Realm
} }
} }

View File

@@ -1,8 +1,18 @@
package com.navinfo.omqs package com.navinfo.omqs
import android.app.Application import android.app.Application
import android.util.Log
import com.navinfo.omqs.tools.FileManager
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import io.realm.Realm
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
@HiltAndroidApp @HiltAndroidApp
class OMQSApplication : Application() { class OMQSApplication : Application() {
override fun onCreate() {
FileManager.initRootDir(this)
super.onCreate()
}
} }

View File

@@ -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)
}
}

View File

@@ -0,0 +1,8 @@
package com.navinfo.omqs.data.process
/**
* 数据处理引擎,数据导入、导出及转换处理
* */
class DataEngine {
}

View File

@@ -6,11 +6,13 @@ import com.google.gson.Gson
import com.navinfo.omqs.Constant import com.navinfo.omqs.Constant
import com.navinfo.omqs.OMQSApplication import com.navinfo.omqs.OMQSApplication
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.tools.RealmCoroutineScope
import dagger.Lazy import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
@@ -25,11 +27,11 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class GlobalModule { class GlobalModule {
// @Singleton @Singleton
// @Provides @Provides
// fun provideApplication(application: Application): OMQSApplication { fun provideApplication(application: Application): OMQSApplication {
// return application as OMQSApplication return application as OMQSApplication
// } }
/** /**
* 注入 网络OKHttp 对象 * 注入 网络OKHttp 对象
@@ -54,7 +56,8 @@ class GlobalModule {
} }
}.apply { }.apply {
level = if (Constant.DEBUG) { level = if (Constant.DEBUG) {
HttpLoggingInterceptor.Level.BODY //坑 下载文件时打印log 内存不足
HttpLoggingInterceptor.Level.HEADERS
} else { } else {
HttpLoggingInterceptor.Level.NONE HttpLoggingInterceptor.Level.NONE
} }
@@ -91,4 +94,13 @@ class GlobalModule {
fun provideNetworkService(retrofit: Retrofit): RetrofitNetworkServiceAPI { fun provideNetworkService(retrofit: Retrofit): RetrofitNetworkServiceAPI {
return retrofit.create(RetrofitNetworkServiceAPI::class.java) return retrofit.create(RetrofitNetworkServiceAPI::class.java)
} }
/**
* realm 注册
*/
@Provides
@Singleton
fun provideRealmService(context: Application): RealmCoroutineScope {
return RealmCoroutineScope(context)
}
} }

View File

@@ -1,13 +1,13 @@
package com.navinfo.omqs.hilt package com.navinfo.omqs.hilt
import android.content.Context
import com.navinfo.collect.library.map.NIMapController import com.navinfo.collect.library.map.NIMapController
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager
import com.navinfo.omqs.tools.RealmCoroutineScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityRetainedScoped import dagger.hilt.android.scopes.ActivityRetainedScoped
@InstallIn(ActivityRetainedComponent::class) @InstallIn(ActivityRetainedComponent::class)
@@ -26,8 +26,11 @@ class MainActivityModule {
*/ */
@ActivityRetainedScoped @ActivityRetainedScoped
@Provides @Provides
fun providesOfflineMapDownloadManager(@ActivityContext context: Context): OfflineMapDownloadManager = fun providesOfflineMapDownloadManager(
OfflineMapDownloadManager(context) networkServiceAPI: RetrofitNetworkServiceAPI,
realmManager: RealmCoroutineScope
): OfflineMapDownloadManager =
OfflineMapDownloadManager(networkServiceAPI, realmManager)
/** /**
* 实验失败这样创建viewmodel不会在activity销毁的时候同时销毁 * 实验失败这样创建viewmodel不会在activity销毁的时候同时销毁
@@ -35,7 +38,6 @@ class MainActivityModule {
// @ActivityRetainedScoped // @ActivityRetainedScoped
// @Provides // @Provides
// fun providesMainViewModel(mapController: NIMapController): MainViewModel { // fun providesMainViewModel(mapController: NIMapController): MainViewModel {
// Log.e("jingo", "MainViewModel 被创建")
// return MainViewModel(mapController) // return MainViewModel(mapController)
// } // }

View File

@@ -1,6 +1,7 @@
package com.navinfo.omqs.http package com.navinfo.omqs.http
import com.navinfo.omqs.bean.OfflineMapCityBean import com.navinfo.collect.library.data.entity.OfflineMapCityBean
/** /**
* 网络访问 业务接口 * 网络访问 业务接口

View File

@@ -1,6 +1,6 @@
package com.navinfo.omqs.http 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject

View File

@@ -1,13 +1,12 @@
package com.navinfo.omqs.http package com.navinfo.omqs.http
import androidx.lifecycle.LiveData import com.navinfo.collect.library.data.entity.OfflineMapCityBean
import com.navinfo.omqs.bean.OfflineMapCityBean
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Streaming import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
import java.util.concurrent.Flow
/** /**
* retrofit2 网络请求接口 * retrofit2 网络请求接口
@@ -47,7 +46,7 @@ interface RetrofitNetworkServiceAPI {
*/ */
@Streaming @Streaming
@GET @GET
suspend fun retrofitDownLoadFile(@Url url: String):Response<ResponseBody> suspend fun retrofitDownLoadFile(@Header("RANGE") start: String? = "0", @Url url: String):Response<ResponseBody>
/** /**

View File

@@ -1,48 +1,114 @@
package com.navinfo.omqs.http.offlinemapdownload package com.navinfo.omqs.http.offlinemapdownload
import android.content.Context import androidx.lifecycle.LifecycleOwner
import android.os.Environment import androidx.lifecycle.Observer
import android.text.TextUtils import com.navinfo.collect.library.data.entity.OfflineMapCityBean
import com.navinfo.omqs.Constant import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.bean.OfflineMapCityBean import com.navinfo.omqs.tools.RealmCoroutineScope
import dagger.hilt.android.qualifiers.ActivityContext
import java.io.Serializable
import java.util.concurrent.ConcurrentHashMap 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<String, OfflineMapDownloadScope> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { private val scopeMap: ConcurrentHashMap<String, OfflineMapDownloadScope> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
ConcurrentHashMap<String, OfflineMapDownloadScope>() ConcurrentHashMap<String, OfflineMapDownloadScope>()
} }
val downloadFolder: String? by lazy { /**
Constant.MAP_PATH + "/offline/" * 存储正在下载的城市队列
*/
private val taskScopeMap: ConcurrentHashMap<String, OfflineMapDownloadScope> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
ConcurrentHashMap<String, OfflineMapDownloadScope>()
} }
/** /**
* 启动下载任务
* 请不要直接使用此方法启动下载任务,它是交由[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]的唯一途径,请不要通过其他方式创建[OfflineMapDownloadScope] * 这是创建[OfflineMapDownloadScope]的唯一途径,请不要通过其他方式创建[OfflineMapDownloadScope]
*/ */
fun request(cityBean: OfflineMapCityBean): OfflineMapDownloadScope? { fun start(id: String) {
//没有下载连接的不能下载 scopeMap[id]?.start()
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 addTask(cityBean: OfflineMapCityBean) {
if (!scopeMap.containsKey(cityBean.id)) {
scopeMap[cityBean.id] = OfflineMapDownloadScope(this, cityBean)
} }
}
fun observer(
id: String, lifecycleOwner: LifecycleOwner, observer: Observer<OfflineMapCityBean>
) {
if (scopeMap.containsKey(id)) {
scopeMap[id]!!.observer(lifecycleOwner, observer)
}
}
fun removeObserver(id: String) {
if (scopeMap.containsKey(id)) {
scopeMap[id]!!.removeObserver()
}
}
} }

View File

@@ -1,26 +1,176 @@
package com.navinfo.omqs.http.offlinemapdownload package com.navinfo.omqs.http.offlinemapdownload
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.navinfo.omqs.bean.OfflineMapCityBean import androidx.lifecycle.Observer
import kotlinx.coroutines.CoroutineScope import com.navinfo.collect.library.data.entity.OfflineMapCityBean
import kotlinx.coroutines.Job import com.navinfo.omqs.Constant
import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
/** /**
* 代表一个下载任务 * 代表一个下载任务
* [OfflineMapCityBean.id]将做为下载任务的唯一标识 * [OfflineMapCityBean.id]将做为下载任务的唯一标识
* 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.download]获取此对象 * 不要直接在外部直接创建此对象,那样就可能无法统一管理下载任务,请通过[OfflineMapDownloadManager.request]获取此对象
* 这是一个协程作用域, * 这是一个协程作用域,
* EmptyCoroutineContext 表示一个不包含任何元素的协程上下文,它通常用于创建新的协程上下文,或者作为协程上下文的基础。 * 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 downloadJob: Job? = null
/** /**
* * 管理观察者,同时只有一个就行了
*/
// private var observer: Observer<OfflineMapCityBean>? = null
private var lifecycleOwner: LifecycleOwner? = null
/**
*通知UI更新
*/ */
private val downloadData = MutableLiveData<OfflineMapCityBean>() private val downloadData = MutableLiveData<OfflineMapCityBean>()
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<OfflineMapCityBean>) {
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
}
}
} }

View File

@@ -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
}
}
}
}
}
}

View File

@@ -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<OfflineMapCityBean> {
var list: List<OfflineMapCityBean> = mutableListOf()
realm.executeTransaction {
val objects = realm.where<OfflineMapCityBean>().findAll().sort("id", Sort.ASCENDING)
list = realm.copyFromRealm(objects)
}
return list
}
suspend fun insertOrUpdate(objects: Collection<RealmModel?>?) {
realm.executeTransaction {
realm.insertOrUpdate(objects)
}
}
suspend fun insertOrUpdate(realmModel: RealmModel?) {
realm.executeTransaction {
realm.insertOrUpdate(realmModel)
}
}
}

View File

@@ -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)
}
}

View File

@@ -16,9 +16,10 @@ open class PermissionsActivity : BaseActivity() {
val permissionList = mutableListOf<String>() val permissionList = mutableListOf<String>()
if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU) { if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
//文件读写 //文件读写
permissionList.add(Permission.READ_MEDIA_IMAGES) // permissionList.add(Permission.READ_MEDIA_IMAGES)
permissionList.add(Permission.READ_MEDIA_AUDIO) // permissionList.add(Permission.READ_MEDIA_AUDIO)
permissionList.add(Permission.READ_MEDIA_VIDEO) // permissionList.add(Permission.READ_MEDIA_VIDEO)
permissionList.add(Permission.MANAGE_EXTERNAL_STORAGE)
} else { } else {
//文件读写 //文件读写
permissionList.add(Permission.WRITE_EXTERNAL_STORAGE) permissionList.add(Permission.WRITE_EXTERNAL_STORAGE)

View File

@@ -7,15 +7,19 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.navinfo.omqs.R import com.navinfo.omqs.R
import com.navinfo.omqs.databinding.ActivityLoginBinding import com.navinfo.omqs.databinding.ActivityLoginBinding
import com.navinfo.omqs.ui.activity.PermissionsActivity import com.navinfo.omqs.ui.activity.PermissionsActivity
import com.navinfo.omqs.ui.activity.map.MainActivity import com.navinfo.omqs.ui.activity.map.MainActivity
import dagger.hilt.android.AndroidEntryPoint
/** /**
* 登陆页面 * 登陆页面
*/ */
@AndroidEntryPoint
class LoginActivity : PermissionsActivity() { class LoginActivity : PermissionsActivity() {
private lateinit var binding: ActivityLoginBinding private lateinit var binding: ActivityLoginBinding
@@ -30,9 +34,12 @@ class LoginActivity : PermissionsActivity() {
initView() initView()
} }
private fun initView() { /**
//登录校验,初始化成功 * 观察登录状态把Observer提出来是为了防止每次数据变化都会有新的observer创建
viewModel.loginStatus.observe(this) { * 还有为了方便释放(需不需要手动释放?不清楚)不需要释放,当viewmodel观察到activity/fragment 的生命周期时会自动释放
* PS:不要在 observer 中修改 LiveData 的值的数据,会影响其他 observer
*/
private val loginObserve = Observer<LoginStatus> {
when (it) { when (it) {
LoginStatus.LOGIN_STATUS_NET_LOADING -> { LoginStatus.LOGIN_STATUS_NET_LOADING -> {
loginDialog("验证用户信息...") loginDialog("验证用户信息...")
@@ -61,10 +68,18 @@ class LoginActivity : PermissionsActivity() {
loginDialog?.dismiss() loginDialog?.dismiss()
loginDialog = null loginDialog = null
} }
LoginStatus.LOGIN_STATUS_NET_OFFLINE_MAP -> {
loginDialog("检查离线地图...")
} }
} }
} }
private fun initView() {
//登录校验,初始化成功
viewModel.loginStatus.observe(this, loginObserve)
}
/** /**
* 登录dialog * 登录dialog
*/ */
@@ -73,7 +88,7 @@ class LoginActivity : PermissionsActivity() {
loginDialog = MaterialAlertDialogBuilder( loginDialog = MaterialAlertDialogBuilder(
this, com.google.android.material.R.style.MaterialAlertDialog_Material3 this, com.google.android.material.R.style.MaterialAlertDialog_Material3
).setTitle("登录").setMessage(message).show() ).setTitle("登录").setMessage(message).show()
loginDialog!!.setCanceledOnTouchOutside(true) loginDialog!!.setCanceledOnTouchOutside(false)
loginDialog!!.setOnCancelListener { loginDialog!!.setOnCancelListener {
viewModel.cancelLogin() viewModel.cancelLogin()
} }
@@ -84,13 +99,17 @@ class LoginActivity : PermissionsActivity() {
//进应用根本不调用,待查 //进应用根本不调用,待查
override fun onPermissionsGranted() { override fun onPermissionsGranted() {
Log.e("jingo","调用了吗") Log.e("jingo", "调用了吗")
} }
override fun onPermissionsDenied() { override fun onPermissionsDenied() {
} }
override fun onDestroy() {
super.onDestroy()
}
/** /**
* 处理登录按钮 * 处理登录按钮
*/ */

View File

@@ -7,11 +7,15 @@ import android.widget.Toast
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.navinfo.omqs.Constant
import com.navinfo.omqs.bean.LoginUserBean 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 kotlinx.coroutines.*
import okio.IOException import okio.IOException
import java.io.File import javax.inject.Inject
enum class LoginStatus { enum class LoginStatus {
/** /**
@@ -19,6 +23,11 @@ enum class LoginStatus {
*/ */
LOGIN_STATUS_NET_LOADING, LOGIN_STATUS_NET_LOADING,
/**
* 访问离线地图列表
*/
LOGIN_STATUS_NET_OFFLINE_MAP,
/** /**
* 初始化文件夹 * 初始化文件夹
*/ */
@@ -45,7 +54,10 @@ enum class LoginStatus {
LOGIN_STATUS_CANCEL, LOGIN_STATUS_CANCEL,
} }
class LoginViewModel( @HiltViewModel
class LoginViewModel @Inject constructor(
private val networkService: NetworkService,
private val realmManager: RealmCoroutineScope
) : ViewModel() { ) : ViewModel() {
//用户信息 //用户信息
val loginUser: MutableLiveData<LoginUserBean> = MutableLiveData() val loginUser: MutableLiveData<LoginUserBean> = MutableLiveData()
@@ -65,7 +77,7 @@ class LoginViewModel(
*/ */
fun onClick(view: View) { fun onClick(view: View) {
loginUser.value!!.username = "admin2" loginUser.value!!.username = "admin2"
loginUser.postValue(loginUser.value) loginUser.value = loginUser.value
} }
/** /**
@@ -81,7 +93,6 @@ class LoginViewModel(
//不指定IO会在主线程里运行 //不指定IO会在主线程里运行
jobLogin = viewModelScope.launch(Dispatchers.IO) { jobLogin = viewModelScope.launch(Dispatchers.IO) {
loginCheck(context, userName, password) 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) { private suspend fun loginCheck(context: Context, userName: String, password: String) {
Log.e("jingo", "我在哪个线程里?${Thread.currentThread().name}")
//上面调用了线程切换,这里不用调用,即使调用了还是在同一个线程中,除非自定义协程域?(待验证) //上面调用了线程切换,这里不用调用,即使调用了还是在同一个线程中,除非自定义协程域?(待验证)
// withContext(Dispatchers.IO) { // withContext(Dispatchers.IO) {
Log.e("jingo", "delay之前${Thread.currentThread().name}")
//网络访问 //网络访问
loginStatus.postValue(LoginStatus.LOGIN_STATUS_NET_LOADING) loginStatus.postValue(LoginStatus.LOGIN_STATUS_NET_LOADING)
//假装网络访问,等待3 //假装网络访问,等待2
delay(3000) delay(1000)
//文件夹初始化 //文件夹初始化
try { try {
loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_INIT) loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_INIT)
createRootFolder(context) createUserFolder(context)
// 初始化Realm
} catch (e: IOException) { } catch (e: IOException) {
loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_FAILURE) loginStatus.postValue(LoginStatus.LOGIN_STATUS_FOLDER_FAILURE)
} }
//假装解压文件等 //假装解压文件等
delay(1000) delay(1000)
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) loginStatus.postValue(LoginStatus.LOGIN_STATUS_SUCCESS)
Log.e("jingo", "delay之后${Thread.currentThread().name}")
// }
} }
/**
* 创建用户目录
*/
@Throws(IOException::class) @Throws(IOException::class)
private fun createRootFolder(context: Context) { private fun createUserFolder(context: Context) {
// 在SD卡创建项目目录 // 在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()
}
}
} }
/** /**
* 取消登录 * 取消登录
*/ */
fun cancelLogin() { fun cancelLogin() {
Log.e("jingo", "取消了?${Thread.currentThread().name}")
jobLogin?.let { jobLogin?.let {
it.cancel() it.cancel()
loginStatus.postValue(LoginStatus.LOGIN_STATUS_CANCEL) loginStatus.value = LoginStatus.LOGIN_STATUS_CANCEL
} }
} }
@@ -143,6 +170,4 @@ class LoginViewModel(
super.onCleared() super.onCleared()
cancelLogin() cancelLogin()
} }
} }

View File

@@ -11,6 +11,7 @@ import com.navinfo.collect.library.map.NIMapController
import com.navinfo.omqs.Constant import com.navinfo.omqs.Constant
import com.navinfo.omqs.R import com.navinfo.omqs.R
import com.navinfo.omqs.databinding.ActivityMainBinding import com.navinfo.omqs.databinding.ActivityMainBinding
import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager
import com.navinfo.omqs.ui.activity.BaseActivity import com.navinfo.omqs.ui.activity.BaseActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -27,6 +28,8 @@ class MainActivity : BaseActivity() {
//注入地图控制器 //注入地图控制器
@Inject @Inject
lateinit var mapController: NIMapController lateinit var mapController: NIMapController
@Inject
lateinit var offlineMapDownloadManager: OfflineMapDownloadManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -66,7 +69,6 @@ class MainActivity : BaseActivity() {
super.onDestroy() super.onDestroy()
mapController.mMapView.onDestroy() mapController.mMapView.onDestroy()
mapController.locationLayerHandler.stopLocation() mapController.locationLayerHandler.stopLocation()
Log.e("jingo", "MainActivity 销毁")
} }
override fun onResume() { override fun onResume() {

View File

@@ -25,7 +25,6 @@ class MainViewModel @Inject constructor(
} }
override fun onCleared() { override fun onCleared() {
Log.e("jingo","MainViewModel 被释放了")
super.onCleared() super.onCleared()
} }
} }

View File

@@ -3,6 +3,7 @@ package com.navinfo.omqs.ui.fragment.offlinemap
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import dagger.hilt.EntryPoint
/** /**
* 离线地图主页面viewpage适配器 * 离线地图主页面viewpage适配器

View File

@@ -1,33 +1,134 @@
package com.navinfo.omqs.ui.fragment.offlinemap 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.R
import com.navinfo.omqs.BR
import com.navinfo.omqs.bean.OfflineMapCityBean
import com.navinfo.omqs.databinding.AdapterOfflineMapCityBinding 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.BaseRecyclerViewAdapter
import com.navinfo.omqs.ui.other.BaseViewHolder import com.navinfo.omqs.ui.other.BaseViewHolder
import javax.inject.Inject import javax.inject.Inject
/** /**
* 离线地图城市列表 RecyclerView 适配器 * 离线地图城市列表 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<OfflineMapCityBean>() {
class OfflineMapCityListAdapter @Inject constructor() :
BaseRecyclerViewAdapter<OfflineMapCityBean>() {
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<OfflineMapCityBean> {
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 { override fun getItemViewType(position: Int): Int {
return R.layout.adapter_offline_map_city return R.layout.adapter_offline_map_city
} }
} }

View File

@@ -10,17 +10,28 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.navinfo.omqs.databinding.FragmentOfflineMapCityListBinding 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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/** /**
* 离线地图城市列表 * 离线地图城市列表
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class OfflineMapCityListFragment : Fragment() { class OfflineMapCityListFragment : Fragment() {
@Inject
lateinit var downloadManager: OfflineMapDownloadManager
private var _binding: FragmentOfflineMapCityListBinding? = null private var _binding: FragmentOfflineMapCityListBinding? = null
private val viewModel by viewModels<OfflineMapCityListViewModel>() private val viewModel by viewModels<OfflineMapCityListViewModel>()
private val binding get() = _binding!! private val binding get() = _binding!!
private val adapter: OfflineMapCityListAdapter by lazy { OfflineMapCityListAdapter() } private val adapter: OfflineMapCityListAdapter by lazy {
OfflineMapCityListAdapter(
downloadManager,
requireContext()
)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@@ -33,8 +44,10 @@ class OfflineMapCityListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(context)
_binding!!.offlineMapCityListRecyclerview.layoutManager = layoutManager //// 设置 RecyclerView 的固定大小,避免在滚动时重新计算视图大小和布局,提高性能
_binding!!.offlineMapCityListRecyclerview.adapter = adapter binding.offlineMapCityListRecyclerview.setHasFixedSize(true)
binding.offlineMapCityListRecyclerview.layoutManager = layoutManager
binding.offlineMapCityListRecyclerview.adapter = adapter
viewModel.cityListLiveData.observe(viewLifecycleOwner) { viewModel.cityListLiveData.observe(viewLifecycleOwner) {
adapter.refreshData(it) adapter.refreshData(it)
} }
@@ -44,6 +57,5 @@ class OfflineMapCityListFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
Log.e("jingo","OfflineMapCityListFragment onDestroyView")
} }
} }

View File

@@ -1,16 +1,18 @@
package com.navinfo.omqs.ui.fragment.offlinemap package com.navinfo.omqs.ui.fragment.offlinemap
import android.app.Application
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.navinfo.omqs.http.NetResult import com.navinfo.collect.library.data.entity.OfflineMapCityBean
import com.navinfo.omqs.http.NetworkService import com.navinfo.omqs.tools.FileManager
import com.navinfo.omqs.bean.OfflineMapCityBean import com.navinfo.omqs.tools.RealmCoroutineScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext 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 kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -19,8 +21,7 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class OfflineMapCityListViewModel @Inject constructor( class OfflineMapCityListViewModel @Inject constructor(
private val networkService: NetworkService, @ApplicationContext val context: Context,
@ApplicationContext val context: Context
) : ViewModel() { ) : ViewModel() {
val cityListLiveData = MutableLiveData<List<OfflineMapCityBean>>() val cityListLiveData = MutableLiveData<List<OfflineMapCityBean>>()
@@ -29,21 +30,15 @@ class OfflineMapCityListViewModel @Inject constructor(
* 去获取离线地图列表 * 去获取离线地图列表
*/ */
fun getCityList() { fun getCityList() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
when (val result = networkService.getOfflineMapCityList()) { val realm = Realm.getDefaultInstance()
is NetResult.Success -> { val objects = realm.where<OfflineMapCityBean>().findAll().sort("id", Sort.ASCENDING)
cityListLiveData.postValue(result.data!!) val list = realm.copyFromRealm(objects)
} realm.close()
is NetResult.Error -> { for (item in list) {
Toast.makeText(context, "${result.exception.message}", Toast.LENGTH_SHORT) FileManager.checkOfflineMapFileInfo(item)
.show()
}
is NetResult.Failure -> {
Toast.makeText(context, "${result.code}:${result.msg}", Toast.LENGTH_SHORT)
.show()
}
NetResult.Loading -> {}
} }
cityListLiveData.postValue(list)
} }
} }
} }

View File

@@ -61,6 +61,5 @@ class OfflineMapFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
Log.e("jingo","OfflineMapFragment onDestroyView")
} }
} }

View File

@@ -33,6 +33,5 @@ class OfflineMapStateListFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
Log.e("jingo","OfflineMapStateListFragment onDestroyView")
} }
} }

View File

@@ -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
}
}

View File

@@ -1,22 +1,30 @@
package com.navinfo.omqs.ui.fragment.personalcenter package com.navinfo.omqs.ui.fragment.personalcenter
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController 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.R
import com.navinfo.omqs.databinding.FragmentPersonalCenterBinding import com.navinfo.omqs.databinding.FragmentPersonalCenterBinding
/** /**
* 个人中心 * 个人中心
*/ */
class PersonalCenterFragment : Fragment() { class PersonalCenterFragment : Fragment(), FSAFActivityCallbacks {
private var _binding: FragmentPersonalCenterBinding? = null private var _binding: FragmentPersonalCenterBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val fileChooser by lazy { FileChooser(requireContext()) }
private val viewModel by lazy { viewModels<PersonalCenterViewModel>().value }
override fun onCreateView( override fun onCreateView(
@@ -25,23 +33,45 @@ class PersonalCenterFragment : Fragment() {
): View { ): View {
_binding = FragmentPersonalCenterBinding.inflate(inflater, container, false) _binding = FragmentPersonalCenterBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Log.e("jingo", "NIMapController PersonalCenterFragment onViewCreated")
binding.root.setNavigationItemSelectedListener { binding.root.setNavigationItemSelectedListener {
when (it.itemId) { when (it.itemId) {
R.id.personal_center_menu_offline_map -> R.id.personal_center_menu_offline_map ->
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) 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 true
} }
fileChooser.setCallbacks(this@PersonalCenterFragment)
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _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)
}
} }

View File

@@ -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("文件不存在")
}
}
}

View File

@@ -1,6 +1,7 @@
package com.navinfo.omqs.ui.other package com.navinfo.omqs.ui.other
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.OnClickListener
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -10,23 +11,51 @@ import androidx.recyclerview.widget.RecyclerView
*/ */
abstract class BaseRecyclerViewAdapter<T>(var data: List<T> = listOf()) : abstract class BaseRecyclerViewAdapter<T>(var data: List<T> = listOf()) :
RecyclerView.Adapter<BaseViewHolder>() { RecyclerView.Adapter<BaseViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { // private var recyclerView: RecyclerView? = null
return BaseViewHolder( // override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
DataBindingUtil.inflate( //
LayoutInflater.from(parent.context), //
viewType, //
parent, // return BaseViewHolder(
false // DataBindingUtil.inflate(
) // LayoutInflater.from(parent.context),
) // viewType,
} // parent,
// false
// )
// )
// }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return data.size return data.size
} }
fun refreshData(newData:List<T>){ fun refreshData(newData: List<T>) {
this.data = newData this.data = newData
this.notifyDataSetChanged() 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
// }
} }

View File

@@ -1,11 +1,55 @@
package com.navinfo.omqs.ui.other 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.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
/** /**
* dataBinding viewHolder 基类 * dataBinding viewHolder 基类
* LifecycleRegistry 这是一个生命周期注册器,继承自 LifecycleLifecycleOwner 通过这个类来分发生命周期事件,并在 getLifecycle() 中返回
*/ */
open class BaseViewHolder(var dataBinding: ViewDataBinding) : open class BaseViewHolder(val viewBinding: ViewBinding) :
RecyclerView.ViewHolder(dataBinding.root) { 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
}
} }

View File

@@ -5,10 +5,13 @@ import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.opengl.ETC1.getHeight
import android.opengl.ETC1.getWidth
import android.util.AttributeSet 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 android.widget.ProgressBar
import com.navinfo.omqs.R
/** /**
@@ -18,8 +21,13 @@ class MyProgressBar : ProgressBar {
private lateinit var mPaint: Paint private lateinit var mPaint: Paint
private var text: String = "" private var text: String = ""
private var rate = 0f private var rate = 0f
private lateinit var bar: ProgressBar
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
// LayoutInflater.from(context).inflate(
// R.layout.my_projressbar, this,
// true
// );
initView() initView()
} }
@@ -33,6 +41,7 @@ class MyProgressBar : ProgressBar {
mPaint.color = Color.BLUE mPaint.color = Color.BLUE
} }
@Synchronized @Synchronized
override fun setProgress(progress: Int) { override fun setProgress(progress: Int) {
setText(progress) setText(progress)
@@ -40,7 +49,7 @@ class MyProgressBar : ProgressBar {
} }
private fun setText(progress: Int) { private fun setText(progress: Int) {
rate = progress * 1.0f / this.getMax() rate = progress * 1.0f / this.max
val i = (rate * 100).toInt() val i = (rate * 100).toInt()
text = "$i%" text = "$i%"
} }
@@ -53,12 +62,14 @@ class MyProgressBar : ProgressBar {
// int x = (getWidth()/2) - rect.centerX(); // int x = (getWidth()/2) - rect.centerX();
// int y = (getHeight()/2) - rect.centerY(); // int y = (getHeight()/2) - rect.centerY();
var x = (width * rate).toInt() 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 = 24f
mPaint.textSize = 22f val y: Int = 10 - rect.top
canvas.drawText(text, x.toFloat(), y.toFloat(), mPaint) canvas.drawText(text, x.toFloat(), y.toFloat(), mPaint)
} }
} }

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#747D8C"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
</vector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--设置ProgressBar背景色-->
<item android:id="@android:id/background">
<shape>
<!--设置ProgressBar进度条圆角半径-->
<corners android:radius="4dp" />
<solid android:color="@color/cv_gray_153" />
</shape>
</item>
<!--设置ProgressBar进度条颜色-->
<item android:id="@android:id/progress">
<clip android:clipOrientation="horizontal">
<shape>
<corners android:radius="4dp" />
<solid android:color="@color/default_blue" />
</shape>
</clip>
</item>
</layer-list>

View File

@@ -17,8 +17,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<ImageView <ImageView
android:id="@+id/login_fragment_logo" android:id="@+id/login_fragment_logo"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.navinfo.collect.library.map.NIMapView
android:id="@+id/main_activity_map1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,82 +1,67 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.navinfo.omqs.ui.fragment.offlinemap.OfflineMapCityListAdapter">
<data>
<import type="com.navinfo.omqs.R" />
<variable
name="cityBean"
type="com.navinfo.omqs.bean.OfflineMapCityBean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="10dp" android:background="@color/cv_bg_color"
android:background="@color/cv_bg_color"> android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="5dp"
tools:context="com.navinfo.omqs.ui.fragment.offlinemap.OfflineMapCityListAdapter">
<TextView <TextView
android:id="@+id/offline_map_city_name" android:id="@+id/offline_map_city_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{cityBean.name}" android:text="省市名称"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="@dimen/default_font_size" android:textSize="@dimen/default_font_size" />
app:layout_constraintLeft_toLeftOf="@id/offline_map_city_size"
app:layout_constraintRight_toRightOf="@id/offline_map_city_size"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/offline_map_city_size" android:id="@+id/offline_map_city_size"
style="@style/map_size_font_style" style="@style/map_size_font_style"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/offline_map_city_name"
android:drawableLeft="@mipmap/point_blue" android:drawableLeft="@mipmap/point_blue"
android:textSize="@dimen/card_title_font_3size" android:layout_marginTop="5dp"
android:text="@{cityBean.getFileSizeText()}" android:text="文件大小"
app:layout_constraintLeft_toLeftOf="parent" android:textSize="@dimen/card_title_font_3size" />
app:layout_constraintTop_toBottomOf="@id/offline_map_city_name" />
<TextView
android:id="@+id/offline_map_download_btn"
style="@style/map_download_style_btn"
android:layout_width="60dp"
android:layout_alignTop="@id/offline_map_city_name"
android:layout_alignBottom="@id/offline_map_city_size"
android:layout_alignParentRight="true"
android:shadowColor="@android:color/transparent"
android:text="下载"
android:textColor="@color/btn_blue_solid"
android:textSize="@dimen/card_title_font_2size" />
<TextView <TextView
android:id="@+id/tv_city_list_status" android:id="@+id/tv_city_list_status"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginBottom="10dp"
android:layout_toLeftOf="@id/offline_map_download_btn"
android:clickable="true" android:clickable="true"
android:focusable="false" android:focusable="false"
android:shadowColor="@android:color/transparent" android:shadowColor="@android:color/transparent"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="@dimen/card_title_font_2size" android:textSize="@dimen/card_title_font_2size" />
app:layout_constraintBottom_toBottomOf="@id/offline_map_download_btn"
app:layout_constraintRight_toLeftOf="@id/offline_map_download_btn"
app:layout_constraintTop_toTopOf="@id/offline_map_download_btn" />
<TextView
android:id="@+id/offline_map_download_btn"
style="@style/map_download_style_btn"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:shadowColor="@android:color/transparent"
android:text="下载"
android:textColor="@color/btn_blue_solid"
android:textSize="@dimen/card_title_font_2size"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/offline_map_progress"
app:layout_constraintTop_toTopOf="parent" />
<com.navinfo.omqs.ui.widget.MyProgressBar <com.navinfo.omqs.ui.widget.MyProgressBar
android:layout_marginTop="5dp"
android:visibility="gone"
android:id="@+id/offline_map_progress" android:id="@+id/offline_map_progress"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="16dp"
android:max="100" android:layout_below="@id/offline_map_download_btn"
app:layout_constraintBottom_toBottomOf="parent" android:progressDrawable="@drawable/progress_bg"
app:layout_constraintLeft_toLeftOf="parent" /> android:paddingTop="10dp"
</androidx.constraintlayout.widget.ConstraintLayout> android:visibility="invisible" />
</layout> </RelativeLayout>

View File

@@ -11,9 +11,9 @@
android:icon="@drawable/baseline_map_24" android:icon="@drawable/baseline_map_24"
android:title="离线地图" /> android:title="离线地图" />
<item <item
android:id="@+id/personal_center_menu_offline_map1" android:id="@+id/personal_center_menu_import_data"
android:icon="@drawable/baseline_person_24" android:icon="@drawable/ic_baseline_import_export_24"
android:title="menu_gallery" /> android:title="导入数据" />
<item <item
android:id="@+id/personal_center_menu_offline_map2" android:id="@+id/personal_center_menu_offline_map2"
android:icon="@drawable/baseline_person_24" android:icon="@drawable/baseline_person_24"

View File

@@ -2,13 +2,12 @@
buildscript { buildscript {
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:10.10.1" classpath "io.realm:realm-gradle-plugin:10.11.1"
} }
} }
plugins { plugins {
id 'com.android.application' version '7.3.1' apply false id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
id 'io.realm.kotlin' version '0.10.0' apply false
id 'com.google.dagger.hilt.android' version '2.44' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false
} }

View File

@@ -0,0 +1,57 @@
package com.navinfo.collect.library.data.entity
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
//enum class StatusEnum(val status: Int) {
// NONE(0), WAITING(1), LOADING(2), PAUSE(3),
// ERROR(4), DONE(5), UPDATE(6)
//}
open class OfflineMapCityBean @JvmOverloads constructor(
@PrimaryKey var id: String = "",
var fileName: String = "",
var name: String = "",
var url: String = "",
var version: Long = 0L,
var fileSize: Long = 0L,
var currentSize: Long = 0L,
var status: Int = NONE
) : RealmObject() {
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 //有新版本要更新
}
// // status的转换对象
// var statusEnum: StatusEnum
// get() {
// return try {
// StatusEnum.values().find { it.status == status }!!
// } catch (e: IllegalArgumentException) {
// StatusEnum.NONE
// }
// }
// set(value) {
// status = value.status
// }
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)
}
}

View File

@@ -0,0 +1,43 @@
package com.navinfo.collect.library.data.entity
import io.realm.RealmModel
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
@RealmClass
open class OfflineMapCityRealmObject: RealmModel {
@PrimaryKey
var id: String = ""
var fileName: String=""
var name: String = ""
var url: String = ""
var version: Long = 0
var fileSize: Long = 0
var currentSize:Long = 0
var status:Int = 0
constructor(){
}
constructor(
id: String,
fileName: String,
name: String,
url: String,
version: Long,
fileSize: Long,
currentSize: Long,
status: Int
) {
this.id = id
this.fileName = fileName
this.name = name
this.url = url
this.version = version
this.fileSize = fileSize
this.currentSize = currentSize
this.status = status
}
}

View File

@@ -91,7 +91,6 @@ DataNiLocationHandler(context: Context, dataBase: MapLifeDataBase) :
) )
mDataBase.niLocationDao.delete(niLocation); mDataBase.niLocationDao.delete(niLocation);
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e("jingo", "删除数据报错 ${e.message}");
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
callback.invoke(false, "${e.message}") callback.invoke(false, "${e.message}")
} }

View File

@@ -37,8 +37,5 @@ class NIMapController {
mapView.setOptions(options) mapView.setOptions(options)
} }
fun print() {
Log.e("jingo", "NIMapController 哈希code ${hashCode()}")
}
} }

View File

@@ -429,7 +429,6 @@ public class GeometryTools {
dList.add(lt + dis); dList.add(lt + dis);
total += dis; total += dis;
} }
Log.e("jingo", "line lengh =" + total);
total = total / 2; total = total / 2;
for (int i = 0; i < dList.size(); i++) { for (int i = 0; i < dList.size(); i++) {
double a = dList.get(i); double a = dList.get(i);
@@ -495,7 +494,6 @@ public class GeometryTools {
dList.add(lt + dis); dList.add(lt + dis);
total += dis; total += dis;
} }
Log.e("jingo", "line lengh =" + total);
total = total / 2; total = total / 2;
for (int i = 0; i < dList.size(); i++) { for (int i = 0; i < dList.size(); i++) {
double a = dList.get(i); double a = dList.get(i);