Jelajahi Sumber

feat 远程更新app基本实现

RobinTan1024 4 tahun lalu
induk
melakukan
4e874f14e8
32 mengubah file dengan 553 tambahan dan 55 penghapusan
  1. 1 0
      .gitignore
  2. 1 0
      .idea/dictionaries/11017950.xml
  3. 1 1
      app/build.gradle
  4. 10 0
      app/src/main/AndroidManifest.xml
  5. 27 6
      app/src/main/java/com/doverfuelingsolutions/issp/api/SystemApi.kt
  6. 11 3
      app/src/main/java/com/doverfuelingsolutions/issp/api/WayneApiConfig.kt
  7. 1 6
      app/src/main/java/com/doverfuelingsolutions/issp/api/basic/LoggingInterceptor.kt
  8. 6 0
      app/src/main/java/com/doverfuelingsolutions/issp/api/dto/ResultVersionInfo.kt
  9. 7 0
      app/src/main/java/com/doverfuelingsolutions/issp/api/dto/ResultVersionInfoItem.kt
  10. 16 0
      app/src/main/java/com/doverfuelingsolutions/issp/api/service/ServiceAssets.kt
  11. 13 0
      app/src/main/java/com/doverfuelingsolutions/issp/receiver/DownloadReceiver.kt
  12. 21 9
      app/src/main/java/com/doverfuelingsolutions/issp/utils/ActivityUtil.kt
  13. 44 0
      app/src/main/java/com/doverfuelingsolutions/issp/utils/AppUtil.kt
  14. 67 0
      app/src/main/java/com/doverfuelingsolutions/issp/utils/download/DownloadAction.kt
  15. 1 0
      app/src/main/java/com/doverfuelingsolutions/issp/utils/sp/SPKeys.kt
  16. 1 0
      app/src/main/java/com/doverfuelingsolutions/issp/utils/sp/SPUtil.kt
  17. 4 12
      app/src/main/java/com/doverfuelingsolutions/issp/view/MainActivity.kt
  18. 172 5
      app/src/main/java/com/doverfuelingsolutions/issp/view/PreferenceActivity.kt
  19. 3 3
      app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentAction.kt
  20. 2 0
      app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentPreference.kt
  21. 13 0
      app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentSelect.kt
  22. 41 6
      app/src/main/res/layout/activity_preference.xml
  23. 5 4
      app/src/main/res/layout/layout_loading.xml
  24. 36 0
      app/src/main/res/layout/layout_version_update.xml
  25. 1 0
      app/src/main/res/values/keys.xml
  26. 16 0
      app/src/main/res/values/strings.xml
  27. 6 0
      app/src/main/res/xml/provider_paths.xml
  28. 6 0
      app/src/main/res/xml/root_preferences.xml
  29. 0 0
      md
  30. 4 0
      project.md
  31. TEMPAT SAMPAH
      signature.jks
  32. 16 0
      version.json

+ 1 - 0
.gitignore

@@ -13,3 +13,4 @@
 .externalNativeBuild
 .cxx
 local.properties
+release

+ 1 - 0
.idea/dictionaries/11017950.xml

@@ -6,6 +6,7 @@
       <w>errcd</w>
       <w>gson</w>
       <w>issp</w>
+      <w>isspt</w>
       <w>noperm</w>
       <w>snackbar</w>
     </words>

+ 1 - 1
app/build.gradle

