浏览代码

加入app版本自更新功能

songchengcheng 2 周之前
父节点
当前提交
db2beccc64

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

@@ -24,6 +24,7 @@
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
     <!-- 来电消音 -->

+ 4 - 0
app/src/main/java/com/sikey/interconnect/app/startup/RingApplication.kt

@@ -34,6 +34,7 @@ import com.sikey.interconnect.component.network.RequestManager
 import com.sikey.interconnect.component.network.http.RetrofitCloudAlbumFactory
 import com.sikey.interconnect.component.network.http.RetrofitFactory
 import com.sikey.interconnect.component.network.http.RetrofitUpsFactory
+import com.sikey.interconnect.component.network.http.UpdateManager
 import com.sikey.interconnect.component.session.IShowSessionExceptionDialog
 import com.sikey.interconnect.juphoon.JCWrapper.JCManager
 import com.sikey.interconnect.utils.MobileUtils
@@ -127,6 +128,9 @@ class RingApplication : Application(), OnTerminateListener {
         if (Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true) || Build.MANUFACTURER.equals("Oppo", ignoreCase = true)) {
             createRegularPushChannel()
         }
+
+        // Initialzie UpdateManager for app update
+        UpdateManager.getInstance().initialize(this)
     }
 
     private fun initializeBaiduMap(context: Context, agreePrivacy: Boolean) {

+ 6 - 0
app/src/main/java/com/sikey/interconnect/component/network/http/HttpService.kt

@@ -48,6 +48,7 @@ import com.sikey.interconnect.component.network.http.model.SchoolModeBool
 import com.sikey.interconnect.component.network.http.model.SchoolModeRepo
 import com.sikey.interconnect.component.network.http.model.UnRegisterReq
 import com.sikey.interconnect.component.network.http.model.UnbindDeviceReq
+import com.sikey.interconnect.component.network.http.model.UpdateAppRepo
 import com.sikey.interconnect.component.network.http.model.UpdateContactItem
 import com.sikey.interconnect.component.network.http.model.UploadFileRepo
 import com.sikey.interconnect.component.network.http.model.UpsSubscribeReq
@@ -369,4 +370,9 @@ interface PackageManagerService : BaseHttpService {
     fun updatePackageList(
         @Body body: PackageManagerInfo
     ): CustomFlowable<BaseResponse<NormalResponse>>
+}
+
+interface UpdateAppService : BaseHttpService {
+    @GET(UrlConstants.queryAppUpgrade)
+    fun queryAppUpgrade(@QueryMap paras: Map<String, String>): Call<UpdateAppRepo>
 }

+ 542 - 0
app/src/main/java/com/sikey/interconnect/component/network/http/UpdateManager.kt

@@ -0,0 +1,542 @@
+package com.sikey.interconnect.component.network.http
+/*
+
+import android.app.AlertDialog
+import android.app.DownloadManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import androidx.activity.ComponentActivity
+import androidx.core.content.FileProvider
+import com.sikey.interconnect.component.log.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+
+object UpdateManager {
+    private val TAG = UpdateManager::class.java.simpleName
+    private var downloadId: Long = -1L
+    private lateinit var downloadManager: DownloadManager
+    private var isReceiverRegistered = false
+
+    // 初始化 DownloadManager 和广播接收器
+    fun initialize(context: Context) {
+        downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+        if (!isReceiverRegistered) {
+            registerDownloadReceiver(context)
+        }
+    }
+
+    // 检查更新
+*/
+/*    fun checkForUpdate(context: Context, onUpdateAvailable: (UpdateInfo) -> Unit) {
+        CoroutineScope(Dispatchers.IO).launch {
+            try {
+                val response = RetrofitClient.apiService.getUpdateInfo()
+                if (response.isSuccessful) {
+                    val updateInfo = response.body()
+                    updateInfo?.let {
+                        val currentVersion = getCurrentVersionCode(context)
+                        if (it.versionCode > currentVersion) {
+                            withContext(Dispatchers.Main) {
+                                onUpdateAvailable(it)
+                            }
+                        }
+                    }
+                }
+            } catch (e: Exception) {
+                withContext(Dispatchers.Main) {
+                    showToast(context, "检查更新失败: ${e.message}")
+                }
+            }
+        }
+    }*//*
+
+
+    // 显示更新对话框
+*/
+/*    fun showUpdateDialog(context: Context, updateInfo: UpdateInfo) {
+        val builder = AlertDialog.Builder(context)
+        builder.setTitle("发现新版本 ${updateInfo.versionName}")
+        builder.setMessage(updateInfo.updateLog)
+        builder.setPositiveButton("立即更新") { _, _ ->
+            downloadAndInstall(context, updateInfo.downloadUrl)
+        }
+        if (!updateInfo.forceUpdate) {
+            builder.setNegativeButton("稍后") { dialog, _ -> dialog.dismiss() }
+        }
+        val dialog = builder.create()
+        dialog.setCancelable(!updateInfo.forceUpdate)
+        dialog.show()
+    }*//*
+
+
+    // 获取当前版本号
+    private fun getCurrentVersionCode(context: Context): Int {
+        return try {
+            context.packageManager.getPackageInfo(context.packageName, 0).versionCode
+        } catch (e: Exception) {
+            0
+        }
+    }
+
+    // 下载 APK
+    fun downloadAndInstall(context: Context, downloadUrl: String) {
+        Logger.d(TAG, "downloadAndInstall")
+        val request = DownloadManager.Request(Uri.parse(downloadUrl)).apply {
+            Logger.d(TAG, "downloadAndInstall 2")
+            setTitle("正在下载更新")
+            setDescription("下载 ${context.packageName} 新版本")
+            setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk")
+            } else {
+                setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.apk")
+            }
+            setMimeType("application/vnd.android.package-archive")
+            setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
+            Logger.d(TAG, "downloadAndInstall 3")
+        }
+        downloadId = downloadManager.enqueue(request)
+    }
+
+    // 安装 APK
+    fun installApk(context: Context, apkFile: File) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            if (!context.packageManager.canRequestPackageInstalls()) {
+                val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
+                    .setData(Uri.parse("package:${context.packageName}"))
+                if (context is ComponentActivity) {
+                    context.startActivity(intent)
+                }
+                return
+            }
+        }
+
+        val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            FileProvider.getUriForFile(context, "com.sikey.interconnect.download", apkFile)
+        } else {
+            Uri.fromFile(apkFile)
+        }
+
+        val intent = Intent(Intent.ACTION_VIEW)
+            .setDataAndType(uri, "application/vnd.android.package-archive")
+            .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        context.startActivity(intent)
+    }
+
+    // 注册下载完成广播
+    private fun registerDownloadReceiver(context: Context) {
+        val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+        context.registerReceiver(object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
+                if (id == downloadId) {
+                    val query = DownloadManager.Query().setFilterById(id)
+                    downloadManager.query(query).use { cursor ->
+                        if (cursor.moveToFirst()) {
+                            val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
+                            if (status == DownloadManager.STATUS_SUCCESSFUL) {
+                                val uriString = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
+                                val fileUri = Uri.parse(uriString)
+                                val file = File(fileUri.path ?: return)
+                                installApk(context, file)
+                            } else {
+                                showToast(context, "下载失败")
+                            }
+                        }
+                    }
+                }
+            }
+        }, filter, Context.RECEIVER_NOT_EXPORTED)
+        isReceiverRegistered = true
+    }
+
+    private fun showToast(context: Context, message: String) {
+        android.widget.Toast.makeText(context, message, android.widget.Toast.LENGTH_SHORT).show()
+    }
+}*/
+
+import android.Manifest
+import android.app.DownloadManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.FileProvider
+import androidx.lifecycle.lifecycleScope
+import com.sikey.interconnect.R
+import com.sikey.interconnect.component.log.Logger
+import com.sikey.interconnect.component.network.http.model.UpdateAppRepo
+import com.sikey.interconnect.constant.UrlConstants.SK_TOOL_URL
+import com.sikey.interconnect.ui.avtivity.base.BaseNoActionBarActivity
+import com.sikey.interconnect.utils.ToastUtils
+import com.sikey.interconnect.utils.VersionUtils
+import kotlinx.coroutines.launch
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.io.File
+import java.io.FileInputStream
+import java.math.BigInteger
+import java.security.MessageDigest
+
+class UpdateManager {
+    private var mContext: Context? = null
+    private val notificationId = 1001
+    private var downloadInfo = DownloadInfo(-1L, "", "", 0)
+    private lateinit var downloadManager: DownloadManager
+    private var isReceiverRegistered = false
+    private var updateAppService: UpdateAppService? = null
+
+    fun initialize(context: Context) {
+        mContext = context
+        downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+        if (!isReceiverRegistered) {
+            Logger.d(TAG, "======== registerDownloadReceiver")
+            registerDownloadReceiver(context)
+        }
+        val retrofit = Retrofit.Builder()
+            .baseUrl(SK_TOOL_URL)
+            .addConverterFactory(GsonConverterFactory.create())
+            .build()
+        updateAppService = retrofit.create(UpdateAppService::class.java)
+    }
+
+    // Check for new version
+/*    suspend fun checkForUpdate(currentVersion: String, updateUrl: String): UpdateInfo? {
+        return try {
+            withContext(Dispatchers.IO) {
+                val latestVersionInfo = URL(updateUrl).readText()
+                // Assuming updateUrl returns JSON with version and apkUrl
+                // Parse JSON to get version and download URL (simplified example)
+                val latestVersion = parseVersionFromJson(latestVersionInfo)
+                val apkUrl = parseApkUrlFromJson(latestVersionInfo)
+
+                if (isNewerVersion(currentVersion, latestVersion)) {
+                    UpdateInfo(latestVersion, apkUrl)
+                } else {
+                    null
+                }
+            }
+        } catch (e: Exception) {
+            null
+        }
+    }*/
+
+    // Start APK download and show notification
+    fun startDownload(updateInfo: UpdateInfo) {
+        val request = DownloadManager.Request(Uri.parse(updateInfo.apkUrl)).apply {
+            setTitle("${mContext?.packageName}")
+            setDescription("Downloading new version...")
+            //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                setDestinationInExternalFilesDir(mContext, Environment.DIRECTORY_DOWNLOADS, DOWNLOADED_APK_FILENAME)
+/*            } else {
+                setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "app-update.apk")
+            }*/
+            setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
+        }
+
+        downloadInfo.downloadId = downloadManager.enqueue(request)
+        downloadInfo.apkUrl = updateInfo.apkUrl
+        downloadInfo.md5 = updateInfo.md5
+        showDownloadNotification(0)
+    }
+
+    fun handleManualUpdate(updateInfo: UpdateInfo) {
+        val file = File(mContext?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), DOWNLOADED_APK_FILENAME)
+        var md5 = ""
+        if (file.exists()) {
+            md5 = calculateMD5(file)
+            Logger.d(TAG, "======== handleManualUpdate updateInfo.md5:${updateInfo.md5}, md5:$md5")
+        }
+        if (file.exists() && updateInfo.md5.equals(md5, ignoreCase = true)) {
+            Logger.d(TAG, "====== app-update.apk already exists")
+            showInstallDialog()
+        } else {
+            Logger.d(TAG, "====== no app-update.apk, start downloading")
+            //showInstallDialog()
+            startDownload(updateInfo)
+            ToastUtils.showLongToast(R.string.update_app_start_downloading)
+        }
+    }
+
+    // Notification with progress bar
+    private fun showDownloadNotification(progress: Int) {
+        val builder = mContext?.let {
+            NotificationCompat.Builder(it, "update_channel").apply {
+                setSmallIcon(android.R.drawable.stat_sys_download)
+                setContentTitle(it.packageName)
+                setContentText("Downloading...")
+                setPriority(NotificationCompat.PRIORITY_LOW)
+                setProgress(100, progress, false)
+                setOngoing(true)
+            }
+        }
+
+        if (mContext?.let {
+                ActivityCompat.checkSelfPermission(
+                    it,
+                    Manifest.permission.POST_NOTIFICATIONS
+                )
+            } != PackageManager.PERMISSION_GRANTED
+        ) {
+            // TODO: Consider calling
+            //    ActivityCompat#requestPermissions
+            // here to request the missing permissions, and then overriding
+            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            //                                          int[] grantResults)
+            // to handle the case where the user grants the permission. See the documentation
+            // for ActivityCompat#requestPermissions for more details.
+            return
+        }
+        if (builder != null) {
+            NotificationManagerCompat.from(mContext!!).notify(notificationId, builder.build())
+        }
+    }
+
+    // Register receiver to monitor download progress and completion
+    private fun registerDownloadReceiver(context: Context) {
+        val receiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
+                if (id != downloadInfo.downloadId) return
+
+                val query = DownloadManager.Query().setFilterById(downloadInfo.downloadId)
+                downloadManager.query(query).use { cursor ->
+                    if (cursor.moveToFirst()) {
+                        val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
+                        when (status) {
+                            DownloadManager.STATUS_SUCCESSFUL -> {
+                                Logger.d(TAG, "======== DownloadManager.STATUS_SUCCESSFUL")
+                                NotificationManagerCompat.from(context).cancel(notificationId)
+                                val apkFile = File(mContext?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), DOWNLOADED_APK_FILENAME)
+                                val md5 = calculateMD5(apkFile)
+                                Logger.d(TAG, "======== DownloadManager downloadInfo.md5:${downloadInfo.md5}, md5:$md5")
+                                if (apkFile.exists() && downloadInfo.md5.equals(md5, ignoreCase = true)) {
+                                    Logger.d(TAG, "======== DownloadManager showInstallDialog")
+                                    showInstallDialog()
+                                } else {
+                                    if (apkFile.exists()) {
+                                        apkFile.delete()
+                                    }
+                                    if (downloadInfo.retry < 2) {
+                                        Logger.d(TAG, "======== DownloadManager downloadInfo.retry:${downloadInfo.retry}")
+                                        downloadInfo.retry ++
+                                        startDownload(UpdateInfo("", "", downloadInfo.apkUrl, downloadInfo.md5))
+                                    }
+                                }
+                            }
+                            DownloadManager.STATUS_RUNNING -> {
+                                Logger.d(TAG, "======== DownloadManager.STATUS_RUNNING")
+                                val bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
+                                val bytesTotal = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
+                                if (bytesTotal > 0) {
+                                    val progress = (bytesDownloaded * 100 / bytesTotal)
+                                    showDownloadNotification(progress)
+                                }
+                            }
+                            DownloadManager.STATUS_FAILED -> {
+                                Logger.d(TAG, "======== DownloadManager.STATUS_FAILED")
+                                NotificationManagerCompat.from(context).cancel(notificationId)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        context.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+            Context.RECEIVER_NOT_EXPORTED)
+        context.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED),
+            Context.RECEIVER_NOT_EXPORTED)
+        isReceiverRegistered = true
+    }
+
+    // Show install dialog
+/*    private fun showInstallDialog() {
+        val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "app-update.apk")
+        val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            mContext?.let { FileProvider.getUriForFile(it, "com.sikey.interconnect.download", file) }
+        } else {
+            Uri.fromFile(file)
+        }
+
+        val intent = Intent(Intent.ACTION_VIEW).apply {
+            setDataAndType(uri, "application/vnd.android.package-archive")
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
+        }
+
+        val pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+        val builder = mContext?.let {
+            NotificationCompat.Builder(it, "update_channel").apply {
+                setSmallIcon(android.R.drawable.stat_sys_download_done)
+                setContentTitle("Update Downloaded")
+                setContentText("Tap to install the new version")
+                setPriority(NotificationCompat.PRIORITY_HIGH)
+                setContentIntent(pendingIntent)
+                setAutoCancel(true)
+            }
+        }
+
+        if (mContext?.let {
+                ActivityCompat.checkSelfPermission(
+                    it,
+                    Manifest.permission.POST_NOTIFICATIONS
+                )
+            } != PackageManager.PERMISSION_GRANTED
+        ) {
+            // TODO: Consider calling
+            //    ActivityCompat#requestPermissions
+            // here to request the missing permissions, and then overriding
+            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            //                                          int[] grantResults)
+            // to handle the case where the user grants the permission. See the documentation
+            // for ActivityCompat#requestPermissions for more details.
+            return
+        }
+        mContext?.let {
+            if (builder != null) {
+                NotificationManagerCompat.from(it).notify(notificationId, builder.build())
+            }
+        }
+    } */
+
+    fun showInstallDialog() {
+        Logger.d(TAG, "installApk")
+        try {
+            val apkFile = File(mContext?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app-update.apk")
+            if (!apkFile.exists()) {
+                Logger.e(TAG, "File not exist")
+                return
+            }
+
+            val apkUri: Uri? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                // Android 7.0+ 使用 FileProvider
+                mContext?.let {
+                    FileProvider.getUriForFile(
+                        it,
+                        "com.sikey.interconnect.download",
+                        apkFile
+                    )
+                }
+            } else {
+                Uri.fromFile(apkFile)
+            }
+
+            val intent = Intent(Intent.ACTION_VIEW).apply {
+                setDataAndType(apkUri, "application/vnd.android.package-archive")
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            }
+            mContext?.startActivity(intent)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Logger.e(TAG, "exception $e")
+        }
+    }
+
+    // Helper functions (implement these based on your needs)
+    private fun parseVersionFromJson(json: String): String {
+        // Parse JSON to extract version (e.g., "1.2.3")
+        return "1.2.3" // Placeholder
+    }
+
+    private fun parseApkUrlFromJson(json: String): String {
+        // Parse JSON to extract APK URL
+        return "https://example.com/app-update.apk" // Placeholder
+    }
+
+    private fun isNewerVersion(currentVersion: String, latestVersion: String): Boolean {
+        // Compare versions (e.g., "1.2.3" vs "1.2.4")
+        val currentParts = currentVersion.split(".").map { it.toInt() }
+        val latestParts = latestVersion.split(".").map { it.toInt() }
+        for (i in currentParts.indices) {
+            if (latestParts[i] > currentParts[i]) return true
+            if (latestParts[i] < currentParts[i]) return false
+        }
+        return false
+    }
+
+    fun calculateMD5(file: File): String {
+        val md = MessageDigest.getInstance("MD5")
+
+        FileInputStream(file).use { fis ->
+            val buffer = ByteArray(8192)
+            var bytesRead: Int
+
+            while (fis.read(buffer).also { bytesRead = it } != -1) {
+                md.update(buffer, 0, bytesRead)
+            }
+        }
+
+        val md5Bytes = md.digest()
+        val bigInt = BigInteger(1, md5Bytes)
+        return bigInt.toString(16).padStart(32, '0')
+    }
+
+    fun checkForUpdate(context: Context, onShowDialog: (updateInfo: UpdateInfo) -> Unit) {
+        (context as BaseNoActionBarActivity).lifecycleScope.launch {
+            val call = updateAppService?.queryAppUpgrade(
+                mutableMapOf(
+                    "PackageName" to context.packageName
+                )
+            )
+            call?.enqueue(object : Callback<UpdateAppRepo> {
+                override fun onResponse(call: Call<UpdateAppRepo>, response: Response<UpdateAppRepo>) {
+                    if (response.isSuccessful) {
+                        val responseBody = response.body()
+                        Logger.d(TAG, "====== Success: $responseBody")
+                        if (responseBody!!.code == "200") {
+                            val versionCode = responseBody.data.versionCode
+                            val versionName = responseBody.data.versionName
+                            val url = responseBody.data.downloadUrl
+                            val md5 = responseBody.data.md5
+                            Logger.d(TAG, "====== checkForUpdate onResponse, versionCode:$versionCode, name:$versionName, md5:$md5")
+                            onShowDialog(UpdateInfo(versionCode, versionName, url, md5))
+                        }
+                    } else {
+                        val errorBody = response.errorBody()?.string()
+                        Logger.e(TAG, "Error: $errorBody")
+                    }
+                }
+
+                override fun onFailure(call: Call<UpdateAppRepo>, t: Throwable) {
+                    Logger.e(TAG, "Network error: ${t.message}")
+                }
+            })
+        }
+    }
+
+    private object UpdateManagerHolder {
+        val instance = UpdateManager()
+    }
+
+    companion object {
+        fun getInstance(): UpdateManager {
+            return UpdateManagerHolder.instance
+        }
+        private const val TAG = "UpdateManager"
+        private const val DOWNLOADED_APK_FILENAME = "app-update.apk"
+    }
+}
+
+data class UpdateInfo(val versionCode: String, val version: String, val apkUrl: String, val md5: String)
+data class DownloadInfo(var downloadId: Long, var apkUrl: String, var md5: String, var retry: Int)

