parent
35d7360f08
commit
2ab637234c
@ -1,137 +0,0 @@ |
|||||||
package com.android.base.utils.upgrade; |
|
||||||
|
|
||||||
import com.android.base.utils.common.Files; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
import java.io.FileOutputStream; |
|
||||||
import java.io.IOException; |
|
||||||
import java.io.InputStream; |
|
||||||
|
|
||||||
import okhttp3.OkHttpClient; |
|
||||||
import okhttp3.Request; |
|
||||||
import okhttp3.Response; |
|
||||||
import okhttp3.ResponseBody; |
|
||||||
|
|
||||||
import static com.blankj.utilcode.util.CloseUtils.closeIOQuietly; |
|
||||||
|
|
||||||
/** |
|
||||||
* 下载APK |
|
||||||
* |
|
||||||
* @author Ztiany |
|
||||||
* Email: 1169654504@qq.com |
|
||||||
* Date : 2017-01-04 10:40 |
|
||||||
*/ |
|
||||||
final class ApkDownloader { |
|
||||||
|
|
||||||
private String mUrl; |
|
||||||
private String mDigitalAbstract; |
|
||||||
private ApkDownloaderListener mApkDownloaderListener; |
|
||||||
private File mDesFile; |
|
||||||
private File mTempDesFile; |
|
||||||
private static final String TEMP_MASK = "temp_%s"; |
|
||||||
private long mNotifyTime = 0; |
|
||||||
|
|
||||||
|
|
||||||
ApkDownloader(String url, String desFilePath, String digitalAbstract, ApkDownloaderListener apkDownloaderListener) { |
|
||||||
mUrl = url; |
|
||||||
mDigitalAbstract = digitalAbstract; |
|
||||||
mApkDownloaderListener = apkDownloaderListener; |
|
||||||
mDesFile = new File(desFilePath); |
|
||||||
String name = mDesFile.getName(); |
|
||||||
mTempDesFile = new File(mDesFile.getParent(), String.format(TEMP_MASK, name)); |
|
||||||
} |
|
||||||
|
|
||||||
void start() { |
|
||||||
OkHttpClient.Builder builder = new OkHttpClient.Builder(); |
|
||||||
builder.interceptors().clear(); |
|
||||||
OkHttpClient okHttpClient = builder.build(); |
|
||||||
|
|
||||||
Request request = new Request.Builder() |
|
||||||
.url(mUrl) |
|
||||||
.build(); |
|
||||||
|
|
||||||
ResponseBody body; |
|
||||||
|
|
||||||
try { |
|
||||||
Response response = okHttpClient.newCall(request).execute(); |
|
||||||
if (response.isSuccessful()) { |
|
||||||
body = response.body(); |
|
||||||
} else { |
|
||||||
mApkDownloaderListener.onFail(new RuntimeException("网络错误")); |
|
||||||
return; |
|
||||||
} |
|
||||||
} catch (IOException e) { |
|
||||||
e.printStackTrace(); |
|
||||||
mApkDownloaderListener.onFail(e); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
//检查body
|
|
||||||
if (body == null) { |
|
||||||
mApkDownloaderListener.onFail(new RuntimeException("网络错误")); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (mDesFile.exists() && mDesFile.length() == body.contentLength() && AppUpgradeChecker.upgradeInteractor.checkApkFile(mDesFile, mDigitalAbstract)) { |
|
||||||
mApkDownloaderListener.onSuccess(mDesFile); |
|
||||||
} else { |
|
||||||
Files.makeParentPath(mDesFile); |
|
||||||
Files.makeParentPath(mTempDesFile); |
|
||||||
boolean success = startDownload(body); |
|
||||||
if (success) { |
|
||||||
boolean renameToSuccess = mTempDesFile.renameTo(mDesFile); |
|
||||||
if (renameToSuccess) { |
|
||||||
if (AppUpgradeChecker.upgradeInteractor.checkApkFile(mDesFile, mDigitalAbstract)) { |
|
||||||
mApkDownloaderListener.onSuccess(mDesFile); |
|
||||||
} else { |
|
||||||
mApkDownloaderListener.onFail(new RuntimeException("数字摘要对比失败")); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private boolean startDownload(ResponseBody body) { |
|
||||||
InputStream is = null; |
|
||||||
FileOutputStream fos = null; |
|
||||||
try { |
|
||||||
byte[] buf = new byte[1024 * 2];//缓冲器
|
|
||||||
int len;//每次读取的长度
|
|
||||||
assert body != null; |
|
||||||
final long total = body.contentLength();//总的长度
|
|
||||||
is = body.byteStream(); |
|
||||||
long sum = 0; |
|
||||||
fos = new FileOutputStream(mTempDesFile); |
|
||||||
|
|
||||||
while ((len = is.read(buf)) != -1) { |
|
||||||
sum += len; |
|
||||||
fos.write(buf, 0, len); |
|
||||||
if ((System.currentTimeMillis() - mNotifyTime) >= 300) { |
|
||||||
mNotifyTime = System.currentTimeMillis(); |
|
||||||
mApkDownloaderListener.onProgress(total, sum); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
mApkDownloaderListener.onProgress(total, total); |
|
||||||
fos.flush(); |
|
||||||
return true; |
|
||||||
} catch (IOException e) { |
|
||||||
e.printStackTrace(); |
|
||||||
mApkDownloaderListener.onFail(e); |
|
||||||
return false; |
|
||||||
} finally { |
|
||||||
closeIOQuietly(is, fos); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
interface ApkDownloaderListener { |
|
||||||
|
|
||||||
void onProgress(long total, long progress); |
|
||||||
|
|
||||||
void onSuccess(File desFile); |
|
||||||
|
|
||||||
void onFail(Exception e); |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,228 +0,0 @@ |
|||||||
package com.android.base.utils.upgrade |
|
||||||
|
|
||||||
import android.app.Activity |
|
||||||
import android.content.BroadcastReceiver |
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.content.IntentFilter |
|
||||||
import androidx.lifecycle.LiveData |
|
||||||
import androidx.lifecycle.MutableLiveData |
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager |
|
||||||
import com.android.base.app.Sword |
|
||||||
import com.android.base.data.State |
|
||||||
import com.android.base.rx.observeOnUI |
|
||||||
import com.android.base.utils.BaseUtils |
|
||||||
import com.android.base.utils.android.XAppUtils |
|
||||||
import com.app.base.upgrade.UpgradeInteractor |
|
||||||
import com.blankj.utilcode.util.AppUtils |
|
||||||
import com.blankj.utilcode.util.Utils |
|
||||||
import timber.log.Timber |
|
||||||
import java.io.File |
|
||||||
|
|
||||||
/** |
|
||||||
* A tool for checking app upgrade. |
|
||||||
* |
|
||||||
* usage, when automatic check new version: |
|
||||||
* |
|
||||||
* ``` |
|
||||||
* AppUpgradeChecker.checkAppUpgrade() |
|
||||||
* ``` |
|
||||||
* |
|
||||||
* usage, when click to check new version: |
|
||||||
* ``` |
|
||||||
* if (AppUpgradeChecker.isDownloading) { |
|
||||||
* showMessage("正在下载更新") |
|
||||||
* return@setOnClickListener |
|
||||||
* } |
|
||||||
* |
|
||||||
* AppUpgradeChecker.checkAppUpgrade(false) |
|
||||||
* |
|
||||||
* if (!subscribed) { |
|
||||||
* subscribeUpgradeInfo() |
|
||||||
* subscribed = true |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
object AppUpgradeChecker { |
|
||||||
|
|
||||||
private var successOnce = false |
|
||||||
|
|
||||||
var isDownloading = false |
|
||||||
private set |
|
||||||
|
|
||||||
val isChecking: Boolean |
|
||||||
get() = currentState.isLoading |
|
||||||
|
|
||||||
private var currentState = State.noChange<UpgradeInfo>() |
|
||||||
set(value) { |
|
||||||
field = value |
|
||||||
notifyStateChanged(value) |
|
||||||
} |
|
||||||
|
|
||||||
private fun notifyStateChanged(state: State<UpgradeInfo>) { |
|
||||||
innerLiveState.value = state |
|
||||||
} |
|
||||||
|
|
||||||
private val innerLiveState = MutableLiveData<State<UpgradeInfo>>() |
|
||||||
|
|
||||||
val upgradeState: LiveData<State<UpgradeInfo>> |
|
||||||
get() = innerLiveState |
|
||||||
|
|
||||||
private var resultReceiver: ResultBroadcastReceiver? = null |
|
||||||
|
|
||||||
private val isUpgradeServiceRunning: Boolean |
|
||||||
get() = XAppUtils.isServiceRunning(BaseUtils.getAppContext(), UpgradeService::class.java.name) |
|
||||||
|
|
||||||
private lateinit var upgradeInfo: UpgradeInfo |
|
||||||
|
|
||||||
lateinit var upgradeInteractor: UpgradeInteractor |
|
||||||
|
|
||||||
fun installInteractor(upgradeInteractor: UpgradeInteractor) { |
|
||||||
AppUpgradeChecker.upgradeInteractor = upgradeInteractor |
|
||||||
} |
|
||||||
|
|
||||||
fun checkAppUpgrade(silence: Boolean = true) { |
|
||||||
Timber.d("checkAppUpgrade-->currentState == $currentState") |
|
||||||
/*正在检查*/ |
|
||||||
if (currentState.isLoading) { |
|
||||||
return |
|
||||||
} |
|
||||||
/*已经检查过了*/ |
|
||||||
if (silence && successOnce) { |
|
||||||
return |
|
||||||
} |
|
||||||
/*正在下载*/ |
|
||||||
if (isUpgradeServiceRunning) { |
|
||||||
return |
|
||||||
} |
|
||||||
realCheck() |
|
||||||
} |
|
||||||
|
|
||||||
private fun realCheck() { |
|
||||||
currentState = State.loading() |
|
||||||
|
|
||||||
upgradeInteractor.checkUpgrade() |
|
||||||
.observeOnUI() |
|
||||||
.subscribe( |
|
||||||
{ upgradeInfo -> |
|
||||||
successOnce = true |
|
||||||
processUpdateInfo(upgradeInfo) |
|
||||||
}, |
|
||||||
{ |
|
||||||
currentState = State.error(it) |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
private fun processUpdateInfo(upgradeInfo: UpgradeInfo) { |
|
||||||
currentState = State.success(upgradeInfo) |
|
||||||
if (upgradeInfo.isNewVersion) { |
|
||||||
AppUpgradeChecker.upgradeInfo = upgradeInfo |
|
||||||
safeContext { |
|
||||||
upgradeInteractor.showUpgradeDialog(it, upgradeInfo, |
|
||||||
onCancel = { |
|
||||||
//do nothing |
|
||||||
}, |
|
||||||
onConfirm = { |
|
||||||
doUpdate() |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun doUpdate() { |
|
||||||
//show loading dialog |
|
||||||
safeContext { |
|
||||||
upgradeInteractor.showDownloadingDialog(it, upgradeInfo.isForce) |
|
||||||
} |
|
||||||
//register result receiver |
|
||||||
var receiver = resultReceiver |
|
||||||
if (receiver == null) { |
|
||||||
receiver = ResultBroadcastReceiver() |
|
||||||
resultReceiver = receiver |
|
||||||
LocalBroadcastManager.getInstance(BaseUtils.getAppContext()).registerReceiver(receiver, IntentFilter(UpgradeService.DOWNLOAD_APK_RESULT_ACTION)) |
|
||||||
} |
|
||||||
//start downloading |
|
||||||
isDownloading = true |
|
||||||
UpgradeService.start(BaseUtils.getAppContext(), upgradeInfo.downloadUrl, upgradeInfo.versionName, upgradeInfo.digitalAbstract, upgradeInfo.isForce) |
|
||||||
} |
|
||||||
|
|
||||||
private class ResultBroadcastReceiver : BroadcastReceiver() { |
|
||||||
override fun onReceive(context: Context, intent: Intent) { |
|
||||||
if (intent.action != UpgradeService.DOWNLOAD_APK_RESULT_ACTION) { |
|
||||||
return |
|
||||||
} |
|
||||||
when (intent.getIntExtra(UpgradeService.NOTIFY_TYPE_KEY, -1)) { |
|
||||||
UpgradeService.NOTIFY_TYPE_SUCCESS -> processOnDownloadingFileSuccessful(intent) |
|
||||||
UpgradeService.NOTIFY_TYPE_FAILED -> processOnDownloadingFileFailed() |
|
||||||
UpgradeService.NOTIFY_TYPE_PROGRESS -> { |
|
||||||
val total = intent.getLongExtra(UpgradeService.TOTAL_PROGRESS_KEY, 0) |
|
||||||
val progress = intent.getLongExtra(UpgradeService.PROGRESS_KEY, 0) |
|
||||||
upgradeInteractor.onProgress(total, progress) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun processOnDownloadingFileSuccessful(intent: Intent) { |
|
||||||
isDownloading = false |
|
||||||
//start installing |
|
||||||
val apkFile = intent.getSerializableExtra(UpgradeService.APK_FILE_KEY) as File |
|
||||||
startInstall(apkFile) |
|
||||||
//dismiss download dialog |
|
||||||
upgradeInteractor.dismissDownloadingDialog() |
|
||||||
// if it is force upgrade, we show a no cancelable dialog to make user have to install the new apk. |
|
||||||
safeContext { |
|
||||||
showInstallTipsDialog(it, apkFile) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun showInstallTipsDialog(topActivity: Activity, apkFile: File) { |
|
||||||
upgradeInteractor.showInstallTipsDialog(topActivity, upgradeInfo.isForce, |
|
||||||
onCancel = { |
|
||||||
currentState = State.success() |
|
||||||
}, |
|
||||||
onConfirm = { |
|
||||||
startInstall(apkFile) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
private fun processOnDownloadingFileFailed() { |
|
||||||
isDownloading = false |
|
||||||
upgradeInteractor.dismissDownloadingDialog() |
|
||||||
safeContext { |
|
||||||
showDownloadingFailedTips(it, upgradeInfo.isForce) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun showDownloadingFailedTips(safeContext: Activity, isForce: Boolean) { |
|
||||||
upgradeInteractor.showDownloadingFailed(safeContext, isForce, |
|
||||||
onConfirm = { |
|
||||||
doUpdate() |
|
||||||
}, |
|
||||||
onCancel = { |
|
||||||
currentState = State.success() |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
private fun safeContext(onContext: (Activity) -> Unit) { |
|
||||||
val topActivity = Sword.topActivity |
|
||||||
if (topActivity != null) { |
|
||||||
onContext(topActivity) |
|
||||||
} else { |
|
||||||
AppUtils.registerAppStatusChangedListener(this, object : Utils.OnAppStatusChangedListener { |
|
||||||
override fun onBackground() = Unit |
|
||||||
override fun onForeground() { |
|
||||||
AppUtils.unregisterAppStatusChangedListener(this) |
|
||||||
Sword.topActivity?.let { onContext(it) } |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun startInstall(apkFile: File) { |
|
||||||
upgradeInteractor.installApk(apkFile, upgradeInfo) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
package com.android.base.utils.upgrade |
|
||||||
|
|
||||||
/** |
|
||||||
*@author Ztiany |
|
||||||
* Email: ztiany3@gmail.com |
|
||||||
* Date : 2019-10-29 19:17 |
|
||||||
*/ |
|
||||||
data class UpgradeInfo( |
|
||||||
val isNewVersion: Boolean, |
|
||||||
val isForce: Boolean, |
|
||||||
val versionName: String, |
|
||||||
val downloadUrl: String, |
|
||||||
val description: String, |
|
||||||
val digitalAbstract: String, |
|
||||||
val raw: Any? |
|
||||||
) |
|
@ -1,35 +0,0 @@ |
|||||||
package com.app.base.upgrade |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import com.android.base.utils.upgrade.UpgradeInfo |
|
||||||
import io.reactivex.Flowable |
|
||||||
import java.io.File |
|
||||||
|
|
||||||
/** |
|
||||||
*@author Ztiany |
|
||||||
* Email: ztiany3@gmail.com |
|
||||||
* Date : 2019-10-29 19:16 |
|
||||||
*/ |
|
||||||
interface UpgradeInteractor { |
|
||||||
|
|
||||||
fun checkUpgrade(): Flowable<UpgradeInfo> |
|
||||||
|
|
||||||
fun showUpgradeDialog(context: Context, upgradeInfo: UpgradeInfo, onCancel: () -> Unit, onConfirm: () -> Unit) |
|
||||||
|
|
||||||
fun showInstallTipsDialog(context: Context, force: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) |
|
||||||
|
|
||||||
fun showDownloadingFailed(context: Context, force: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) |
|
||||||
|
|
||||||
fun showDownloadingDialog(context: Context, force: Boolean) |
|
||||||
|
|
||||||
fun dismissDownloadingDialog() |
|
||||||
|
|
||||||
fun onProgress(total: Long, progress: Long) |
|
||||||
|
|
||||||
fun installApk(file: File, upgradeInfo: UpgradeInfo) |
|
||||||
|
|
||||||
fun checkApkFile(apkFile: File, digitalAbstract: String): Boolean |
|
||||||
|
|
||||||
fun generateAppDownloadPath(versionName: String): String |
|
||||||
|
|
||||||
} |
|
@ -1,110 +0,0 @@ |
|||||||
package com.android.base.utils.upgrade; |
|
||||||
|
|
||||||
|
|
||||||
import android.app.IntentService; |
|
||||||
import android.content.Context; |
|
||||||
import android.content.Intent; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Looper; |
|
||||||
import android.text.TextUtils; |
|
||||||
|
|
||||||
import com.android.base.utils.BaseUtils; |
|
||||||
import com.android.base.utils.common.Strings; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager; |
|
||||||
import timber.log.Timber; |
|
||||||
|
|
||||||
/** |
|
||||||
* 下载服务,不要配置在子进程。 |
|
||||||
*/ |
|
||||||
public class UpgradeService extends IntentService { |
|
||||||
|
|
||||||
static final String DOWNLOAD_APK_RESULT_ACTION = BaseUtils.getAppContext().getPackageName() + ".upgrade.result"; |
|
||||||
|
|
||||||
static final String IS_FORCE = "is_force"; |
|
||||||
static final String NOTIFY_TYPE_KEY = "notify_type"; |
|
||||||
static final String APK_FILE_KEY = "apk_path_key"; |
|
||||||
static final String URL_KEY = "url_key"; |
|
||||||
static final String VERSION_KEY = "version_key"; |
|
||||||
static final String TOTAL_PROGRESS_KEY = "total_progress_key"; |
|
||||||
static final String PROGRESS_KEY = "progress_key"; |
|
||||||
static final String DIGITAL_ABSTRACT_KEY = "digital_abstract_key"; |
|
||||||
|
|
||||||
public static final int NOTIFY_TYPE_FAILED = 1; |
|
||||||
public static final int NOTIFY_TYPE_PROGRESS = 2; |
|
||||||
public static final int NOTIFY_TYPE_SUCCESS = 3; |
|
||||||
|
|
||||||
private Handler mHandler; |
|
||||||
private boolean mIsForceUpdate; |
|
||||||
private String mDigitalAbstract; |
|
||||||
|
|
||||||
private class FlagDownloaderListener implements ApkDownloader.ApkDownloaderListener { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onProgress(long total, long progress) { |
|
||||||
Timber.d("onProgress() called with: total = [" + total + "], progress = [" + progress + "]"); |
|
||||||
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_PROGRESS, null, total, progress)); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onSuccess(File desFile) { |
|
||||||
Timber.d("onSuccess() called with: desFile = [" + desFile + "]"); |
|
||||||
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_SUCCESS, desFile, 0, 0)); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFail(Exception e) { |
|
||||||
Timber.d("onFail() called with: e = [" + e + "]"); |
|
||||||
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_FAILED, null, 0, 0)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public UpgradeService() { |
|
||||||
super("GW-UpgradeService"); |
|
||||||
} |
|
||||||
|
|
||||||
public static void start(Context context, String updateUrl, String versionName, String digitalAbstract, boolean isForceUpdate) { |
|
||||||
if (Strings.isEmpty(updateUrl) || Strings.isEmpty(versionName)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
Intent intent = new Intent(context, UpgradeService.class); |
|
||||||
intent.putExtra(URL_KEY, updateUrl); |
|
||||||
intent.putExtra(IS_FORCE, isForceUpdate); |
|
||||||
intent.putExtra(VERSION_KEY, versionName); |
|
||||||
intent.putExtra(DIGITAL_ABSTRACT_KEY, digitalAbstract); |
|
||||||
context.startService(intent); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
protected void onHandleIntent(Intent intent) { |
|
||||||
String url = intent.getStringExtra(URL_KEY); |
|
||||||
String versionName = intent.getStringExtra(VERSION_KEY); |
|
||||||
mIsForceUpdate = intent.getBooleanExtra(IS_FORCE, false); |
|
||||||
mDigitalAbstract = intent.getStringExtra(DIGITAL_ABSTRACT_KEY); |
|
||||||
if (!TextUtils.isEmpty(url) && !TextUtils.isEmpty(versionName)) { |
|
||||||
new ApkDownloader(url, AppUpgradeChecker.upgradeInteractor.generateAppDownloadPath(versionName), mDigitalAbstract, new FlagDownloaderListener()).start(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onCreate() { |
|
||||||
super.onCreate(); |
|
||||||
mHandler = new Handler(Looper.getMainLooper()); |
|
||||||
} |
|
||||||
|
|
||||||
private void notifyDownloadResult(int notifyType, File desFile, long total, long progress) { |
|
||||||
Intent intent = new Intent(); |
|
||||||
intent.setAction(DOWNLOAD_APK_RESULT_ACTION); |
|
||||||
intent.putExtra(IS_FORCE, mIsForceUpdate); |
|
||||||
intent.putExtra(NOTIFY_TYPE_KEY, notifyType); |
|
||||||
intent.putExtra(TOTAL_PROGRESS_KEY, total); |
|
||||||
intent.putExtra(PROGRESS_KEY, progress); |
|
||||||
if (desFile != null) { |
|
||||||
intent.putExtra(APK_FILE_KEY, desFile); |
|
||||||
} |
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,18 @@ |
|||||||
|
package com.android.base.widget.shape |
||||||
|
|
||||||
|
import androidx.annotation.IntDef |
||||||
|
import com.android.base.widget.shape.Direction.Companion.BOTTOM |
||||||
|
import com.android.base.widget.shape.Direction.Companion.LEFT |
||||||
|
import com.android.base.widget.shape.Direction.Companion.RIGHT |
||||||
|
import com.android.base.widget.shape.Direction.Companion.TOP |
||||||
|
|
||||||
|
@IntDef(TOP, BOTTOM, LEFT, RIGHT) |
||||||
|
@Retention(AnnotationRetention.SOURCE) |
||||||
|
annotation class Direction { |
||||||
|
companion object { |
||||||
|
const val TOP = 1 |
||||||
|
const val BOTTOM = 2 |
||||||
|
const val LEFT = 3 |
||||||
|
const val RIGHT = 4 |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,208 @@ |
|||||||
|
package com.android.base.widget.shape |
||||||
|
|
||||||
|
import android.annotation.SuppressLint |
||||||
|
import android.content.Context |
||||||
|
import android.graphics.Canvas |
||||||
|
import android.graphics.Color |
||||||
|
import android.graphics.Paint |
||||||
|
import android.graphics.Path |
||||||
|
import android.util.AttributeSet |
||||||
|
import android.widget.TextView |
||||||
|
import com.android.base.R |
||||||
|
import com.android.base.utils.android.views.use |
||||||
|
|
||||||
|
/** |
||||||
|
*@author Ztiany |
||||||
|
* Email: ztiany3@gmail.com |
||||||
|
* Date : 2019-01-16 10:09 |
||||||
|
*/ |
||||||
|
class TriangleView @JvmOverloads constructor( |
||||||
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 |
||||||
|
) : TextView(context, attrs, defStyleAttr) { |
||||||
|
|
||||||
|
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) |
||||||
|
|
||||||
|
@Suppress var bottomColor: Int = 0 |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
var strokeColor: Int = 0 |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
@Suppress var triangleSolidColor: Int = 0 |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
var strokeWidth: Float = 0F |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
@Direction var direction: Int = 0 |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
var trianglePercent: Float = 0F |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
private val mTrianglePath: Path = Path() |
||||||
|
|
||||||
|
init { |
||||||
|
initAttribute(context, attrs) |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressLint("Recycle") |
||||||
|
private fun initAttribute(context: Context, attrs: AttributeSet?) { |
||||||
|
context.obtainStyledAttributes(attrs, R.styleable.TriangleView).use { |
||||||
|
strokeColor = it.getColor(R.styleable.TriangleView_tv_triangle_stroke_color, Color.BLACK) |
||||||
|
bottomColor = it.getColor(R.styleable.TriangleView_tv_triangle_bottom_color, -10) |
||||||
|
if (bottomColor == -10) { |
||||||
|
bottomColor = strokeColor |
||||||
|
} |
||||||
|
triangleSolidColor = it.getColor(R.styleable.TriangleView_tv_triangle_solid_color, Color.TRANSPARENT) |
||||||
|
strokeWidth = it.getDimension(R.styleable.TriangleView_tv_triangle_stroke_width, 0F) |
||||||
|
direction = it.getInt(R.styleable.TriangleView_tv_triangle_direction, Direction.TOP) |
||||||
|
trianglePercent = it.getFloat(R.styleable.TriangleView_tv_triangle_angle_percent, 0.5F) |
||||||
|
} |
||||||
|
|
||||||
|
mPaint.strokeWidth = strokeWidth |
||||||
|
mPaint.strokeCap = Paint.Cap.ROUND |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) { |
||||||
|
super.onDraw(canvas) |
||||||
|
if (measuredHeight <= 0 || measuredWidth <= 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
when (direction) { |
||||||
|
Direction.TOP -> drawTopTriangle(canvas) |
||||||
|
Direction.BOTTOM -> drawBottomTriangle(canvas) |
||||||
|
Direction.LEFT -> drawLeftTriangle(canvas) |
||||||
|
Direction.RIGHT -> drawRightTriangle(canvas) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawLeftTriangle(canvas: Canvas) { |
||||||
|
val trianglePoint = trianglePercent * measuredHeight |
||||||
|
val drawingHeight = measuredHeight.toFloat() |
||||||
|
val drawingWidth = measuredWidth.toFloat() |
||||||
|
val haftStroke = strokeWidth * 0.5F |
||||||
|
|
||||||
|
//draw solid |
||||||
|
if (needDrawSolid()) { |
||||||
|
mTrianglePath.reset() |
||||||
|
mTrianglePath.moveTo(drawingWidth, 0F) |
||||||
|
mTrianglePath.lineTo(drawingWidth, drawingHeight) |
||||||
|
mTrianglePath.lineTo(0F, trianglePoint) |
||||||
|
mTrianglePath.close() |
||||||
|
mPaint.style = Paint.Style.FILL |
||||||
|
mPaint.color = triangleSolidColor |
||||||
|
canvas.drawPath(mTrianglePath, mPaint) |
||||||
|
} |
||||||
|
//draw stroke |
||||||
|
if (strokeWidth != 0F) { |
||||||
|
mPaint.color = strokeColor |
||||||
|
canvas.drawLine(0F, trianglePoint, drawingWidth, 0F, mPaint) |
||||||
|
canvas.drawLine(0F, trianglePoint, drawingWidth, drawingHeight, mPaint) |
||||||
|
mPaint.color = bottomColor |
||||||
|
canvas.drawLine(drawingWidth - haftStroke, 0F, drawingWidth - haftStroke, drawingHeight, mPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawRightTriangle(canvas: Canvas) { |
||||||
|
val trianglePoint = trianglePercent * measuredHeight |
||||||
|
val drawingHeight = measuredHeight.toFloat() |
||||||
|
val drawingWidth = measuredWidth.toFloat() |
||||||
|
val haftStroke = strokeWidth * 0.5F |
||||||
|
|
||||||
|
//draw solid |
||||||
|
if (needDrawSolid()) { |
||||||
|
mTrianglePath.reset() |
||||||
|
mTrianglePath.moveTo(0F, 0F) |
||||||
|
mTrianglePath.lineTo(0F, drawingHeight) |
||||||
|
mTrianglePath.lineTo(drawingWidth, trianglePoint) |
||||||
|
mTrianglePath.close() |
||||||
|
mPaint.style = Paint.Style.FILL |
||||||
|
mPaint.color = triangleSolidColor |
||||||
|
canvas.drawPath(mTrianglePath, mPaint) |
||||||
|
} |
||||||
|
//draw stroke |
||||||
|
if (strokeWidth != 0F) { |
||||||
|
mPaint.color = strokeColor |
||||||
|
canvas.drawLine(0F, 0F, drawingWidth, trianglePoint, mPaint) |
||||||
|
canvas.drawLine(drawingWidth, trianglePoint, 0F, drawingHeight, mPaint) |
||||||
|
mPaint.color = bottomColor |
||||||
|
canvas.drawLine(haftStroke, 0F, haftStroke, drawingHeight, mPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawBottomTriangle(canvas: Canvas) { |
||||||
|
val trianglePoint = trianglePercent * measuredWidth |
||||||
|
val drawingHeight = measuredHeight.toFloat() |
||||||
|
val drawingWidth = measuredWidth.toFloat() |
||||||
|
val haftStroke = strokeWidth * 0.5F |
||||||
|
|
||||||
|
//draw solid |
||||||
|
if (needDrawSolid()) { |
||||||
|
mTrianglePath.reset() |
||||||
|
mTrianglePath.moveTo(0F, 0F) |
||||||
|
mTrianglePath.lineTo(trianglePoint, drawingHeight) |
||||||
|
mTrianglePath.lineTo(drawingWidth, 0F) |
||||||
|
mTrianglePath.close() |
||||||
|
mPaint.style = Paint.Style.FILL |
||||||
|
mPaint.color = triangleSolidColor |
||||||
|
canvas.drawPath(mTrianglePath, mPaint) |
||||||
|
} |
||||||
|
//draw stroke |
||||||
|
if (strokeWidth != 0F) { |
||||||
|
mPaint.color = strokeColor |
||||||
|
canvas.drawLine(0F, 0F, trianglePoint, drawingHeight, mPaint) |
||||||
|
canvas.drawLine(trianglePoint, drawingHeight, drawingWidth, 0F, mPaint) |
||||||
|
mPaint.color = bottomColor |
||||||
|
canvas.drawLine(0F, haftStroke, drawingWidth, haftStroke, mPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawTopTriangle(canvas: Canvas) { |
||||||
|
val trianglePoint = trianglePercent * measuredWidth |
||||||
|
val drawingHeight = measuredHeight.toFloat() |
||||||
|
val drawingWidth = measuredWidth.toFloat() |
||||||
|
val haftStroke = strokeWidth * 0.5F |
||||||
|
|
||||||
|
//draw solid |
||||||
|
if (needDrawSolid()) { |
||||||
|
mTrianglePath.reset() |
||||||
|
mTrianglePath.moveTo(0F, drawingHeight) |
||||||
|
mTrianglePath.lineTo(drawingWidth, drawingHeight) |
||||||
|
mTrianglePath.lineTo(trianglePoint, 0F) |
||||||
|
mTrianglePath.close() |
||||||
|
mPaint.style = Paint.Style.FILL |
||||||
|
mPaint.color = triangleSolidColor |
||||||
|
canvas.drawPath(mTrianglePath, mPaint) |
||||||
|
} |
||||||
|
//draw stroke |
||||||
|
if (strokeWidth != 0F) { |
||||||
|
mPaint.color = strokeColor |
||||||
|
canvas.drawLine(0F, drawingHeight, trianglePoint, 0F, mPaint) |
||||||
|
canvas.drawLine(trianglePoint, 0F, drawingWidth, drawingHeight, mPaint) |
||||||
|
mPaint.color = bottomColor |
||||||
|
canvas.drawLine(drawingWidth, drawingHeight - haftStroke, 0F, drawingHeight - haftStroke, mPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun needDrawSolid() = triangleSolidColor != Color.TRANSPARENT |
||||||
|
|
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<module external.linked.project.id=":lib_coroutines" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../AndroidArchitecture" external.system.id="GRADLE" type="JAVA_MODULE" version="4"> |
||||||
|
<component name="FacetManager"> |
||||||
|
<facet type="android-gradle" name="Android-Gradle"> |
||||||
|
<configuration> |
||||||
|
<option name="GRADLE_PROJECT_PATH" value=":lib_coroutines" /> |
||||||
|
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" /> |
||||||
|
<option name="LAST_KNOWN_AGP_VERSION" /> |
||||||
|
</configuration> |
||||||
|
</facet> |
||||||
|
<facet type="java-gradle" name="Java-Gradle"> |
||||||
|
<configuration> |
||||||
|
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" /> |
||||||
|
<option name="BUILDABLE" value="false" /> |
||||||
|
</configuration> |
||||||
|
</facet> |
||||||
|
</component> |
||||||
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true"> |
||||||
|
<exclude-output /> |
||||||
|
<content url="file://$MODULE_DIR$"> |
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> |
||||||
|
</content> |
||||||
|
<orderEntry type="inheritedJdk" /> |
||||||
|
<orderEntry type="sourceFolder" forTests="false" /> |
||||||
|
</component> |
||||||
|
</module> |
Loading…
Reference in new issue