@@ -14,7 +14,7 @@ android {
         minSdkVersion 25
         targetSdkVersion 30
         versionCode 1
-        versionName "1.0"
+        versionName "1.0.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }

+ 10 - 0
app/src/main/AndroidManifest.xml

@@ -36,6 +36,16 @@
                 android:name="com.doverfuelingsolutions.issp.data.GlobalDataInitializer"
                 android:value="androidx.startup" />
         </provider>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.provider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths" />
+        </provider>
     </application>
 
 </manifest>

+ 27 - 6
app/src/main/java/com/doverfuelingsolutions/issp/api/SystemApi.kt

@@ -6,10 +6,7 @@ import com.doverfuelingsolutions.issp.DFSApplication
 import com.doverfuelingsolutions.issp.R
 import com.doverfuelingsolutions.issp.api.basic.RetrofitUtil
 import com.doverfuelingsolutions.issp.api.dto.*
-import com.doverfuelingsolutions.issp.api.service.ServiceBase
-import com.doverfuelingsolutions.issp.api.service.ServiceConfig
-import com.doverfuelingsolutions.issp.api.service.ServiceLogin
-import com.doverfuelingsolutions.issp.api.service.ServiceTrx
+import com.doverfuelingsolutions.issp.api.service.*
 import com.doverfuelingsolutions.issp.data.GlobalData
 import com.doverfuelingsolutions.issp.utils.StringUtil
 import com.doverfuelingsolutions.issp.utils.thread.ThreadUtil
@@ -32,6 +29,7 @@ object SystemApi {
     private var serviceBase = makeBaseService()
     private var serviceTrx = makeTrxService()
     private var serviceConfig = makeConfigService()
+    private var serviceAssets = makeAssetsService()
 
     /**
      * 登录 + 获取油站信息
@@ -211,9 +209,25 @@ object SystemApi {
         return DFSResult.fail(R.string.req_fail)
     }
 
+    /**
+     * 获取 APP 版本信息
+     */
+    suspend fun remoteVersion() = suspendCoroutine<DFSResult<ResultVersionInfo>> {
+        serviceAssets.remoteVersion().enqueue(object : Callback<ResultVersionInfo> {
+            override fun onResponse(call: Call<ResultVersionInfo>, response: Response<ResultVersionInfo>) {
+                if (response.code() == 200 && response.body() is ResultVersionInfo) {
+                    it.resume(DFSResult.success(response.body() as ResultVersionInfo))
+                } else {
+                    it.resume(DFSResult.fail(R.string.fail_request))
+                }
+            }
 
-
-
+            override fun onFailure(call: Call<ResultVersionInfo>, t: Throwable) {
+                DFSLog.e("SystemApi.remoteVersion.onFailure", t)
+                it.resume(DFSResult.fail(R.string.fail_request))
+            }
+        })
+    }
 
     /**
      * 单纯的登录操作
@@ -374,4 +388,11 @@ object SystemApi {
             .build()
             .create(ServiceConfig::class.java)
     }
+
+    private fun makeAssetsService(): ServiceAssets {
+        return RetrofitUtil.getAuthBuilder()
+            .baseUrl(WayneApiConfig.HOST_ASSETS)
+            .build()
+            .create(ServiceAssets::class.java)
+    }
 }

+ 11 - 3
app/src/main/java/com/doverfuelingsolutions/issp/api/WayneApiConfig.kt

@@ -15,6 +15,7 @@ class WayneApiConfig {
         const val PATH_CONFIG_FILE = "api/Config/file"
         const val PATH_BARCODE = "api/products/barcode/{id}"
         const val PATH_PAY_BY_ID = "api/Transactions/{id}/payment"
+        const val PATH_VERSION_FILE = "isspt/version.json"
 
         const val CONFIG_LOGO = "SiteLogo"
 
@@ -23,9 +24,11 @@ class WayneApiConfig {
         const val PORT_BASE_DEFAULT = "8698"
         const val PORT_TRX_DEFAULT = "8699"
         const val PORT_CONFIG_DEFAULT = "8889"
+        const val PORT_ASSETS_DEFAULT = "8080"
         private const val HOST_BASE_DEFAULT = "$DOMAIN_DEFAULT:$PORT_BASE_DEFAULT"
         private const val HOST_TRX_DEFAULT = "$DOMAIN_DEFAULT:$PORT_TRX_DEFAULT"
         private const val HOST_CONFIG_DEFAULT = "$DOMAIN_DEFAULT:$PORT_CONFIG_DEFAULT"
+        private const val HOST_ASSETS_DEFAULT = "$DOMAIN_DEFAULT:$PORT_ASSETS_DEFAULT"
         // 默认值 - 中控
         const val MIDDLE_IP_DEFAULT = "192.168.1.80"
         const val MIDDLE_PORT_DEFAULT = "4711"
@@ -38,17 +41,22 @@ class WayneApiConfig {
         val HOST_BASE: String get() {
             val domain = SPUtil.getString(SPKeys.SERVER_DOMAIN)
             val port = SPUtil.getString(SPKeys.SERVER_PORT_BASE)
-            return if (domain.isEmpty() && port.isEmpty()) HOST_BASE_DEFAULT else "$domain:$port"
+            return if (domain.isEmpty() || port.isEmpty()) HOST_BASE_DEFAULT else "$domain:$port"
         }
         val HOST_TRX: String get() {
             val domain = SPUtil.getString(SPKeys.SERVER_DOMAIN)
             val port = SPUtil.getString(SPKeys.SERVER_PORT_TRX)
-            return if (domain.isEmpty() && port.isEmpty()) HOST_TRX_DEFAULT else "$domain:$port"
+            return if (domain.isEmpty() || port.isEmpty()) HOST_TRX_DEFAULT else "$domain:$port"
         }
         val HOST_CONFIG: String get() {
             val domain = SPUtil.getString(SPKeys.SERVER_DOMAIN)
             val port = SPUtil.getString(SPKeys.SERVER_PORT_CONFIG)
-            return if (domain.isEmpty() && port.isEmpty()) HOST_CONFIG_DEFAULT else "$domain:$port"
+            return if (domain.isEmpty() || port.isEmpty()) HOST_CONFIG_DEFAULT else "$domain:$port"
+        }
+        val HOST_ASSETS: String get() {
+            val domain = SPUtil.getString(SPKeys.SERVER_DOMAIN)
+            val port = SPUtil.getString(SPKeys.SERVER_PORT_ASSETS)
+            return if (domain.isEmpty() || port.isEmpty()) HOST_ASSETS_DEFAULT else "$domain:$port"
         }
     }
 }

+ 1 - 6
app/src/main/java/com/doverfuelingsolutions/issp/api/basic/LoggingInterceptor.kt

@@ -22,12 +22,7 @@ class LoggingInterceptor : Interceptor {
         DFSLog.v(
             "=====>>>>> request[$requestName] done: ${System.currentTimeMillis() - time}ms",
             "code = ${response.code()}",
-            "body = ${
-                stringBody.replace(
-                    "\n",
-                    ""
-                ).replace(" ", "")
-            }"
+            "body = $stringBody"
         )
         return response
     }

+ 6 - 0
app/src/main/java/com/doverfuelingsolutions/issp/api/dto/ResultVersionInfo.kt

@@ -0,0 +1,6 @@
+package com.doverfuelingsolutions.issp.api.dto
+
+class ResultVersionInfo(
+    val name: String,
+    val versionList: List<ResultVersionInfoItem>
+)

+ 7 - 0
app/src/main/java/com/doverfuelingsolutions/issp/api/dto/ResultVersionInfoItem.kt

@@ -0,0 +1,7 @@
+package com.doverfuelingsolutions.issp.api.dto
+
+class ResultVersionInfoItem(
+    val name: String,
+    val version: String,
+    val info: String,
+)

+ 16 - 0
app/src/main/java/com/doverfuelingsolutions/issp/api/service/ServiceAssets.kt

@@ -0,0 +1,16 @@
+package com.doverfuelingsolutions.issp.api.service
+
+import com.doverfuelingsolutions.issp.api.WayneApiConfig
+import com.doverfuelingsolutions.issp.api.dto.ResultBarcode
+import com.doverfuelingsolutions.issp.api.dto.ResultDeviceSessionInfo
+import com.doverfuelingsolutions.issp.api.dto.ResultVersionInfo
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.http.*
+
+interface ServiceAssets {
+
+    @Headers("requestName: remoteVersion", "Accept: application/json")
+    @GET(WayneApiConfig.PATH_VERSION_FILE)
+    fun remoteVersion(): Call<ResultVersionInfo>
+}

+ 13 - 0
app/src/main/java/com/doverfuelingsolutions/issp/receiver/DownloadReceiver.kt

@@ -0,0 +1,13 @@
+package com.doverfuelingsolutions.issp.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+class DownloadReceiver : BroadcastReceiver() {
+
+    override fun onReceive(context: Context?, intent: Intent?) {
+
+
+    }
+}

+ 21 - 9
app/src/main/java/com/doverfuelingsolutions/issp/utils/ActivityUtil.kt

@@ -1,7 +1,10 @@
 package com.doverfuelingsolutions.issp.utils
 
+import android.app.Activity
 import android.view.View
+import android.view.Window
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.HandlerCompat
 import androidx.lifecycle.lifecycleScope
 import com.doverfuelingsolutions.issp.utils.log.DFSLog
 import kotlinx.coroutines.delay
@@ -11,22 +14,31 @@ class ActivityUtil {
     companion object {
 
         @Suppress("DEPRECATION")
-        fun setFullscreen(activity: AppCompatActivity) {
-            makeFullscreen(activity)
+        fun setFullscreen(activity: Activity) {
+            makeFullscreen(activity.window)
 
             activity.window.decorView.setOnSystemUiVisibilityChangeListener {
-                makeFullscreen(activity)
+                makeFullscreen(activity.window)
             }
         }
 
         @Suppress("DEPRECATION")
-        private fun makeFullscreen(activity: AppCompatActivity) {
-            val systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-            activity.lifecycleScope.launchWhenStarted {
-                activity.window.decorView.systemUiVisibility = systemUiVisibility
-                delay(1000)
-                activity.window.decorView.systemUiVisibility = systemUiVisibility
+        fun setFullscreen(window: Window) {
+            makeFullscreen(window)
+
+            window.decorView.setOnSystemUiVisibilityChangeListener {
+                makeFullscreen(window)
             }
         }
+
+        @Suppress("DEPRECATION")
+        private fun makeFullscreen(window: Window) {
+            val systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+            window.decorView.systemUiVisibility = systemUiVisibility
+            HandlerCompat.createAsync(window.context.mainLooper).postDelayed({
+                if (window.isActive)
+                    window.decorView.systemUiVisibility = systemUiVisibility
+            }, 1000)
+        }
     }
 }

+ 44 - 0
app/src/main/java/com/doverfuelingsolutions/issp/utils/AppUtil.kt

@@ -0,0 +1,44 @@
+package com.doverfuelingsolutions.issp.utils
+
+import com.doverfuelingsolutions.issp.BuildConfig
+import kotlin.math.pow
+
+class AppUtil {
+
+    companion object {
+
+        /* 版本相关 */
+
+        /**
+         * 获取版本代号
+         */
+        fun getVersionName(): String = BuildConfig.VERSION_NAME
+
+        /**
+         * 获取版本代号
+         */
+        fun getVersionCode(): Int = BuildConfig.VERSION_CODE
+
+        /**
+         * 比较版本大小
+         *
+         * @param version1 版本代码,形如「1.0.0」。「0001.0.0」等同于「1.0.0」
+         * @param version2 形同 version1
+         * @return 正返回值说明1>2,负返回值说明1<2,0说明1=2
+         */
+        fun compareVersion(version1: String, version2: String): Int {
+            return calculateVersionNum(version1) - calculateVersionNum(version2)
+        }
+        /**
+         * compareVersion 的辅助函数,用于计算的 version 的版本大小值
+         */
+        private fun calculateVersionNum(version: String): Int {
+            return version
+                .split(".")
+                .map { it.toInt() }
+                .reduceIndexed { index, acc, i ->
+                    acc + i * 10.toDouble().pow(index * 2).toInt() * i
+                }
+        }
+    }
+}