+ 24 - 0
app/src/main/java/com/sikey/interconnect/component/network/http/model/UpdateAppInfo.kt

@@ -0,0 +1,24 @@
+package com.sikey.interconnect.component.network.http.model
+
+import com.google.gson.annotations.SerializedName
+import com.sikey.interconnect.component.network.http.BaseResponse
+
+data class UpdateAppData(
+    @SerializedName("versionCode")
+    var versionCode: String,
+    @SerializedName("versionName")
+    var versionName: String,
+    @SerializedName("downloadUrl")
+    var downloadUrl: String,
+    @SerializedName("md5")
+    var md5: String
+) : BaseResponse<UpdateAppRepo>()
+
+data class UpdateAppRepo(
+    @SerializedName("code")
+    val code: String,
+    @SerializedName("message")
+    val message: String,
+    @SerializedName("data")
+    val data: UpdateAppData
+)

+ 3 - 0
app/src/main/java/com/sikey/interconnect/constant/UrlConstants.kt

@@ -129,6 +129,9 @@ object UrlConstants {
     const val getPackageList = "api/v2/userctx/user/child/device/getAppManageList"
     const val updatePackageList = "api/v2/userctx/user/child/device/updateAppManage"
 
+    // App Upgrade
+    const val queryAppUpgrade = "app-api/tools/app/queryAppUpgrade"
+
     //Constants
     const val START_REAL_TIME_POS =
         "/service/V2/ChildrenTraceService.asmx/StartRealTimePositioningV2"

+ 29 - 2
app/src/main/java/com/sikey/interconnect/ui/avtivity/component/right/AboutusActivity.kt

@@ -1,5 +1,6 @@
 package com.sikey.interconnect.ui.avtivity.component.right
 
+import android.app.AlertDialog
 import android.content.ClipData
 import android.content.ClipboardManager
 import android.content.Context
@@ -16,17 +17,18 @@ import android.widget.Toast
 import androidx.appcompat.widget.Toolbar
 import com.sikey.interconnect.R
 import com.sikey.interconnect.app.DataManager.Companion.instance
+import com.sikey.interconnect.component.network.http.UpdateInfo
+import com.sikey.interconnect.component.network.http.UpdateManager
 import com.sikey.interconnect.component.other.SubSkipHandler
 import com.sikey.interconnect.constant.UrlConstants
-import com.sikey.interconnect.constant.UrlConstants.SUPPORT_LANGUAGE
 import com.sikey.interconnect.ui.adapter.AboutusListAdapter
 import com.sikey.interconnect.ui.adapter.item.AboutusItem
 import com.sikey.interconnect.ui.adapter.type.AboutusType
 import com.sikey.interconnect.ui.avtivity.base.BaseNoActionBarActivity
 import com.sikey.interconnect.ui.fragment.signup.HtmlActivity
 import com.sikey.interconnect.utils.ResUtils
+import com.sikey.interconnect.utils.ToastUtils
 import com.sikey.interconnect.utils.VersionUtils
-import java.util.Locale
 
 class AboutusActivity : BaseNoActionBarActivity() {
     private var mListView: ListView? = null
@@ -203,6 +205,15 @@ class AboutusActivity : BaseNoActionBarActivity() {
                     item.content = VersionUtils.getVersionName(this)
                 }
 
+                ResUtils.getString(R.string.check_update) -> {
+                    item.title = ResUtils.getString(R.string.check_update)
+                    item.subHandler = SubSkipHandler { context ->
+                        UpdateManager.getInstance().checkForUpdate(this) {
+                            showUpdateDialog(this, it)
+                        }
+                    }
+                }
+
                 else -> {
                     item.type = AboutusType.CONTENT_WITH_SUB
                 }
@@ -211,6 +222,22 @@ class AboutusActivity : BaseNoActionBarActivity() {
         }
     }
 
+    fun showUpdateDialog(context: Context, updateInfo: UpdateInfo) {
+        if (VersionUtils.getVersionCode(context) >= updateInfo.versionCode.toInt()) {
+            ToastUtils.showLongToast(R.string.update_app_already_latest_ver)
+        } else {
+            val builder = AlertDialog.Builder(context)
+            builder.setTitle(getString(R.string.update_app_title))
+            builder.setMessage("${getString(R.string.update_app_version)}: ${updateInfo.version} \n${getString(R.string.update_app_detail)}")
+            builder.setPositiveButton(getString(R.string.update_now)) { _, _ ->
+                UpdateManager.getInstance().handleManualUpdate(updateInfo)
+            }
+            builder.setNegativeButton(getString(R.string.update_later)) { dialog, _ -> dialog.dismiss() }
+            val dialog = builder.create()
+            dialog.show()
+        }
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_aboutus)

+ 12 - 0
app/src/main/java/com/sikey/interconnect/utils/VersionUtils.java

@@ -24,4 +24,16 @@ public class VersionUtils {
 
     }
 
+    public static int getVersionCode(Context context)
+    {
+        try
+        {
+            return context.getPackageManager().getPackageInfo(context.getPackageName(),0).versionCode;
+        }
+        catch(PackageManager.NameNotFoundException ex)
+        {
+            ex.printStackTrace();
+            return 0;
+        }
+    }
 }

+ 6 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -818,6 +818,7 @@
     <string name="data_m">米</string>
     <string name="checking_version">正在检查版本信息</string>
     <string name="update_now">立即更新</string>
+    <string name="update_later">稍后</string>
     <string name="update_msg">手表会在充电或电量充足时更新到%s</string>
     <string name="monitor">监听</string>
     <string name="holiday">避开法定节假日</string>
@@ -1075,5 +1076,10 @@
     <string name="background_popup_perm_detail">为确保视频通话等功能正常,请授权应用后台弹出界面权限</string>
     <string name="background_popup_perm_detail_oppo">为确保视频通话等功能正常,请在手机设置中开启后台弹出界面权限:\n设置 > 隐私 > 权限管理 > 其他权限 > 特殊应用权限 > 后台弹出界面</string>
     <string name="goto_settings">前往设置</string>
+    <string name="update_app_title">发现新版本</string>
+    <string name="update_app_version">版本号</string>
+    <string name="update_app_detail">优化相关功能,修复了若干问题</string>
+    <string name="update_app_already_latest_ver">已是最新版本</string>
+    <string name="update_app_start_downloading">开始下载,请在通知栏查看进度</string>
 </resources>
 

+ 2 - 0
app/src/main/res/values/string_array.xml

@@ -54,6 +54,7 @@
         <item>@string/software_privacy</item>
         <item>@string/version_info</item>
         <item>@string/current_version</item>
+        <item>@string/check_update</item>
     </string-array>
 
     <string-array name="newbie_guide_title_array">
@@ -92,6 +93,7 @@
         <item></item>
         <item></item>
         <item></item>
+        <item></item>
     </string-array>
     <string-array name="guardian_names">
         <item>爸爸</item>

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

@@ -871,6 +871,7 @@
     <string name="data_m">meter</string>
     <string name="checking_version">Checking software version</string>
     <string name="update_now">Update now</string>
+    <string name="update_later">Later</string>
     <string name="update_msg">Watch will update to version %s when charged or battery level resumed. </string>
     <string name="monitor">Monitor</string>
     <string name="holiday">Avoid holiday</string>
@@ -1179,4 +1180,9 @@
     <string name="background_popup_perm_detail">To ensure functions like video calls work properly, please grant the app permission to open new windows while running in the background.</string>
     <string name="background_popup_perm_detail_oppo">To ensure functions like video calls work properly, please ,Please enable the \'Background pop-up windows\' permission in your phone settings:\nSettings > Privacy > Permission manager > Other permissions > Special app access > Display pop-ups while in background</string>
     <string name="goto_settings">Go to settings</string>
+    <string name="update_app_title">Found new version</string>
+    <string name="update_app_version">Version</string>
+    <string name="update_app_detail">Optimized relevant features and fixed several issues.</string>
+    <string name="update_app_already_latest_ver">Already the latest version</string>
+    <string name="update_app_start_downloading">The download has started. Please check the progress in notification bar.</string>
 </resources>