Fix Update

This commit is contained in:
phanhuuloi27
2022-07-19 14:50:57 +07:00
parent 6570142066
commit 2a7ea6022b
42 changed files with 2422 additions and 86 deletions
+3 -3
View File
@@ -10,14 +10,13 @@ android {
minSdk 23 minSdk 23
targetSdk 30 targetSdk 30
versionCode 1 versionCode 1
versionName "88.0" versionName "87.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
@@ -90,7 +89,8 @@ dependencies {
implementation 'com.daimajia.slider:library:1.1.5@aar' implementation 'com.daimajia.slider:library:1.1.5@aar'
implementation "androidx.browser:browser:1.3.0" implementation "androidx.browser:browser:1.3.0"
implementation 'com.github.Piashsarker:AndroidAppUpdateLibrary:1.0.4' implementation project(':appupdate')
} }
-13
View File
@@ -11,8 +11,6 @@
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" /> <uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -22,21 +20,10 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Foodify" android:theme="@style/Theme.Foodify"
tools:replace="android:theme" tools:replace="android:theme"
tools:targetApi="31"> tools:targetApi="31">
<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>
<receiver android:name=".OTP_Receiver"> <receiver android:name=".OTP_Receiver">
<intent-filter> <intent-filter>
@@ -38,11 +38,11 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.andremion.counterfab.CounterFab; import com.andremion.counterfab.CounterFab;
import com.azhon.appupdate.manager.DownloadManager;
import com.daimajia.slider.library.Animations.DescriptionAnimation; import com.daimajia.slider.library.Animations.DescriptionAnimation;
import com.daimajia.slider.library.SliderLayout; import com.daimajia.slider.library.SliderLayout;
import com.daimajia.slider.library.SliderTypes.BaseSliderView; import com.daimajia.slider.library.SliderTypes.BaseSliderView;
import com.daimajia.slider.library.SliderTypes.TextSliderView; import com.daimajia.slider.library.SliderTypes.TextSliderView;
import com.dcastalia.localappupdate.DownloadApk;
import com.firebase.ui.database.FirebaseRecyclerAdapter; import com.firebase.ui.database.FirebaseRecyclerAdapter;
import com.firebase.ui.database.FirebaseRecyclerOptions; import com.firebase.ui.database.FirebaseRecyclerOptions;
import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.OnCompleteListener;
@@ -98,6 +98,8 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
HashMap<String, String> image_list; HashMap<String, String> image_list;
SliderLayout mSlider; SliderLayout mSlider;
DownloadManager manager = null;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -180,7 +182,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
database = FirebaseDatabase.getInstance(); database = FirebaseDatabase.getInstance();
category = database.getReference("Category"); category = database.getReference("Category");
if(Double.parseDouble(versionNameApp) < Double.parseDouble(Common.versionAppNewest)){ if (Double.parseDouble(versionNameApp) < Double.parseDouble(Common.versionAppNewest)) {
alertDialogUpdate(); alertDialogUpdate();
} }
@@ -239,7 +241,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
//Setup Slider //Setup Slider
setupSlider(); setupSlider();
if(!NotificationManagerCompat.from(this).areNotificationsEnabled()){ if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) {
showNotificationAlertDialog("Hãy bật quyền thông báo trên thiết bị của bạn để chúng thôi có thể cung cấp thông tin cho bạn một cách nhanh nhất!"); showNotificationAlertDialog("Hãy bật quyền thông báo trên thiết bị của bạn để chúng thôi có thể cung cấp thông tin cho bạn một cách nhanh nhất!");
} }
@@ -247,28 +249,22 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
} }
private void alertDialogUpdate() { private void alertDialogUpdate() {
AlertDialog alertDialog = new AlertDialog.Builder(Home.this) String url = "https://github.com/Nezumi2711/PRM392/raw/main/app-debug.apk";
.setTitle("Thông báo!") manager = new DownloadManager.Builder(Home.this)
.setMessage("Hiện tại đã có phiên bản mới hơn! Vui lòng cập nhật ứng dụng để mang đến những trải nghiệm tốt nhất!") .apkUrl(url)
.setPositiveButton("Đồng ý", null) .apkName("app-debug.apk")
.setNegativeButton("Để sau", null) .smallIcon(R.mipmap.ic_launcher)
.create(); .apkVersionCode(3)
.apkSize("26.5")
.showNotification(false)
.showBgdToast(false)
.apkVersionName(Common.versionAppNewest + ".0 ")
.apkDescription("1. Sửa lỗi đặt hàng!\n2. Cải thiện hiệu suất\n3. Tối ưu giao diện\n4. Cập nhật thư viện app.")
.dialogProgressBarColor(0xFFFF5353)
.jumpInstallPage(false)
.build();
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { manager.download();
@Override
public void onShow(DialogInterface dialog) {
Button agree = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
agree.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkWriteExternalStoragePermission();
alertDialog.dismiss();
}
});
}
});
alertDialog.show();
} }
private void alertDialogNewest() { private void alertDialogNewest() {
@@ -280,44 +276,6 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
alertDialog.show(); alertDialog.show();
} }
private void checkWriteExternalStoragePermission() {
if (ActivityCompat.checkSelfPermission(Home.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
// If we have permission than we can Start the Download the task
downloadTask();
} else {
// If we don't have permission than requesting the permission
requestWriteExternalStoragePermission();
}
}
private void requestWriteExternalStoragePermission() {
if (ActivityCompat.checkSelfPermission(Home.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(Home.this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
} else{
ActivityCompat.requestPermissions(Home.this,new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode==MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadTask();
} else {
Toast.makeText(Home.this, "Permission Not Granted.", Toast.LENGTH_SHORT).show();
}
}
private void downloadTask() {
// This @DownloadApk class is provided by our library
// Pass the Context when creating object of DownloadApk
String url = "https://github.com/Nezumi2711/PRM392/raw/main/app-debug.apk";
DownloadApk downloadApk = new DownloadApk(Home.this);
downloadApk.startDownloadingApk(url, "Update 2.0");
}
private void showNotificationAlertDialog(String msg) { private void showNotificationAlertDialog(String msg) {
AlertDialog alertDialog = new AlertDialog.Builder(Home.this) AlertDialog alertDialog = new AlertDialog.Builder(Home.this)
.setTitle("Thông báo!") .setTitle("Thông báo!")
@@ -358,7 +316,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
alertDialog.show(); alertDialog.show();
TextView textView = (TextView) alertDialog.findViewById(android.R.id.message); TextView textView = (TextView) alertDialog.findViewById(android.R.id.message);
Typeface face=Typeface.createFromAsset(getAssets(),"fonts/regular.ttf"); Typeface face = Typeface.createFromAsset(getAssets(), "fonts/regular.ttf");
textView.setTypeface(face); textView.setTypeface(face);
} }
@@ -479,7 +437,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
} else if (id == R.id.nav_favorites) { } else if (id == R.id.nav_favorites) {
startActivity(new Intent(Home.this, FavoritesActivity.class)); startActivity(new Intent(Home.this, FavoritesActivity.class));
} else if (id == R.id.nav_update) { } else if (id == R.id.nav_update) {
if(Double.parseDouble(versionNameApp) < Double.parseDouble(Common.versionAppNewest)){ if (Double.parseDouble(versionNameApp) < Double.parseDouble(Common.versionAppNewest)) {
alertDialogUpdate(); alertDialogUpdate();
} else { } else {
alertDialogNewest(); alertDialogNewest();
@@ -529,7 +487,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
FirebaseMessaging.getInstance().subscribeToTopic(Common.topicName); FirebaseMessaging.getInstance().subscribeToTopic(Common.topicName);
//Write value //Write value
Paper.book().write("sub_new", "true"); Paper.book().write("sub_new", "true");
if(!NotificationManagerCompat.from(Home.this).areNotificationsEnabled()){ if (!NotificationManagerCompat.from(Home.this).areNotificationsEnabled()) {
showNotificationAlertDialog("Oops, máy bạn chưa được cấp quyền thông báo! Bạn có muốn bật để nhận thông báo từ hệ thống không?"); showNotificationAlertDialog("Oops, máy bạn chưa được cấp quyền thông báo! Bạn có muốn bật để nhận thông báo từ hệ thống không?");
} }
} else { } else {
@@ -652,11 +610,11 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
} }
} else { } else {
waitingDialog.dismiss(); waitingDialog.dismiss();
if(TextUtils.isEmpty(edtPassword.getText().toString())){ if (TextUtils.isEmpty(edtPassword.getText().toString())) {
edtPassword.setError("Vui lòng điền vào trường này!"); edtPassword.setError("Vui lòng điền vào trường này!");
} else if(TextUtils.isEmpty(edtNewPassword.getText().toString())){ } else if (TextUtils.isEmpty(edtNewPassword.getText().toString())) {
edtNewPassword.setError("Vui lòng điền vào trường này!"); edtNewPassword.setError("Vui lòng điền vào trường này!");
} else if(TextUtils.isEmpty(edtRepeatPassword.getText().toString())){ } else if (TextUtils.isEmpty(edtRepeatPassword.getText().toString())) {
edtRepeatPassword.setError("Vui lòng điền vào trường này!"); edtRepeatPassword.setError("Vui lòng điền vào trường này!");
} }
} }
@@ -684,7 +642,7 @@ public class Home extends AppCompatActivity implements NavigationView.OnNavigati
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if(backPressedTime + 2000 > System.currentTimeMillis()) { if (backPressedTime + 2000 > System.currentTimeMillis()) {
super.onBackPressed(); super.onBackPressed();
return; return;
} else { } else {
@@ -8,4 +8,14 @@
<string name="nav_header_desc">Navigation header</string> <string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="nav_header_title">Android Studio</string> <string name="nav_header_title">Android Studio</string>
<string name="app_title">一個簡單好用的版本更新庫</string>
<string name="dialog_title">發現新版本</string>
<string name="dialog_msg">1.支持Android4.1及以上版本\n2.支持自定義下載過程\n3.支持通知欄進度條展示\n4.支持文字國際化\n5.使用Kotlin協程重構</string>
<string name="dialog_confirm">升級</string>
<string name="layout_msg">使用說明:三個方式不能並行 請等待一個方式下載完成在使用其他方式</string>
<string name="layout_custom_dialog">使用自己的對話框更新</string>
<string name="layout_simple_use">簡單易用</string>
<string name="layout_library_dialog">使用版本庫內置的對話框更新</string>
<string name="layout_cancel">取消下載</string>
</resources> </resources>
+1
View File
@@ -0,0 +1 @@
/build
+41
View File
@@ -0,0 +1,41 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 32
defaultConfig {
minSdk 23
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.azhon.appupdate
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.azhon.appupdate.test", appContext.packageName)
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.azhon.appupdate">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:usesCleartextTraffic="true">
<service android:name=".service.DownloadService" />
<provider
android:name="com.azhon.appupdate.config.AppUpdateFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/app_update_file" />
</provider>
<activity
android:name=".view.UpdateDialogActivity"
android:theme="@style/UpdateDialog" />
</application>
</manifest>
@@ -0,0 +1,34 @@
package com.azhon.appupdate.base
import com.azhon.appupdate.base.bean.DownloadStatus
import kotlinx.coroutines.flow.Flow
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.base
* FileName: BaseHttpDownloadManager
* CreateDate: 2022/4/7 on 10:24
* Desc:
*
* @author azhon
*/
abstract class BaseHttpDownloadManager {
/**
* download apk from apkUrl
*
* @param apkUrl
* @param apkName
*/
abstract fun download(apkUrl: String, apkName: String): Flow<DownloadStatus>
/**
* cancel download apk
*/
abstract fun cancel()
/**
* release memory
*/
abstract fun release()
}
@@ -0,0 +1,28 @@
package com.azhon.appupdate.base.bean
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.base.bean
* FileName: DownloadStatus
* CreateDate: 2022/4/14 on 11:18
* Desc:
*
* @author azhon
*/
sealed class DownloadStatus {
object Start : DownloadStatus()
data class Downloading(val max: Int, val progress: Int) : DownloadStatus()
class Done(val apk: File) : DownloadStatus()
object Cancel : DownloadStatus()
data class Error(val e: Throwable) : DownloadStatus()
}
@@ -0,0 +1,16 @@
package com.azhon.appupdate.config
import androidx.core.content.FileProvider
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.config
* FileName: AppUpdateFileProvider
* CreateDate: 2022/4/7 on 10:30
* Desc:
*
* @author azhon
*/
class AppUpdateFileProvider : FileProvider()
@@ -0,0 +1,59 @@
package com.azhon.appupdate.config
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.config
* FileName: Constant
* CreateDate: 2022/4/7 on 10:28
* Desc:
*
* @author azhon
*/
object Constant {
/**
* Http timeout(ms)
*/
const val HTTP_TIME_OUT = 30_000
/**
* Logcat tag
*/
const val TAG = "AppUpdate."
/**
* Apk file extension
*/
const val APK_SUFFIX = ".apk"
/**
* Coroutine Name
*/
const val COROUTINE_NAME = "app-update-coroutine"
/**
* Notification channel id
*/
const val DEFAULT_CHANNEL_ID = "appUpdate"
/**
* Notification id
*/
const val DEFAULT_NOTIFY_ID = 1011
/**
* Notification channel name
*/
const val DEFAULT_CHANNEL_NAME = "AppUpdate"
/**
* Compat Android N file uri
*/
var AUTHORITIES: String? = null
/**
* Apk path
*/
var APK_PATH = "/storage/emulated/0/Android/data/%s/cache"
}
@@ -0,0 +1,40 @@
package com.azhon.appupdate.listener
import android.app.Activity
import android.app.Application
import android.os.Bundle
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.listener
* FileName: LifecycleCallbacksAdapter
* CreateDate: 2022/4/8 on 11:26
* Desc:
*
* @author azhon
*/
abstract class LifecycleCallbacksAdapter : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
}
@@ -0,0 +1,28 @@
package com.azhon.appupdate.listener
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.listener
* FileName: OnButtonClickListener
* CreateDate: 2022/4/7 on 15:56
* Desc:
*
* @author azhon
*/
interface OnButtonClickListener {
companion object {
/**
* click update button
*/
const val UPDATE = 0
/**
* click cancel button
*/
const val CANCEL = 1
}
fun onButtonClick(id: Int)
}
@@ -0,0 +1,43 @@
package com.azhon.appupdate.listener
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.listener
* FileName: OnDownloadListener
* CreateDate: 2022/4/7 on 10:27
* Desc:
*
* @author azhon
*/
interface OnDownloadListener {
/**
* start download
*/
fun start()
/**
*
* @param max file length
* @param progress downloaded file size
*/
fun downloading(max: Int, progress: Int)
/**
* @param apk
*/
fun done(apk: File)
/**
* cancel download
*/
fun cancel()
/**
*
* @param e
*/
fun error(e: Throwable)
}
@@ -0,0 +1,31 @@
package com.azhon.appupdate.listener
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.listener
* FileName: OnDownloadListenerAdapter
* CreateDate: 2022/4/8 on 10:58
* Desc:
*
* @author azhon
*/
abstract class OnDownloadListenerAdapter : OnDownloadListener {
override fun start() {
}
override fun downloading(max: Int, progress: Int) {
}
override fun done(apk: File) {
}
override fun cancel() {
}
override fun error(e: Throwable) {
}
}
@@ -0,0 +1,437 @@
package com.azhon.appupdate.manager
import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.content.Intent
import android.widget.Toast
import com.azhon.appupdate.R
import com.azhon.appupdate.base.BaseHttpDownloadManager
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.LifecycleCallbacksAdapter
import com.azhon.appupdate.listener.OnButtonClickListener
import com.azhon.appupdate.listener.OnDownloadListener
import com.azhon.appupdate.service.DownloadService
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.LogUtil
import com.azhon.appupdate.view.UpdateDialogActivity
import java.io.Serializable
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.manager
* FileName: DownloadManager
* CreateDate: 2022/4/7 on 10:36
* Desc:
*
* @author azhon
*/
class DownloadManager private constructor(builder: Builder) : Serializable {
companion object {
private const val TAG = "DownloadManager"
private var instance: DownloadManager? = null
fun getInstance(builder: Builder? = null): DownloadManager? {
if (instance == null) {
if (builder == null) return null
instance = DownloadManager(builder)
}
return instance!!
}
}
var application: Application = builder.application
var contextClsName: String = builder.contextClsName
var downloadState: Boolean = false
var apkUrl: String
var apkName: String
var apkVersionCode: Int
var apkVersionName: String
var downloadPath: String
var showNewerToast: Boolean
var smallIcon: Int
var apkDescription: String
var apkSize: String
var apkMD5: String
var httpManager: BaseHttpDownloadManager?
var notificationChannel: NotificationChannel?
var onDownloadListeners: MutableList<OnDownloadListener>
var onButtonClickListener: OnButtonClickListener?
var showNotification: Boolean
var jumpInstallPage: Boolean
var showBgdToast: Boolean
var forcedUpgrade: Boolean
var notifyId: Int
var dialogImage: Int
var dialogButtonColor: Int
var dialogButtonTextColor: Int
var dialogProgressBarColor: Int
init {
apkUrl = builder.apkUrl
apkName = builder.apkName
apkVersionCode = builder.apkVersionCode
apkVersionName = builder.apkVersionName
downloadPath =
builder.downloadPath ?: String.format(Constant.APK_PATH, application.packageName)
showNewerToast = builder.showNewerToast
smallIcon = builder.smallIcon
apkDescription = builder.apkDescription
apkSize = builder.apkSize
apkMD5 = builder.apkMD5
httpManager = builder.httpManager
notificationChannel = builder.notificationChannel
onDownloadListeners = builder.onDownloadListeners
onButtonClickListener = builder.onButtonClickListener
showNotification = builder.showNotification
jumpInstallPage = builder.jumpInstallPage
showBgdToast = builder.showBgdToast
forcedUpgrade = builder.forcedUpgrade
notifyId = builder.notifyId
dialogImage = builder.dialogImage
dialogButtonColor = builder.dialogButtonColor
dialogButtonTextColor = builder.dialogButtonTextColor
dialogProgressBarColor = builder.dialogProgressBarColor
// Fix memory leak
application.registerActivityLifecycleCallbacks(object : LifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
super.onActivityDestroyed(activity)
if (contextClsName == activity.javaClass.name) {
clearListener()
}
}
})
}
/**
* Start download
*/
fun download() {
if (!checkParams()) {
return
}
if (checkVersionCode()) {
application.startService(Intent(application, DownloadService::class.java))
} else {
if (apkVersionCode > ApkUtil.getVersionCode(application)) {
application.startActivity(
Intent(application, UpdateDialogActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} else {
if (showNewerToast) {
Toast.makeText(application, R.string.latest_version, Toast.LENGTH_SHORT).show()
}
LogUtil.d(TAG, application.resources.getString(R.string.latest_version))
}
}
}
private fun checkParams(): Boolean {
if (apkUrl.isEmpty()) {
LogUtil.e(TAG, "apkUrl can not be empty!")
return false
}
if (apkName.isEmpty()) {
LogUtil.e(TAG, "apkName can not be empty!")
return false
}
if (!apkName.endsWith(Constant.APK_SUFFIX)) {
LogUtil.e(TAG, "apkName must endsWith .apk!")
return false
}
if (smallIcon == -1) {
LogUtil.e(TAG, "smallIcon can not be empty!");
return false
}
Constant.AUTHORITIES = "${application.packageName}.fileProvider"
return true
}
/**
* Check the set apkVersionCode if it is not the default then use the built-in dialog
* If it is the default value Int.MIN_VALUE, directly start the service download
*/
private fun checkVersionCode(): Boolean {
if (apkVersionCode == Int.MIN_VALUE) {
return true
}
if (apkDescription.isEmpty()) {
LogUtil.e(TAG, "apkDescription can not be empty!")
}
return false
}
fun cancel() {
httpManager?.cancel()
}
/**
* release objects
*/
internal fun release() {
instance = null
clearListener()
}
private fun clearListener() {
onButtonClickListener = null
onDownloadListeners.clear()
}
class Builder constructor(activity: Activity) {
/**
* library context
*/
internal var application: Application = activity.application
/**
* Fix the memory leak caused by Activity destroy
*/
internal var contextClsName: String = activity.javaClass.name
/**
* Apk download url
*/
internal var apkUrl = ""
/**
* Apk file name on disk
*/
internal var apkName = ""
/**
* The apk versionCode that needs to be downloaded
*/
internal var apkVersionCode = Int.MIN_VALUE
/**
* The versionName of the dialog reality
*/
internal var apkVersionName = ""
/**
* The file path where the Apk is saved
* eg: /storage/emulated/0/Android/data/ your packageName /cache
*/
internal var downloadPath = application.externalCacheDir?.path
/**
* whether to tip to user "Currently the latest version!"
*/
internal var showNewerToast = false
/**
* Notification icon resource
*/
internal var smallIcon = -1
/**
* New version description information
*/
internal var apkDescription = ""
/**
* Apk Size,Unit MB
*/
internal var apkSize = ""
/**
* Apk md5 file verification(32-bit) verification repeated download
*/
internal var apkMD5 = ""
/**
* Apk download manager
*/
internal var httpManager: BaseHttpDownloadManager? = null
/**
* The following are unimportant filed
*/
/**
* adapter above Android O notification
*/
internal var notificationChannel: NotificationChannel? = null
/**
* download listeners
*/
internal var onDownloadListeners = mutableListOf<OnDownloadListener>()
/**
* dialog button click listener
*/
internal var onButtonClickListener: OnButtonClickListener? = null
/**
* Whether to show the progress of the notification
*/
internal var showNotification = true
/**
* Whether the installation page will pop up automatically after the download is complete
*/
internal var jumpInstallPage = true
/**
* Does the download start tip "Downloading a new version in the background..."
*/
internal var showBgdToast = true
/**
* Whether to force an upgrade
*/
internal var forcedUpgrade = false
/**
* Notification id
*/
internal var notifyId = Constant.DEFAULT_NOTIFY_ID
/**
* dialog background Image resource
*/
internal var dialogImage = -1
/**
* dialog button background color
*/
internal var dialogButtonColor = -1
/**
* dialog button text color
*/
internal var dialogButtonTextColor = -1
/**
* dialog progress bar color and progress-text color
*/
internal var dialogProgressBarColor = -1
fun apkUrl(apkUrl: String): Builder {
this.apkUrl = apkUrl
return this
}
fun apkName(apkName: String): Builder {
this.apkName = apkName
return this
}
fun apkVersionCode(apkVersionCode: Int): Builder {
this.apkVersionCode = apkVersionCode
return this
}
fun apkVersionName(apkVersionName: String): Builder {
this.apkVersionName = apkVersionName
return this
}
fun showNewerToast(showNewerToast: Boolean): Builder {
this.showNewerToast = showNewerToast
return this
}
fun smallIcon(smallIcon: Int): Builder {
this.smallIcon = smallIcon
return this
}
fun apkDescription(apkDescription: String): Builder {
this.apkDescription = apkDescription
return this
}
fun apkSize(apkSize: String): Builder {
this.apkSize = apkSize
return this
}
fun apkMD5(apkMD5: String): Builder {
this.apkMD5 = apkMD5
return this
}
fun httpManager(httpManager: BaseHttpDownloadManager): Builder {
this.httpManager = httpManager
return this
}
fun notificationChannel(notificationChannel: NotificationChannel): Builder {
this.notificationChannel = notificationChannel
return this
}
fun onButtonClickListener(onButtonClickListener: OnButtonClickListener): Builder {
this.onButtonClickListener = onButtonClickListener
return this
}
fun onDownloadListener(onDownloadListener: OnDownloadListener): Builder {
this.onDownloadListeners.add(onDownloadListener)
return this
}
fun showNotification(showNotification: Boolean): Builder {
this.showNotification = showNotification
return this
}
fun jumpInstallPage(jumpInstallPage: Boolean): Builder {
this.jumpInstallPage = jumpInstallPage
return this
}
fun showBgdToast(showBgdToast: Boolean): Builder {
this.showBgdToast = showBgdToast
return this
}
fun forcedUpgrade(forcedUpgrade: Boolean): Builder {
this.forcedUpgrade = forcedUpgrade
return this
}
fun notifyId(notifyId: Int): Builder {
this.notifyId = notifyId
return this
}
fun dialogImage(dialogImage: Int): Builder {
this.dialogImage = dialogImage
return this
}
fun dialogButtonColor(dialogButtonColor: Int): Builder {
this.dialogButtonColor = dialogButtonColor
return this
}
fun dialogButtonTextColor(dialogButtonTextColor: Int): Builder {
this.dialogButtonTextColor = dialogButtonTextColor
return this
}
fun dialogProgressBarColor(dialogProgressBarColor: Int): Builder {
this.dialogProgressBarColor = dialogProgressBarColor
return this
}
fun enableLog(enable: Boolean): Builder {
LogUtil.enable(enable)
return this
}
fun build(): DownloadManager {
return getInstance(this)!!
}
}
}
@@ -0,0 +1,134 @@
package com.azhon.appupdate.manager
import com.azhon.appupdate.base.BaseHttpDownloadManager
import com.azhon.appupdate.base.bean.DownloadStatus
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.util.LogUtil
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.manager
* FileName: HttpDownloadManager
* CreateDate: 2022/4/7 on 14:29
* Desc:
*
* @author azhon
*/
@Suppress("BlockingMethodInNonBlockingContext")
class HttpDownloadManager(private val path: String) : BaseHttpDownloadManager() {
companion object {
private const val TAG = "HttpDownloadManager"
}
private var shutdown: Boolean = false
override fun download(apkUrl: String, apkName: String): Flow<DownloadStatus> {
trustAllHosts()
shutdown = false
File(path, apkName).let {
if (it.exists()) it.delete()
}
return flow {
emit(DownloadStatus.Start)
connectToDownload(apkUrl, apkName, this)
}.catch {
emit(DownloadStatus.Error(it))
}.flowOn(Dispatchers.IO)
}
private suspend fun connectToDownload(
apkUrl: String, apkName: String, flow: FlowCollector<DownloadStatus>
) {
val con = URL(apkUrl).openConnection() as HttpURLConnection
con.apply {
requestMethod = "GET"
readTimeout = Constant.HTTP_TIME_OUT
connectTimeout = Constant.HTTP_TIME_OUT
setRequestProperty("Accept-Encoding", "identity")
}
if (con.responseCode == HttpURLConnection.HTTP_OK) {
val inStream = con.inputStream
val length = con.contentLength
var len: Int
var progress = 0
val buffer = ByteArray(1024 * 2)
val file = File(path, apkName)
FileOutputStream(file).use { out ->
while (inStream.read(buffer).also { len = it } != -1 && !shutdown) {
out.write(buffer, 0, len)
progress += len
flow.emit(DownloadStatus.Downloading(length, progress))
}
out.flush()
}
inStream.close()
if (shutdown) {
flow.emit(DownloadStatus.Cancel)
} else {
flow.emit(DownloadStatus.Done(file))
}
} else if (con.responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| con.responseCode == HttpURLConnection.HTTP_MOVED_TEMP
) {
con.disconnect()
val locationUrl = con.getHeaderField("Location")
LogUtil.d(
TAG,
"The current url is the redirect Url, the redirected url is $locationUrl"
)
connectToDownload(locationUrl, apkName, flow)
} else {
val e = SocketTimeoutException("Error: Http response code = ${con.responseCode}")
flow.emit(DownloadStatus.Error(e))
}
con.disconnect()
}
/**
* fix https url (SSLHandshakeException) exception
*/
private fun trustAllHosts() {
val manager: TrustManager = object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
LogUtil.d(TAG, "checkClientTrusted")
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
LogUtil.d(TAG, "checkServerTrusted")
}
}
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(manager), SecureRandom())
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} catch (e: Exception) {
LogUtil.e(TAG, "trustAllHosts error: $e")
}
}
override fun cancel() {
shutdown = true
}
override fun release() {
}
}
@@ -0,0 +1,188 @@
package com.azhon.appupdate.service
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.widget.Toast
import com.azhon.appupdate.R
import com.azhon.appupdate.base.bean.DownloadStatus
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.OnDownloadListener
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.manager.HttpDownloadManager
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.FileUtil
import com.azhon.appupdate.util.LogUtil
import com.azhon.appupdate.util.NotificationUtil
import kotlinx.coroutines.*
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.service
* FileName: DownloadService
* CreateDate: 2022/4/7 on 11:42
* Desc:
*
* @author azhon
*/
class DownloadService : Service(), OnDownloadListener {
companion object {
private const val TAG = "DownloadService"
}
private lateinit var manager: DownloadManager
private var lastProgress = 0
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
return START_NOT_STICKY
}
init()
return super.onStartCommand(intent, flags, startId)
}
private fun init() {
val tempManager = DownloadManager.getInstance()
if (tempManager == null) {
LogUtil.e(TAG, "An exception occurred by DownloadManager=null,please check your code!")
return
}
manager = tempManager
FileUtil.createDirDirectory(manager.downloadPath)
val enable = NotificationUtil.notificationEnable(this@DownloadService)
LogUtil.d(
TAG,
if (enable) "Notification switch status: opened" else " Notification switch status: closed"
)
if (checkApkMd5()) {
LogUtil.d(TAG, "Apk already exist and install it directly.")
//install apk
done(File(manager.downloadPath, manager.apkName))
} else {
LogUtil.d(TAG, "Apk don't exist will start download.")
download()
}
}
/**
* Check whether the Apk has been downloaded, don't download again
*/
private fun checkApkMd5(): Boolean {
val file = File(manager.downloadPath, manager.apkName)
if (file.exists()) {
return FileUtil.md5(file).equals(manager.apkMD5, ignoreCase = true)
}
return false
}
@Synchronized
private fun download() {
if (manager.downloadState) {
LogUtil.e(TAG, "Currently downloading, please download again!")
return
}
if (manager.httpManager == null) {
manager.httpManager = HttpDownloadManager(manager.downloadPath)
}
GlobalScope.launch(Dispatchers.Main + CoroutineName(Constant.COROUTINE_NAME)) {
manager.httpManager!!.download(manager.apkUrl, manager.apkName)
.collect {
when (it) {
is DownloadStatus.Start -> start()
is DownloadStatus.Downloading -> downloading(it.max, it.progress)
is DownloadStatus.Done -> done(it.apk)
is DownloadStatus.Cancel -> this@DownloadService.cancel()
is DownloadStatus.Error -> error(it.e)
}
}
}
manager.downloadState = true
}
override fun start() {
LogUtil.i(TAG, "download start")
if (manager.showBgdToast) {
Toast.makeText(this, R.string.background_downloading, Toast.LENGTH_SHORT).show()
}
if (manager.showNotification) {
NotificationUtil.showNotification(
this@DownloadService, manager.smallIcon,
resources.getString(R.string.start_download),
resources.getString(R.string.start_download_hint)
)
}
manager.onDownloadListeners.forEach { it.start() }
}
override fun downloading(max: Int, progress: Int) {
if (manager.showNotification) {
val curr = (progress / max.toDouble() * 100.0).toInt()
if (curr == lastProgress) return
LogUtil.i(TAG, "downloading max: $max --- progress: $progress")
lastProgress = curr
val content = if (curr < 0) "" else "$curr%"
NotificationUtil.showProgressNotification(
this@DownloadService, manager.smallIcon,
resources.getString(R.string.start_downloading),
content, if (max == -1) -1 else 100, curr
)
}
manager.onDownloadListeners.forEach { it.downloading(max, progress) }
}
override fun done(apk: File) {
LogUtil.d(TAG, "apk downloaded to ${apk.path}")
manager.downloadState = false
//If it is android Q (api=29) and above, (showNotification=false) will also send a
// download completion notification
if (manager.showNotification || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
NotificationUtil.showDoneNotification(
this@DownloadService, manager.smallIcon,
resources.getString(R.string.download_completed),
resources.getString(R.string.click_hint),
Constant.AUTHORITIES!!, apk
)
}
if (manager.jumpInstallPage) {
ApkUtil.installApk(this@DownloadService, Constant.AUTHORITIES!!, apk)
}
manager.onDownloadListeners.forEach { it.done(apk) }
// release objects
releaseResources()
}
override fun cancel() {
LogUtil.i(TAG, "download cancel")
manager.downloadState = false
if (manager.showNotification) {
NotificationUtil.cancelNotification(this@DownloadService)
}
manager.onDownloadListeners.forEach { it.cancel() }
}
override fun error(e: Throwable) {
LogUtil.e(TAG, "download error: $e")
manager.downloadState = false
if (manager.showNotification) {
NotificationUtil.showErrorNotification(
this@DownloadService, manager.smallIcon,
resources.getString(R.string.download_error),
resources.getString(R.string.continue_downloading),
)
}
manager.onDownloadListeners.forEach { it.error(e) }
}
private fun releaseResources() {
manager.httpManager?.release()
manager.release()
stopSelf()
}
}
@@ -0,0 +1,83 @@
package com.azhon.appupdate.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PatternMatcher
import androidx.core.content.FileProvider
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.util
* FileName: ApkUtil
* CreateDate: 2022/4/7 on 17:02
* Desc:
*
* @author azhon
*/
class ApkUtil {
companion object {
/**
* install package form file
*/
fun installApk(context: Context, authorities: String, apk: File) {
context.startActivity(createInstallIntent(context, authorities, apk))
}
fun createInstallIntent(context: Context, authorities: String, apk: File): Intent {
val intent = Intent().apply {
action = Intent.ACTION_VIEW
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addCategory(Intent.CATEGORY_DEFAULT)
}
val uri: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, authorities, apk)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
uri = Uri.fromFile(apk)
}
intent.setDataAndType(uri, "application/vnd.android.package-archive")
return intent
}
fun getVersionCode(context: Context): Long {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
return packageInfo.versionCode.toLong()
}
}
fun deleteOldApk(context: Context, oldApkPath: String): Boolean {
val curVersionCode = getVersionCode(context)
try {
val apk = File(oldApkPath)
if (apk.exists()) {
val oldVersionCode = getVersionCodeByPath(context, oldApkPath)
if (curVersionCode > oldVersionCode) {
return apk.delete()
}
}
} catch (e: Exception) {
}
return false
}
private fun getVersionCodeByPath(context: Context, path: String): Long {
val packageInfo =
context.packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo?.longVersionCode ?: 1
} else {
return packageInfo?.versionCode?.toLong() ?: 1
}
}
}
}
@@ -0,0 +1,23 @@
package com.azhon.appupdate.util
import android.content.Context
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.util
* FileName: DensityUtil
* CreateDate: 2022/4/7 on 17:52
* Desc:
*
* @author azhon
*/
class DensityUtil {
companion object {
fun dip2px(context: Context, dpValue: Float): Float {
val scale = context.resources.displayMetrics.density
return dpValue * scale + 0.5f
}
}
}
@@ -0,0 +1,46 @@
package com.azhon.appupdate.util
import java.io.File
import java.io.FileInputStream
import java.math.BigInteger
import java.security.MessageDigest
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.util
* FileName: FileUtil
* CreateDate: 2022/4/7 on 11:52
* Desc:
*
* @author azhon
*/
class FileUtil {
companion object {
fun createDirDirectory(path: String) {
File(path).let {
if (!it.exists()) {
it.mkdirs()
}
}
}
fun md5(file: File): String {
try {
val buffer = ByteArray(1024)
var len: Int
val digest = MessageDigest.getInstance("MD5")
val inStream = FileInputStream(file)
while (inStream.read(buffer).also { len = it } != -1) {
digest.update(buffer, 0, len)
}
inStream.close()
val bigInt = BigInteger(1, digest.digest())
return bigInt.toString(16).uppercase()
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
}
}
@@ -0,0 +1,39 @@
package com.azhon.appupdate.util
import android.util.Log
import com.azhon.appupdate.config.Constant
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.util
* FileName: LogUtil
* CreateDate: 2022/4/7 on 11:23
* Desc:
*
* @author azhon
*/
class LogUtil {
companion object {
var b = true
fun enable(enable: Boolean) {
b = enable
}
fun e(tag: String, msg: String) {
if (b) Log.e(Constant.TAG + tag, msg)
}
fun d(tag: String, msg: String) {
if (b) Log.d(Constant.TAG + tag, msg)
}
fun i(tag: String, msg: String) {
if (b) Log.i(Constant.TAG + tag, msg)
}
}
}
@@ -0,0 +1,163 @@
package com.azhon.appupdate.util
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.service.DownloadService
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.util
* FileName: NotificationUtil
* CreateDate: 2022/4/7 on 13:36
* Desc:
*
* @author azhon
*/
class NotificationUtil {
companion object {
fun notificationEnable(context: Context): Boolean {
return NotificationManagerCompat.from(context).areNotificationsEnabled()
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun getNotificationChannelId(): String {
val channel = DownloadManager.getInstance()?.notificationChannel
return if (channel == null) {
Constant.DEFAULT_CHANNEL_ID
} else {
channel.id
}
}
private fun builderNotification(
context: Context, icon: Int, title: String, content: String
): NotificationCompat.Builder {
var channelId = ""
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channelId = getNotificationChannelId()
}
return NotificationCompat.Builder(context, channelId)
.setSmallIcon(icon)
.setContentTitle(title)
.setWhen(System.currentTimeMillis())
.setContentText(content)
.setAutoCancel(false)
.setOngoing(true)
}
fun showNotification(context: Context, icon: Int, title: String, content: String) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
afterO(manager)
}
val notify = builderNotification(context, icon, title, content)
.setDefaults(Notification.DEFAULT_SOUND)
.build()
manager.notify(
DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID,
notify
)
}
/**
* send a downloading Notification
*/
fun showProgressNotification(
context: Context, icon: Int, title: String, content: String, max: Int, progress: Int
) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notify = builderNotification(context, icon, title, content)
.setProgress(max, progress, max == -1).build()
manager.notify(
DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID,
notify
)
}
/**
* send a downloaded Notification
*/
fun showDoneNotification(
context: Context, icon: Int, title: String, content: String,
authorities: String, apk: File
) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID)
val intent = ApkUtil.createInstallIntent(context, authorities, apk)
val pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notify = builderNotification(context, icon, title, content)
.setContentIntent(pi)
.build()
notify.flags = notify.flags or Notification.FLAG_AUTO_CANCEL
manager.notify(
DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID,
notify
)
}
/**
* send a error Notification
*/
fun showErrorNotification(
context: Context, icon: Int, title: String, content: String
) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
afterO(manager)
}
val intent = Intent(context, DownloadService::class.java)
val pi = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notify = builderNotification(context, icon, title, content)
.setAutoCancel(true)
.setOngoing(false)
.setContentIntent(pi)
.setDefaults(Notification.DEFAULT_SOUND)
.build()
manager.notify(
DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID,
notify
)
}
/**
* cancel Notification by id
*/
fun cancelNotification(context: Context) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID)
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun afterO(manager: NotificationManager) {
var channel = DownloadManager.getInstance()?.notificationChannel
if (channel == null) {
channel = NotificationChannel(
Constant.DEFAULT_CHANNEL_ID, Constant.DEFAULT_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
enableLights(true)
setShowBadge(true)
}
}
manager.createNotificationChannel(channel)
}
}
}
@@ -0,0 +1,479 @@
package com.azhon.appupdate.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import com.azhon.appupdate.R;
/**
* Created by daimajia on 14-4-30.
* <a href="https://github.com/daimajia/NumberProgressBar/}"/>
*/
public class NumberProgressBar extends View {
private int mMaxProgress = 100;
/**
* Current progress, can not exceed the max progress.
*/
private int mCurrentProgress = 0;
/**
* The progress area bar color.
*/
private int mReachedBarColor;
/**
* The bar unreached area color.
*/
private int mUnreachedBarColor;
/**
* The progress text color.
*/
private int mTextColor;
/**
* The progress text size.
*/
private float mTextSize;
/**
* The height of the reached area.
*/
private float mReachedBarHeight;
/**
* The height of the unreached area.
*/
private float mUnreachedBarHeight;
/**
* The suffix of the number.
*/
private String mSuffix = "%";
/**
* The prefix.
*/
private String mPrefix = "";
private final int default_text_color = Color.rgb(255, 137, 91);
private final int default_reached_color = Color.rgb(255, 137, 91);
private final int default_unreached_color = Color.rgb(204, 204, 204);
private final float default_progress_text_offset;
private final float default_text_size;
/**
* For save and restore instance of progressbar.
*/
private static final String INSTANCE_STATE = "saved_instance";
private static final String INSTANCE_TEXT_COLOR = "text_color";
private static final String INSTANCE_TEXT_SIZE = "text_size";
private static final String INSTANCE_REACHED_BAR_HEIGHT = "reached_bar_height";
private static final String INSTANCE_REACHED_BAR_COLOR = "reached_bar_color";
private static final String INSTANCE_UNREACHED_BAR_HEIGHT = "unreached_bar_height";
private static final String INSTANCE_UNREACHED_BAR_COLOR = "unreached_bar_color";
private static final String INSTANCE_MAX = "max";
private static final String INSTANCE_PROGRESS = "progress";
private static final String INSTANCE_SUFFIX = "suffix";
private static final String INSTANCE_PREFIX = "prefix";
private static final String INSTANCE_TEXT_VISIBILITY = "text_visibility";
private static final int PROGRESS_TEXT_VISIBLE = 0;
/**
* The width of the text that to be drawn.
*/
private float mDrawTextWidth;
/**
* The drawn text start.
*/
private float mDrawTextStart;
/**
* The drawn text end.
*/
private float mDrawTextEnd;
/**
* The text that to be drawn in onDraw().
*/
private String mCurrentDrawText;
/**
* The Paint of the reached area.
*/
private Paint mReachedBarPaint;
/**
* The Paint of the unreached area.
*/
private Paint mUnreachedBarPaint;
/**
* The Paint of the progress text.
*/
private Paint mTextPaint;
/**
* Unreached bar area to draw rect.
*/
private RectF mUnreachedRectF = new RectF(0, 0, 0, 0);
/**
* Reached bar area rect.
*/
private RectF mReachedRectF = new RectF(0, 0, 0, 0);
/**
* The progress text offset.
*/
private float mOffset;
/**
* Determine if need to draw unreached area.
*/
private boolean mDrawUnreachedBar = true;
private boolean mDrawReachedBar = true;
private boolean mIfDrawText = true;
public enum ProgressTextVisibility {
Visible, Invisible
}
public NumberProgressBar(Context context) {
this(context, null);
}
public NumberProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mReachedBarHeight = dp2px(1.5f);
mUnreachedBarHeight = dp2px(1.0f);
default_text_size = sp2px(10);
default_progress_text_offset = dp2px(3.0f);
//load styled attributes.
final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar,
defStyleAttr, 0);
mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color);
mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color);
mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color);
mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size);
attributes.recycle();
initializePainters();
}
@Override
protected int getSuggestedMinimumWidth() {
return (int) mTextSize;
}
@Override
protected int getSuggestedMinimumHeight() {
return Math.max((int) mTextSize, Math.max((int) mReachedBarHeight, (int) mUnreachedBarHeight));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
}
private int measure(int measureSpec, boolean isWidth) {
int result;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
result += padding;
if (mode == MeasureSpec.AT_MOST) {
if (isWidth) {
result = Math.max(result, size);
} else {
result = Math.min(result, size);
}
}
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
if (mIfDrawText) {
calculateDrawRectF();
} else {
calculateDrawRectFWithoutProgressText();
}
if (mDrawReachedBar) {
canvas.drawRect(mReachedRectF, mReachedBarPaint);
}
if (mDrawUnreachedBar) {
canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint);
}
if (mIfDrawText)
canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint);
}
private void initializePainters() {
mReachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mReachedBarPaint.setColor(mReachedBarColor);
mUnreachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mUnreachedBarPaint.setColor(mUnreachedBarColor);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
}
private void calculateDrawRectFWithoutProgressText() {
mReachedRectF.left = getPaddingLeft();
mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft();
mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;
mUnreachedRectF.left = mReachedRectF.right;
mUnreachedRectF.right = getWidth() - getPaddingRight();
mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
}
private void calculateDrawRectF() {
mCurrentDrawText = String.format("%d", getProgress() * 100 / getMax());
mCurrentDrawText = mPrefix + mCurrentDrawText + mSuffix;
mDrawTextWidth = mTextPaint.measureText(mCurrentDrawText);
if (getProgress() == 0) {
mDrawReachedBar = false;
mDrawTextStart = getPaddingLeft();
} else {
mDrawReachedBar = true;
mReachedRectF.left = getPaddingLeft();
mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() - mOffset + getPaddingLeft();
mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;
mDrawTextStart = (mReachedRectF.right + mOffset);
}
mDrawTextEnd = (int) ((getHeight() / 2.0f) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
if ((mDrawTextStart + mDrawTextWidth) >= getWidth() - getPaddingRight()) {
mDrawTextStart = getWidth() - getPaddingRight() - mDrawTextWidth;
mReachedRectF.right = mDrawTextStart - mOffset;
}
float unreachedBarStart = mDrawTextStart + mDrawTextWidth + mOffset;
if (unreachedBarStart >= getWidth() - getPaddingRight()) {
mDrawUnreachedBar = false;
} else {
mDrawUnreachedBar = true;
mUnreachedRectF.left = unreachedBarStart;
mUnreachedRectF.right = getWidth() - getPaddingRight();
mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
}
}
/**
* Get progress text color.
*
* @return progress text color.
*/
public int getTextColor() {
return mTextColor;
}
/**
* Get progress text size.
*
* @return progress text size.
*/
public float getProgressTextSize() {
return mTextSize;
}
public int getUnreachedBarColor() {
return mUnreachedBarColor;
}
public int getReachedBarColor() {
return mReachedBarColor;
}
public int getProgress() {
return mCurrentProgress;
}
public int getMax() {
return mMaxProgress;
}
public float getReachedBarHeight() {
return mReachedBarHeight;
}
public float getUnreachedBarHeight() {
return mUnreachedBarHeight;
}
public void setProgressTextSize(float textSize) {
this.mTextSize = textSize;
mTextPaint.setTextSize(mTextSize);
invalidate();
}
public void setProgressTextColor(int textColor) {
this.mTextColor = textColor;
mTextPaint.setColor(mTextColor);
invalidate();
}
public void setUnreachedBarColor(int barColor) {
this.mUnreachedBarColor = barColor;
mUnreachedBarPaint.setColor(mUnreachedBarColor);
invalidate();
}
public void setReachedBarColor(int progressColor) {
this.mReachedBarColor = progressColor;
mReachedBarPaint.setColor(mReachedBarColor);
invalidate();
}
public void setReachedBarHeight(float height) {
mReachedBarHeight = height;
}
public void setUnreachedBarHeight(float height) {
mUnreachedBarHeight = height;
}
public void setMax(int maxProgress) {
if (maxProgress > 0) {
this.mMaxProgress = maxProgress;
invalidate();
}
}
public void setSuffix(String suffix) {
if (suffix == null) {
mSuffix = "";
} else {
mSuffix = suffix;
}
}
public String getSuffix() {
return mSuffix;
}
public void setPrefix(String prefix) {
if (prefix == null)
mPrefix = "";
else {
mPrefix = prefix;
}
}
public String getPrefix() {
return mPrefix;
}
public void incrementProgressBy(int by) {
if (by > 0) {
setProgress(getProgress() + by);
}
}
public void setProgress(int progress) {
if (progress <= getMax() && progress >= 0) {
this.mCurrentProgress = progress;
invalidate();
}
}
@Override
protected Parcelable onSaveInstanceState() {
final Bundle bundle = new Bundle();
bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState());
bundle.putInt(INSTANCE_TEXT_COLOR, getTextColor());
bundle.putFloat(INSTANCE_TEXT_SIZE, getProgressTextSize());
bundle.putFloat(INSTANCE_REACHED_BAR_HEIGHT, getReachedBarHeight());
bundle.putFloat(INSTANCE_UNREACHED_BAR_HEIGHT, getUnreachedBarHeight());
bundle.putInt(INSTANCE_REACHED_BAR_COLOR, getReachedBarColor());
bundle.putInt(INSTANCE_UNREACHED_BAR_COLOR, getUnreachedBarColor());
bundle.putInt(INSTANCE_MAX, getMax());
bundle.putInt(INSTANCE_PROGRESS, getProgress());
bundle.putString(INSTANCE_SUFFIX, getSuffix());
bundle.putString(INSTANCE_PREFIX, getPrefix());
bundle.putBoolean(INSTANCE_TEXT_VISIBILITY, getProgressTextVisibility());
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle bundle = (Bundle) state;
mTextColor = bundle.getInt(INSTANCE_TEXT_COLOR);
mTextSize = bundle.getFloat(INSTANCE_TEXT_SIZE);
mReachedBarHeight = bundle.getFloat(INSTANCE_REACHED_BAR_HEIGHT);
mUnreachedBarHeight = bundle.getFloat(INSTANCE_UNREACHED_BAR_HEIGHT);
mReachedBarColor = bundle.getInt(INSTANCE_REACHED_BAR_COLOR);
mUnreachedBarColor = bundle.getInt(INSTANCE_UNREACHED_BAR_COLOR);
initializePainters();
setMax(bundle.getInt(INSTANCE_MAX));
setProgress(bundle.getInt(INSTANCE_PROGRESS));
setPrefix(bundle.getString(INSTANCE_PREFIX));
setSuffix(bundle.getString(INSTANCE_SUFFIX));
setProgressTextVisibility(bundle.getBoolean(INSTANCE_TEXT_VISIBILITY) ? ProgressTextVisibility.Visible : ProgressTextVisibility.Invisible);
super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE));
return;
}
super.onRestoreInstanceState(state);
}
public float dp2px(float dp) {
final float scale = getResources().getDisplayMetrics().density;
return dp * scale + 0.5f;
}
public float sp2px(float sp) {
final float scale = getResources().getDisplayMetrics().scaledDensity;
return sp * scale;
}
public void setProgressTextVisibility(ProgressTextVisibility visibility) {
mIfDrawText = visibility == ProgressTextVisibility.Visible;
invalidate();
}
public boolean getProgressTextVisibility() {
return mIfDrawText;
}
}
@@ -0,0 +1,183 @@
package com.azhon.appupdate.view
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.azhon.appupdate.R
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.OnButtonClickListener
import com.azhon.appupdate.listener.OnDownloadListenerAdapter
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.service.DownloadService
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.DensityUtil
import com.azhon.appupdate.util.LogUtil
import java.io.File
/**
* ProjectName: AppUpdate
* PackageName: com.azhon.appupdate.view
* FileName: UpdateDialogActivity
* CreateDate: 2022/4/7 on 17:40
* Desc:
*
* @author azhon
*/
class UpdateDialogActivity : AppCompatActivity(), View.OnClickListener {
private val install = 0x45
private lateinit var manager: DownloadManager
private lateinit var apk: File
private lateinit var progressBar: NumberProgressBar
private lateinit var btnUpdate: Button
companion object {
private const val TAG = "UpdateDialogActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
overridePendingTransition(0, 0)
title = ""
setContentView(R.layout.dialog_update)
init()
}
private fun init() {
val tempManager = DownloadManager.getInstance()
if (tempManager == null) {
LogUtil.e(TAG, "An exception occurred by DownloadManager=null,please check your code!")
return
}
manager = tempManager
manager.onDownloadListeners.add(listenerAdapter)
setWindowSize()
initView()
}
private fun initView() {
val ibClose = findViewById<View>(R.id.ib_close)
val vLine = findViewById<View>(R.id.line)
val ivBg = findViewById<ImageView>(R.id.iv_bg)
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvSize = findViewById<TextView>(R.id.tv_size)
val tvDescription = findViewById<TextView>(R.id.tv_description)
progressBar = findViewById(R.id.np_bar)
btnUpdate = findViewById(R.id.btn_update)
progressBar.visibility = View.VISIBLE
btnUpdate.tag = 0
btnUpdate.setOnClickListener(this)
ibClose.setOnClickListener(this)
if (manager.dialogImage != -1) {
ivBg.setBackgroundResource(manager.dialogImage)
}
if (manager.dialogButtonTextColor != -1) {
btnUpdate.setTextColor(manager.dialogButtonTextColor)
}
if (manager.dialogProgressBarColor != -1) {
progressBar.reachedBarColor = manager.dialogProgressBarColor
progressBar.setProgressTextColor(manager.dialogProgressBarColor)
}
if (manager.dialogButtonColor != -1) {
val colorDrawable = GradientDrawable().apply {
setColor(manager.dialogButtonColor)
cornerRadius = DensityUtil.dip2px(this@UpdateDialogActivity, 3f)
}
val drawable = StateListDrawable().apply {
addState(intArrayOf(android.R.attr.state_pressed), colorDrawable)
addState(IntArray(0), colorDrawable)
}
btnUpdate.background = drawable
}
if (manager.apkVersionName.isNotEmpty()) {
tvTitle.text =
String.format(resources.getString(R.string.dialog_new), manager.apkVersionName)
}
if (manager.apkSize.isNotEmpty()) {
tvSize.text =
String.format(resources.getString(R.string.dialog_new_size), manager.apkSize)
tvSize.visibility = View.VISIBLE
}
tvDescription.text = manager.apkDescription
}
private fun setWindowSize() {
val attributes = window.attributes
attributes.width = (resources.displayMetrics.widthPixels * 0.75f).toInt()
attributes.height = WindowManager.LayoutParams.WRAP_CONTENT
attributes.gravity = Gravity.CENTER
window.attributes = attributes
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.ib_close -> {
if (!manager.forcedUpgrade) {
finish()
}
manager.onButtonClickListener?.onButtonClick(OnButtonClickListener.CANCEL)
}
R.id.btn_update -> {
if (btnUpdate.tag == install) {
ApkUtil.installApk(this, Constant.AUTHORITIES!!, apk)
return
}
if (!manager.forcedUpgrade) {
btnUpdate.isEnabled = false
btnUpdate.text = resources.getString(R.string.background_downloading)
} else {
finish()
}
manager.onButtonClickListener?.onButtonClick(OnButtonClickListener.UPDATE)
startService(Intent(this, DownloadService::class.java))
}
}
}
override fun onBackPressed() {
if (!manager.forcedUpgrade) return
super.onBackPressed()
}
override fun finish() {
super.finish()
overridePendingTransition(0, 0)
}
private val listenerAdapter: OnDownloadListenerAdapter = object : OnDownloadListenerAdapter() {
override fun downloading(max: Int, progress: Int) {
if (max != -1 && progressBar.visibility == View.VISIBLE) {
val curr = (progress / max.toDouble() * 100.0).toInt()
progressBar.progress = curr
} else {
progressBar.visibility = View.GONE
}
}
override fun done(apk: File) {
this@UpdateDialogActivity.apk = apk
if (!manager.forcedUpgrade) {
btnUpdate.tag = install
btnUpdate.isEnabled = true
btnUpdate.text = resources.getString(R.string.click_hint)
}
}
}
override fun onDestroy() {
super.onDestroy()
manager.onDownloadListeners.remove(listenerAdapter)
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<corners android:radius="3dp" />
<solid android:color="#ff895b" />
</shape>
</item>
<item android:state_pressed="false">
<shape>
<corners android:radius="3dp" />
<solid android:color="#ff895b" />
</shape>
</item>
</selector>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:bottomLeftRadius="6dp"
android:bottomRightRadius="6dp" />
<solid android:color="@android:color/white" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_bg"
android:layout_width="match_parent"
android:layout_height="140dp"
android:background="@drawable/ic_dialog_default" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_6"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textColor="@android:color/black"
android:textSize="15sp"
tools:text="发现新版v2.0.1可以下载啦!" />
<TextView
android:id="@+id/tv_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:textColor="#757575"
android:textSize="14sp"
android:visibility="gone"
tools:text="新版本大小:5M" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:overScrollMode="never">
<TextView
android:id="@+id/tv_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:textColor="#757575"
android:textSize="14sp"
tools:text="" />
</ScrollView>
<com.azhon.appupdate.view.NumberProgressBar
android:id="@+id/np_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp" />
<Button
android:id="@+id/btn_update"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/bg_button"
android:text="@string/update"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="2dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@android:color/white" />
<ImageButton
android:id="@+id/ib_close"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@drawable/ic_dialog_close" />
</LinearLayout>
@@ -0,0 +1,15 @@
<resources>
<string name="latest_version">Hiện tại bạn đang ở bản mới nhất!</string>
<string name="start_download">Bắt đầu tải</string>
<string name="start_download_hint">Bạn có thể kiểm tra tiến trình tải sau ít phút!</string>
<string name="start_downloading">Tải phiên bản mới</string>
<string name="download_completed">Đã tải xong</string>
<string name="click_hint">Nhấn đề cài đặt</string>
<string name="download_error">Tải bị lỗi</string>
<string name="continue_downloading">Nhấn để tiếp tục tải</string>
<string name="background_downloading">Ứng dụng đang được tải ở dưới nền!</string>
<string name="dialog_new">Phiên bản v%s đã ra mắt! Hãy cập nhật ngay!</string>
<string name="dialog_new_size">Dung lượng tệp%sMB</string>
<string name="update">Cập nhật</string>
</resources>
@@ -0,0 +1,14 @@
<resources>
<string name="latest_version">當前已是最新版本!</string>
<string name="start_download">開始下載</string>
<string name="start_download_hint">可稍後查看下載進度</string>
<string name="start_downloading">正在下載新版本</string>
<string name="download_completed">下載完成</string>
<string name="click_hint">點擊進行安裝</string>
<string name="download_error">下載出錯</string>
<string name="continue_downloading">點擊繼續下載</string>
<string name="background_downloading">正在後臺下載新版本…</string>
<string name="dialog_new">發現新版本v%s可以下載啦!</string>
<string name="dialog_new_size">新版本大小:%sM</string>
<string name="update">升級</string>
</resources>
@@ -0,0 +1,14 @@
<resources>
<string name="latest_version">当前已是最新版本!</string>
<string name="start_download">开始下载</string>
<string name="start_download_hint">可稍后查看下载进度</string>
<string name="start_downloading">正在下载新版本</string>
<string name="download_completed">下载完成</string>
<string name="click_hint">点击进行安装</string>
<string name="download_error">下载出错</string>
<string name="continue_downloading">点击继续下载</string>
<string name="background_downloading">正在后台下载新版本…</string>
<string name="dialog_new">发现新版本v%s可以下载啦!</string>
<string name="dialog_new_size">新版本大小:%sM</string>
<string name="update">升级</string>
</resources>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialogActivity" parent="Theme.AppCompat">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="UpdateDialog" parent="DialogActivity">
<item name="android:backgroundDimEnabled">true</item>
</style>
<declare-styleable name="NumberProgressBar">
<attr name="progress_unreached_color" format="color" />
<attr name="progress_reached_color" format="color" />
<attr name="progress_text_size" format="dimension" />
<attr name="progress_text_color" format="color" />
</declare-styleable>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="app_update_external"
path="/" />
<external-cache-path
name="app_update_cache"
path="/" />
</paths>
@@ -0,0 +1,17 @@
package com.azhon.appupdate
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+3
View File
@@ -1,17 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.7.10'
repositories { repositories {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
plugins { plugins {
id 'com.android.application' version '7.2.1' apply false id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false id 'com.android.library' version '7.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
} }
allprojects { allprojects {
+1
View File
@@ -8,3 +8,4 @@ pluginManagement {
rootProject.name = "Foodify" rootProject.name = "Foodify"
include ':app' include ':app'
include ':appupdate'
BIN
View File
Binary file not shown.