+ 67 - 0
app/src/main/java/com/doverfuelingsolutions/issp/utils/download/DownloadAction.kt

@@ -0,0 +1,67 @@
+package com.doverfuelingsolutions.issp.utils.download
+
+import android.app.DownloadManager
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Environment
+import com.doverfuelingsolutions.issp.utils.log.DFSLog
+import com.doverfuelingsolutions.issp.utils.thread.ThreadUtil
+import java.io.File
+
+/**
+ * 下载行为类,直接下载到 Environment.DIRECTORY_DOWNLOADS 中
+ */
+class DownloadAction(
+    val context: Context,
+    uri: Uri,
+    private val fileName: String,
+) {
+
+    private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+    private val request = DownloadManager.Request(uri).apply {
+        DFSLog.d("download target: ${uri.path}, local name: $fileName")
+        setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName)
+        setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
+    }
+    private var downloadId = 0L
+
+    fun start(progressListener: ((status: Int, size: Int, total: Int) -> Unit)) {
+        ThreadUtil.io {
+            // 删除本地重名文件
+            context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.let {
+                val localFile = File("${it.absolutePath}/$fileName")
+                if (localFile.exists()) localFile.delete()
+            }
+
+            // 开始下载
+            downloadId = downloadManager.enqueue(request)
+
+            // 进度
+            val query = DownloadManager.Query().setFilterById(downloadId)
+            var cursor: Cursor? = downloadManager.query(query)
+            while (cursor != null && cursor.moveToFirst()) {
+                cursor.use {
+                    // 已经下载文件大小
+                    val downloaded = it.getInt(it.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
+                    // 下载文件的总大小
+                    val downloadTotal = it.getInt(it.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
+                    // 下载状态
+                    val status = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
+                    DFSLog.d("apk download: status=$status, size: $downloaded, total: $downloadTotal")
+
+                    progressListener.invoke(status, downloaded, downloadTotal)
+                    cursor = when (status) {
+                        DownloadManager.STATUS_SUCCESSFUL, DownloadManager.STATUS_FAILED -> {
+                            null
+                        }
+                        else -> {
+                            Thread.sleep(1000)
+                            downloadManager.query(query)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 0
app/src/main/java/com/doverfuelingsolutions/issp/utils/sp/SPKeys.kt

@@ -25,6 +25,7 @@ class SPKeys {
         val SERVER_PORT_BASE: String = StringUtil.get(R.string.sp_server_port_base)
         val SERVER_PORT_TRX: String = StringUtil.get(R.string.sp_server_port_trx)
         val SERVER_PORT_CONFIG: String = StringUtil.get(R.string.sp_server_port_config)
+        val SERVER_PORT_ASSETS: String = StringUtil.get(R.string.sp_server_port_assets)
         val MIDDLE_IP: String = StringUtil.get(R.string.sp_middle_ip)
         val MIDDLE_PORT: String = StringUtil.get(R.string.sp_middle_port)
         val MIDDLE_WORKSTATION_ID: String = StringUtil.get(R.string.sp_middle_workstation_id)

+ 1 - 0
app/src/main/java/com/doverfuelingsolutions/issp/utils/sp/SPUtil.kt

@@ -75,6 +75,7 @@ object SPUtil {
             putString(SPKeys.SERVER_PORT_BASE, WayneApiConfig.PORT_BASE_DEFAULT)
             putString(SPKeys.SERVER_PORT_TRX, WayneApiConfig.PORT_TRX_DEFAULT)
             putString(SPKeys.SERVER_PORT_CONFIG, WayneApiConfig.PORT_CONFIG_DEFAULT)
+            putString(SPKeys.SERVER_PORT_ASSETS, WayneApiConfig.PORT_ASSETS_DEFAULT)
 
             putString(SPKeys.MIDDLE_IP, WayneApiConfig.MIDDLE_IP_DEFAULT)
             putString(SPKeys.MIDDLE_PORT, WayneApiConfig.MIDDLE_PORT_DEFAULT)

+ 4 - 12
app/src/main/java/com/doverfuelingsolutions/issp/view/MainActivity.kt

@@ -13,6 +13,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.lifecycleScope
 import com.doverfuelingsolutions.issp.R
+import com.doverfuelingsolutions.issp.api.SystemApi
 import com.doverfuelingsolutions.issp.data.GlobalData
 import com.doverfuelingsolutions.issp.databinding.ActivityMainBinding
 import com.doverfuelingsolutions.issp.fusion.FusionError
@@ -21,6 +22,8 @@ import com.doverfuelingsolutions.issp.fusion.callback.OnFusionEvent
 import com.doverfuelingsolutions.issp.utils.ActivityUtil
 import com.doverfuelingsolutions.issp.utils.NetworkUtil
 import com.doverfuelingsolutions.issp.utils.StringUtil
+import com.doverfuelingsolutions.issp.utils.log.DFSLog
+import com.doverfuelingsolutions.issp.utils.thread.ThreadUtil
 import com.doverfuelingsolutions.issp.view.fragment.FragmentAction
 import com.doverfuelingsolutions.issp.view.fragment.FragmentLoading
 import com.doverfuelingsolutions.issp.view.fragment.FragmentSelect
@@ -31,8 +34,7 @@ import kotlinx.coroutines.launch
 import java.io.File
 
 class MainActivity : AppCompatActivity(),
-    OnFusionEvent,
-    View.OnLongClickListener {
+    OnFusionEvent {
 
     companion object {
         fun start(context: Context) {
@@ -99,15 +101,6 @@ class MainActivity : AppCompatActivity(),
         if (!isBlockBackPress) super.onBackPressed()
     }
 
-    override fun onLongClick(v: View?): Boolean {
-        when (v) {
-            binding.clock -> {
-                PreferenceActivity.startForResult(this)
-            }
-        }
-        return false
-    }
-
     override fun onFusionInit(code: FusionError, msg: String) {
         lifecycleScope.launch {
             // WrongAddress Timeout 需要修改中控信息,返回时如已修改则重启Fusion
@@ -172,7 +165,6 @@ class MainActivity : AppCompatActivity(),
                     }
                 }
         }
-        binding.clock.setOnLongClickListener(this)
 
         setFragment(FragmentLoading.build(R.string.in_connect_fusion))
     }

+ 172 - 5
app/src/main/java/com/doverfuelingsolutions/issp/view/PreferenceActivity.kt

@@ -1,19 +1,44 @@
 package com.doverfuelingsolutions.issp.view
 
 import android.app.Activity
+import android.app.DownloadManager
 import android.content.Context
 import android.content.Intent
+import android.net.Uri
+import android.os.Build
 import android.os.Bundle
+import android.os.Environment
 import android.view.MenuItem
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.lifecycleScope
 import com.doverfuelingsolutions.issp.R
-import com.doverfuelingsolutions.issp.api.SystemApi
 import com.doverfuelingsolutions.issp.api.FuelInfoApi
+import com.doverfuelingsolutions.issp.api.SystemApi
+import com.doverfuelingsolutions.issp.api.WayneApiConfig
+import com.doverfuelingsolutions.issp.databinding.ActivityPreferenceBinding
+import com.doverfuelingsolutions.issp.utils.ActivityUtil
+import com.doverfuelingsolutions.issp.utils.AppUtil
+import com.doverfuelingsolutions.issp.utils.DFSToastUtil
+import com.doverfuelingsolutions.issp.utils.StringUtil
+import com.doverfuelingsolutions.issp.utils.download.DownloadAction
+import com.doverfuelingsolutions.issp.utils.log.DFSLog
 import com.doverfuelingsolutions.issp.utils.sp.SPKeys
 import com.doverfuelingsolutions.issp.utils.sp.SPUtil
 import com.doverfuelingsolutions.issp.view.fragment.FragmentPreference
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.launch
+import java.io.File
+import java.util.*
 
-class PreferenceActivity : AppCompatActivity() {
+class PreferenceActivity : AppCompatActivity(),
+    View.OnClickListener {
 
     companion object {
 
@@ -33,13 +58,21 @@ class PreferenceActivity : AppCompatActivity() {
         }
     }
 
+    private val binding: ActivityPreferenceBinding by lazy {
+        DataBindingUtil.setContentView(this, R.layout.activity_preference)
+    }
+    private val viewModel = PreferenceViewModel()
+
     private var isForResult = false
     private var oldMiddleInfo = ""
     private var oldFuelInfo = ""
+    private var oldSystemInfo = ""
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_preference)
+        binding.lifecycleOwner = this
+        binding.viewModel = viewModel
+        binding.handler = this
         supportActionBar?.setDisplayHomeAsUpEnabled(true)
         if (savedInstanceState == null) {
             supportFragmentManager
@@ -53,15 +86,23 @@ class PreferenceActivity : AppCompatActivity() {
             isForResult = true
             oldMiddleInfo = makeMiddleInfo()
             oldFuelInfo = makeFuelInfo()
+            oldSystemInfo = makeSystemInfo()
         } else {
             isForResult = false
         }
     }
 
+    override fun onResume() {
+        super.onResume()
+
+        ActivityUtil.setFullscreen(this)
+    }
+
     override fun onBackPressed() {
         val hasMiddleModified = makeMiddleInfo() != oldMiddleInfo
         val hasFuelModified = makeFuelInfo() != oldFuelInfo
-        if (hasMiddleModified) SystemApi.regenerateAllService()
+        val hasSystemModified = makeSystemInfo() != oldSystemInfo
+        if (hasSystemModified) SystemApi.regenerateAllService()
         if (hasFuelModified) FuelInfoApi.generateService()
         if (isForResult) {
             setResult(Activity.RESULT_OK, Intent().apply {
@@ -81,11 +122,137 @@ class PreferenceActivity : AppCompatActivity() {
         return super.onOptionsItemSelected(item)
     }
 
+    override fun onClick(v: View?) {
+        when (v) {
+            binding.checkUpdate -> {
+                processUpdate()
+            }
+        }
+    }
+
     private fun makeMiddleInfo(): String {
-        return "${SPUtil.getString(SPKeys.MIDDLE_IP)}, ${SPUtil.getString(SPKeys.MIDDLE_PORT)}, ${SPUtil.getString(SPKeys.MIDDLE_WORKSTATION_ID)}"
+        return "${SPUtil.getString(SPKeys.MIDDLE_IP)}, ${SPUtil.getString(SPKeys.MIDDLE_PORT)}, ${SPUtil.getString(
+            SPKeys.MIDDLE_WORKSTATION_ID
+        )}"
     }
 
     private fun makeFuelInfo(): String {
         return "${SPUtil.getString(SPKeys.FUEL_IP)}, ${SPUtil.getString(SPKeys.FUEL_PORT)}"
     }
+
+    private fun makeSystemInfo(): String {
+        return "${SPUtil.getString(SPKeys.SERVER_DOMAIN)}, ${SPUtil.getString(SPKeys.SERVER_PORT_CONFIG)}, ${SPUtil.getString(
+            SPKeys.SERVER_PORT_BASE
+        )}, ${SPUtil.getString(SPKeys.SERVER_PORT_TRX)}, ${SPUtil.getString(SPKeys.SERVER_PORT_ASSETS)}"
+    }
+
+    private fun processUpdate() {
+        lifecycleScope.launch {
+            val currentVersion = AppUtil.getVersionName()
+            DFSLog.i("current version is $currentVersion")
+
+            viewModel.isLookingUpdate.value = true
+            val result = SystemApi.remoteVersion()
+            viewModel.isLookingUpdate.value = false
+
+            if (!result.success || result.data == null) {
+                DFSLog.e("latest version check failed: ${result.message}")
+                DFSToastUtil.fail(StringUtil.get(R.string.fail_behave_reason, StringUtil.get(R.string.check_update), result.message))
+                return@launch
+            }
+
+            if (result.data.versionList.isEmpty()) {
+                DFSLog.e("check version update: no update")
+                DFSToastUtil.fail(R.string.no_update_version)
+                return@launch
+            }
+
+            val remoteVersion = result.data.versionList.last()
+            val compare = AppUtil.compareVersion(remoteVersion.version, currentVersion)
+            if (compare <= 0) {
+                DFSLog.i("latest version check: ${remoteVersion.version}")
+                DFSToastUtil.fail(R.string.current_version_is_latest)
+                return@launch
+            }
+
+            val dialogView = View.inflate(this@PreferenceActivity, R.layout.layout_version_update, null).apply {
+                findViewById<TextView>(R.id.versionTitle).text = StringUtil.get(R.string.new_version, remoteVersion.version)
+                findViewById<TextView>(R.id.versionInfo).text = StringUtil.get(R.string.new_version, remoteVersion.info)
+            }
+            val loadingTip = dialogView.findViewById<TextView>(R.id.loadingTip).apply {
+                text = StringUtil.get(R.string.in_download)
+            }
+            val dialog = MaterialAlertDialogBuilder(this@PreferenceActivity)
+                .setTitle(R.string.find_new_version)
+                .setView(dialogView)
+                .setCancelable(false)
+                .show()
+            dialog.window?.let { ActivityUtil.setFullscreen(it) }
+            val actionButton = dialogView.findViewById<Button>(R.id.close)
+            actionButton.setOnClickListener { dialog.dismiss() }
+
+            // TODO 下载中途断网,下载失败,下载完成但是未成功安装,成功安装后怎么删除
+            // 准备下载
+            val localName = "update/${remoteVersion.name}"
+            val downloadAction = DownloadAction(
+                this@PreferenceActivity,
+                Uri.parse("${WayneApiConfig.HOST_ASSETS}/isspt/${remoteVersion.name}"),
+                localName
+            )
+            downloadAction.start { status, size, total ->
+                lifecycleScope.launch {
+                    when (status) {
+                        // 下载中
+                        DownloadManager.STATUS_RUNNING -> {
+                            val percentage = (size.toDouble() * 10000 / total).toInt().toDouble() / 100
+                            loadingTip.text = StringUtil.get(R.string.download_progress, percentage)
+                        }
+                        // 下载结束
+                        DownloadManager.STATUS_SUCCESSFUL -> {
+                            loadingTip.text = StringUtil.get(R.string.download_done)
+                            actionButton.visibility = View.VISIBLE
+
+                            // 安装应用
+                            getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.let { folder ->
+                                val file = File("${folder.absolutePath}/$localName")
+                                if (!file.isFile) {
+                                    DFSToastUtil.fail(R.string.cant_find_local_file)
+                                    dialog.dismiss()
+                                    return@let
+                                }
+                                // 先删掉其他APK
+                                file.parentFile?.list()?.forEach {
+                                    File(it).let { f ->
+                                        if (f.absolutePath != file.absolutePath && f.isFile && f.name.endsWith("apk")) {
+                                            f.delete()
+                                        }
+                                    }
+                                }
+                                val fileUri = FileProvider.getUriForFile(
+                                    this@PreferenceActivity,
+                                    "$packageName.provider",
+                                    file
+                                )
+                                Intent(Intent.ACTION_VIEW).let { intent ->
+                                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+                                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                                    intent.setDataAndType(fileUri, "application/vnd.android.package-archive")
+                                    this@PreferenceActivity.startActivity(intent)
+                                }
+                            }
+                        }
+                        // 下载失败
+                        DownloadManager.STATUS_FAILED -> {
+                            loadingTip.text = StringUtil.get(R.string.download_fail)
+                            actionButton.visibility = View.VISIBLE
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    class PreferenceViewModel : ViewModel() {
+        val isLookingUpdate = MutableLiveData(false)
+    }
 }

+ 3 - 3
app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentAction.kt

@@ -38,10 +38,10 @@ class FragmentAction private constructor(
             binding = DataBindingUtil.inflate(inflater, R.layout.fragment_action, container, false)
             binding.lifecycleOwner = viewLifecycleOwner
             binding.viewModel = viewModel
-            viewModel.actionText.value = tip
-            viewModel.actionButtonText.value = buttonText
-            binding.actionButton.setOnClickListener { handler.invoke() }
+            binding.handler = this
         }
+        viewModel.actionText.value = tip
+        viewModel.actionButtonText.value = buttonText
         return binding.root
     }
 

+ 2 - 0
app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentPreference.kt

@@ -20,6 +20,7 @@ class FragmentPreference : PreferenceFragmentCompat(), EditTextPreference.OnBind
         setNumberInput(SPKeys.SERVER_PORT_BASE)
         setNumberInput(SPKeys.SERVER_PORT_TRX)
         setNumberInput(SPKeys.SERVER_PORT_CONFIG)
+        setNumberInput(SPKeys.SERVER_PORT_ASSETS)
         setNumberInput(SPKeys.MIDDLE_PORT)
         setNumberInput(SPKeys.MIDDLE_WORKSTATION_ID)
         setNumberInput(SPKeys.FUEL_PORT)
@@ -41,6 +42,7 @@ class FragmentPreference : PreferenceFragmentCompat(), EditTextPreference.OnBind
             SPKeys.SERVER_PORT_BASE,
             SPKeys.SERVER_PORT_TRX,
             SPKeys.SERVER_PORT_CONFIG,
+            SPKeys.SERVER_PORT_ASSETS,
             SPKeys.MIDDLE_PORT,
             SPKeys.MIDDLE_WORKSTATION_ID,
             SPKeys.FUEL_PORT -> ValidateUtil.isPort(value)

+ 13 - 0
app/src/main/java/com/doverfuelingsolutions/issp/view/fragment/FragmentSelect.kt

@@ -19,9 +19,11 @@ import com.doverfuelingsolutions.issp.fusion.FusionManager
 import com.doverfuelingsolutions.issp.utils.log.DFSLog
 import com.doverfuelingsolutions.issp.utils.sp.SPUtil
 import com.doverfuelingsolutions.issp.view.MainActivity
+import com.doverfuelingsolutions.issp.view.PreferenceActivity
 import kotlinx.coroutines.*
 
 class FragmentSelect : Fragment(),
+    View.OnLongClickListener,
     View.OnClickListener {
 
     private lateinit var binding: FragmentSearchTypeBinding
@@ -40,6 +42,7 @@ class FragmentSelect : Fragment(),
             binding.lifecycleOwner = this
             binding.viewModel = viewModel
             binding.handler = this
+            binding.deviceNum.setOnLongClickListener(this)
         }
         return binding.root
     }
@@ -81,6 +84,16 @@ class FragmentSelect : Fragment(),
         }
     }
 
+    override fun onLongClick(v: View?): Boolean {
+        return when (v) {
+            binding.deviceNum -> {
+                PreferenceActivity.startForResult(requireActivity())
+                true
+            }
+            else -> false
+        }
+    }
+
     // TODO 当本地未处理订单无法按预期处理时(如本地锁的单,但在服务器上被其他渠道解锁了,造成解锁失败;消单同理;)
     /* 未处理的订单 */
     private fun handleUnsolvedOrder() {

+ 41 - 6
app/src/main/res/layout/activity_preference.xml

@@ -1,7 +1,42 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    style="@style/match">
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <FrameLayout
-        android:id="@+id/container"
-        style="@style/match" />
-</LinearLayout>
+    <data>
+
+        <variable
+            name="viewModel"
+            type="com.doverfuelingsolutions.issp.view.PreferenceActivity.PreferenceViewModel" />
+
+        <variable
+            name="handler"
+            type="com.doverfuelingsolutions.issp.view.PreferenceActivity" />
+    </data>
+
+    <ScrollView style="@style/match">
+
+        <LinearLayout
+            style="@style/fullWidth"
+            android:orientation="vertical">
+
+            <FrameLayout
+                android:id="@+id/container"
+                style="@style/match" />
+
+            <LinearLayout
+                style="@style/fullWidth"
+                android:layout_marginTop="40dp"
+                android:gravity="center_horizontal"
+                android:orientation="horizontal">
+
+                <Button
+                    android:id="@+id/checkUpdate"
+                    android:enabled="@{!viewModel.isLookingUpdate}"
+                    style="@style/Widget.MaterialComponents.Button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="20dp"
+                    android:onClick="@{handler}"
+                    android:text="@{viewModel.isLookingUpdate ? @string/in_check_update : @string/check_update}" />
+            </LinearLayout>
+        </LinearLayout>
+    </ScrollView>
+</layout>

+ 5 - 4
app/src/main/res/layout/layout_loading.xml

@@ -13,12 +13,13 @@
     <androidx.core.widget.ContentLoadingProgressBar
         android:id="@+id/progressBar"
         style="@android:style/Widget.Material.Light.ProgressBar"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:layout_marginBottom="10dp" />
+        android:layout_width="70dp"
+        android:layout_height="70dp"
+        android:layout_marginBottom="40dp" />
 
     <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/loadingTip"
         style="@style/wrap"
         android:text="@string/in_loading"
-        android:textSize="16sp" />
+        android:textSize="18sp" />
 </androidx.appcompat.widget.LinearLayoutCompat>

+ 36 - 0
app/src/main/res/layout/layout_version_update.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/wrap"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:minWidth="300dp"
+    android:paddingVertical="80dp"
+    android:paddingHorizontal="40dp">
+
+    <include layout="@layout/layout_loading" />
+
+    <TextView
+        android:id="@+id/versionTitle"
+        android:layout_marginTop="80dp"
+        android:layout_gravity="start"
+        android:text="@string/new_version"
+        android:textSize="24sp"
+        style="@style/wrap"/>
+
+    <TextView
+        android:id="@+id/versionInfo"
+        android:layout_marginTop="20dp"
+        android:layout_gravity="start"
+        android:text="@string/new_version"
+        android:textSize="18sp"
+        style="@style/wrap"/>
+
+    <Button
+        android:id="@+id/close"
+        style="@style/Widget.MaterialComponents.Button"
+        android:layout_width="wrap_content"
+        android:visibility="gone"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="100dp"
+        android:text="@string/click_close" />
+</LinearLayout>

+ 1 - 0
app/src/main/res/values/keys.xml

@@ -15,6 +15,7 @@
     <string name="sp_server_port_base">server_port_base</string>
     <string name="sp_server_port_trx">server_port_trx</string>
     <string name="sp_server_port_config">server_port_config</string>
+    <string name="sp_server_port_assets">server_port_assets</string>
     <string name="sp_middle_ip">middle_ip</string>
     <string name="sp_middle_port">middle_port</string>
     <string name="sp_middle_workstation_id">middle_workstation_id</string>

+ 16 - 0
app/src/main/res/values/strings.xml

@@ -6,6 +6,7 @@
     <string name="login">登录</string>
     <string name="preference">应用设置</string>
     <string name="blank"> </string>
+    <string name="back_desktop">退回桌面</string>
 
     <string name="start">开始</string>
     <string name="confirm">确认</string>
@@ -55,6 +56,7 @@
     <string name="port_auth">认证端口</string>
     <string name="port_trx">交易端口</string>
     <string name="port_config">配置端口</string>
+    <string name="port_assets">资源端口</string>
     <string name="middle_info">中控信息</string>
     <string name="workstation_id">workstationId</string>
     <string name="fuel_info">加油信息</string>
@@ -129,6 +131,20 @@
     <string name="fail_print_again">小票打印失败,请尝试重新打印</string>
     <string name="reprint">重新打印</string>
 
+    <!-- 版本迭代 -->
+    <string name="check_update">检查更新</string>
+    <string name="find_new_version">发现新版本</string>
+    <string name="new_version">新版本 %1$s 信息</string>
+    <string name="in_check_update">检查更新中</string>
+    <string name="no_update_version">无可更新版本</string>
+    <string name="current_version_is_latest">当前版本已是最新版本</string>
+    <string name="in_download">下载中</string>
+    <string name="download_done">下载已完成</string>
+    <string name="download_fail">下载失败,请在网络畅通的情况下重试</string>
+    <string name="download_progress">已下载:%1$s%%</string>
+    <string name="click_close">点击关闭</string>
+    <string name="cant_find_local_file">未找到本地文件</string>
+
     <string name="format_date">yyyy-MM-dd HH:mm:ss</string>
 
     <string name="toastSuccess">✔️ %1$s</string>

+ 6 - 0
app/src/main/res/xml/provider_paths.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <external-files-path
+        name="download"
+        path="." />
+</paths>

+ 6 - 0
app/src/main/res/xml/root_preferences.xml

@@ -40,6 +40,12 @@
             app:title="@string/port_config"
             app:dialogTitle="@string/hint_port_input"
             app:useSimpleSummaryProvider="true" />
+
+        <EditTextPreference
+            app:key="@string/sp_server_port_assets"
+            app:title="@string/port_assets"
+            app:dialogTitle="@string/hint_port_input"
+            app:useSimpleSummaryProvider="true" />
     </PreferenceCategory>
 
     <PreferenceCategory app:title="@string/middle_info">

File diff ditekan karena terlalu besar
+ 0 - 0
md


+ 4 - 0
project.md

@@ -0,0 +1,4 @@
+signature.jks:
+key store password: 123456
+key alias: isspt
+key password: 123456

TEMPAT SAMPAH
signature.jks


+ 16 - 0
version.json

@@ -0,0 +1,16 @@
+{
+  "name": "ISSPT Application Version Management",
+  "versionList": [
+    {
+      "name": "1.0.0.apk",
+      "version": "1.0.0",
+      "info": "室内自助支付APP第一个版本!"
+    },
+
+    {
+      "name": "1.0.1.apk",
+      "version": "1.0.1",
+      "info": "测试远程更新版本"
+    }
+  ]
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini