From 17ff366f5113b40a4c83c36614af6075d0c377da Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Sun, 27 Sep 2020 22:16:27 +0800 Subject: [PATCH] Implement download and install apk --- Example/testHotUpdate/src/index.js | 23 +++++++- android/src/main/AndroidManifest.xml | 10 ++++ .../modules/update/DownloadTask.java | 25 +++++--- .../modules/update/DownloadTaskParams.java | 7 ++- .../modules/update/PushyFileProvider.java | 14 +++++ .../modules/update/UpdateContext.java | 44 +++++++------- .../modules/update/UpdateModule.java | 58 +++++++++++++++++-- android/src/main/res/xml/file_paths.xml | 3 + lib/index.d.ts | 10 +++- lib/index.js | 24 +++++++- 10 files changed, 176 insertions(+), 42 deletions(-) create mode 100644 android/src/main/java/cn/reactnative/modules/update/PushyFileProvider.java create mode 100644 android/src/main/res/xml/file_paths.xml diff --git a/Example/testHotUpdate/src/index.js b/Example/testHotUpdate/src/index.js index 6859e58..164158b 100644 --- a/Example/testHotUpdate/src/index.js +++ b/Example/testHotUpdate/src/index.js @@ -21,6 +21,7 @@ import { switchVersion, switchVersionLater, markSuccess, + downloadAndInstallApk, } from 'react-native-update'; import _updateConfig from '../update.json'; @@ -94,11 +95,29 @@ export default class App extends Component { return; } if (info.expired) { - Alert.alert('提示', '您的应用版本已更新,请前往应用商店下载新的版本', [ + Alert.alert('提示', '您的应用版本已更新,点击确定下载安装新版本', [ { text: '确定', onPress: () => { - info.downloadUrl && Linking.openURL(info.downloadUrl); + if (info.downloadUrl) { + // apk可直接下载安装 + if ( + Platform.OS === 'android' && + info.downloadUrl.endsWith('.apk') + ) { + downloadAndInstallApk({ + url: info.downloadUrl, + onDownloadProgress: ({received, total}) => { + this.setState({ + received, + total, + }); + }, + }); + } else { + Linking.openURL(info.downloadUrl); + } + } }, }, ]); diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 868a3a1..99a484e 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,17 @@ + + + + diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java index 36ec1b1..917553b 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java @@ -72,7 +72,7 @@ class DownloadTask extends AsyncTask { private void downloadFile(DownloadTaskParams param) throws IOException { String url = param.url; - File writePath = param.zipFilePath; + File writePath = param.targetFile; this.hash = param.hash; OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(url) @@ -243,10 +243,10 @@ class DownloadTask extends AsyncTask { copyFilesWithBlacklist("", from, to, blackList); } - private void doDownload(DownloadTaskParams param) throws IOException { + private void doFullPatch(DownloadTaskParams param) throws IOException { downloadFile(param); - ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.zipFilePath))); + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.targetFile))); ZipEntry ze; String filename; @@ -303,7 +303,7 @@ class DownloadTask extends AsyncTask { private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONException { downloadFile(param); - ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.zipFilePath))); + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.targetFile))); ZipEntry ze; int count; String filename; @@ -379,7 +379,7 @@ class DownloadTask extends AsyncTask { private void doPatchFromPpk(DownloadTaskParams param) throws IOException, JSONException { downloadFile(param); - ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.zipFilePath))); + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(param.targetFile))); ZipEntry ze; int count; String filename; @@ -464,8 +464,8 @@ class DownloadTask extends AsyncTask { protected Void doInBackground(DownloadTaskParams... params) { try { switch (params[0].type) { - case DownloadTaskParams.TASK_TYPE_FULL_DOWNLOAD: - doDownload(params[0]); + case DownloadTaskParams.TASK_TYPE_PATCH_FULL: + doFullPatch(params[0]); break; case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK: doPatchFromApk(params[0]); @@ -473,11 +473,18 @@ class DownloadTask extends AsyncTask { case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK: doPatchFromPpk(params[0]); break; - case DownloadTaskParams.TASK_TYPE_CLEARUP: + case DownloadTaskParams.TASK_TYPE_CLEANUP: doCleanUp(params[0]); break; + case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD: + downloadFile(params[0]); + break; + default: + break; + } + if (params[0].listener != null) { + params[0].listener.onDownloadCompleted(params[0]); } - params[0].listener.onDownloadCompleted(); } catch (Throwable e) { if (UpdateContext.DEBUG) { e.printStackTrace(); diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java index 042cb69..248a392 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java @@ -8,17 +8,18 @@ import java.io.File; * Created by tdzl2003 on 3/31/16. */ class DownloadTaskParams { - static final int TASK_TYPE_FULL_DOWNLOAD = 1; + static final int TASK_TYPE_PATCH_FULL = 1; static final int TASK_TYPE_PATCH_FROM_APK = 2; static final int TASK_TYPE_PATCH_FROM_PPK = 3; + static final int TASK_TYPE_PLAIN_DOWNLOAD = 4; - static final int TASK_TYPE_CLEARUP = 0; //Keep hash & originHash + static final int TASK_TYPE_CLEANUP = 0; //Keep hash & originHash int type; String url; String hash; String originHash; - File zipFilePath; + File targetFile; File unzipDirectory; File originDirectory; UpdateContext.DownloadFileListener listener; diff --git a/android/src/main/java/cn/reactnative/modules/update/PushyFileProvider.java b/android/src/main/java/cn/reactnative/modules/update/PushyFileProvider.java new file mode 100644 index 0000000..b7af30c --- /dev/null +++ b/android/src/main/java/cn/reactnative/modules/update/PushyFileProvider.java @@ -0,0 +1,14 @@ +package cn.reactnative.modules.update; + +import android.support.v4.content.FileProvider; + +/** + * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. + * + * See https://developer.android.com/guide/topics/manifest/provider-element.html for details. + */ +public class PushyFileProvider extends FileProvider { + + // This class intentionally left blank. + +} \ No newline at end of file diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java b/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java index 3582dac..ab782fb 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java @@ -46,7 +46,7 @@ public class UpdateContext { editor.putString("packageVersion", packageVersion); editor.apply(); - this.clearUp(); + this.cleanUp(); } } @@ -86,28 +86,39 @@ public class UpdateContext { } public interface DownloadFileListener { - void onDownloadCompleted(); + void onDownloadCompleted(DownloadTaskParams params); void onDownloadFailed(Throwable error); } - public void downloadFile(String url, String hash, DownloadFileListener listener) { + public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) { DownloadTaskParams params = new DownloadTaskParams(); - params.type = DownloadTaskParams.TASK_TYPE_FULL_DOWNLOAD; + params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL; params.url = url; params.hash = hash; params.listener = listener; - params.zipFilePath = new File(rootDir, hash + ".ppk"); + params.targetFile = new File(rootDir, hash + ".ppk"); params.unzipDirectory = new File(rootDir, hash); new DownloadTask(context).executeOnExecutor(this.executor, params); } + public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) { + DownloadTaskParams params = new DownloadTaskParams(); + params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD; + params.url = url; + params.hash = hash; + params.listener = listener; + params.targetFile = new File(rootDir, fileName); +// params.unzipDirectory = new File(rootDir, hash); + new DownloadTask(context).executeOnExecutor(this.executor, params); + } + public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) { DownloadTaskParams params = new DownloadTaskParams(); params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK; params.url = url; params.hash = hash; params.listener = listener; - params.zipFilePath = new File(rootDir, hash + ".apk.patch"); + params.targetFile = new File(rootDir, hash + ".apk.patch"); params.unzipDirectory = new File(rootDir, hash); new DownloadTask(context).executeOnExecutor(this.executor, params); } @@ -119,7 +130,7 @@ public class UpdateContext { params.hash = hash; params.originHash = originHash; params.listener = listener; - params.zipFilePath = new File(rootDir, originHash + "-" + hash + ".ppk.patch"); + params.targetFile = new File(rootDir, originHash + "-" + hash + ".ppk.patch"); params.unzipDirectory = new File(rootDir, hash); params.originDirectory = new File(rootDir, originHash); new DownloadTask(context).executeOnExecutor(this.executor, params); @@ -174,7 +185,7 @@ public class UpdateContext { editor.remove("lastVersion"); editor.apply(); - this.clearUp(); + this.cleanUp(); } public void clearFirstTime() { @@ -182,7 +193,7 @@ public class UpdateContext { editor.putBoolean("firstTime", false); editor.apply(); - this.clearUp(); + this.cleanUp(); } public void clearRollbackMark() { @@ -190,7 +201,7 @@ public class UpdateContext { editor.putBoolean("rolledBack", false); editor.apply(); - this.clearUp(); + this.cleanUp(); } @@ -256,21 +267,12 @@ public class UpdateContext { return lastVersion; } - private void clearUp() { + private void cleanUp() { DownloadTaskParams params = new DownloadTaskParams(); - params.type = DownloadTaskParams.TASK_TYPE_CLEARUP; + params.type = DownloadTaskParams.TASK_TYPE_CLEANUP; params.hash = sp.getString("currentVersion", null); params.originHash = sp.getString("lastVersion", null); params.unzipDirectory = rootDir; - params.listener = new DownloadFileListener() { - @Override - public void onDownloadCompleted() { - } - - @Override - public void onDownloadFailed(Throwable error) { - } - }; new DownloadTask(context).executeOnExecutor(this.executor, params); } } diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateModule.java b/android/src/main/java/cn/reactnative/modules/update/UpdateModule.java index 8a6051b..0ff24c1 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateModule.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateModule.java @@ -3,6 +3,8 @@ package cn.reactnative.modules.update; import android.app.Activity; import android.app.Application; import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.util.Log; import com.facebook.react.ReactApplication; @@ -18,11 +20,14 @@ import com.facebook.react.bridge.JSBundleLoader; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import static android.support.v4.content.FileProvider.getUriForFile; + /** * Created by tdzl2003 on 3/31/16. */ @@ -71,9 +76,9 @@ public class UpdateModule extends ReactContextBaseJavaModule{ public void downloadUpdate(ReadableMap options, final Promise promise){ String url = options.getString("updateUrl"); String hash = options.getString("hash"); - updateContext.downloadFile(url, hash, new UpdateContext.DownloadFileListener() { + updateContext.downloadFullUpdate(url, hash, new UpdateContext.DownloadFileListener() { @Override - public void onDownloadCompleted() { + public void onDownloadCompleted(DownloadTaskParams params) { promise.resolve(null); } @@ -84,13 +89,58 @@ public class UpdateModule extends ReactContextBaseJavaModule{ }); } + @ReactMethod + public void downloadAndInstallApk(ReadableMap options, final Promise promise){ + String url = options.getString("url"); + String hash = options.getString("hash"); + String target = options.getString("target"); + updateContext.downloadFile(url, hash, target, new UpdateContext.DownloadFileListener() { + @Override + public void onDownloadCompleted(DownloadTaskParams params) { + installApk(params.targetFile); + promise.resolve(null); + } + + @Override + public void onDownloadFailed(Throwable error) { + promise.reject(error); + } + }); + } + + // install downloaded apk + @ReactMethod + public static void installApk(String url) { + File toInstall = new File(url); + installApk(toInstall); + } + + public static void installApk(File toInstall) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Uri apkUri = getUriForFile(mContext, mContext.getPackageName() + ".pushy.fileprovider", toInstall); + + Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + intent.setData(apkUri); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } else { + Uri apkUri = Uri.fromFile(toInstall); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + } + + @ReactMethod public void downloadPatchFromPackage(ReadableMap options, final Promise promise){ String url = options.getString("updateUrl"); String hash = options.getString("hash"); updateContext.downloadPatchFromApk(url, hash, new UpdateContext.DownloadFileListener() { @Override - public void onDownloadCompleted() { + public void onDownloadCompleted(DownloadTaskParams params) { promise.resolve(null); } @@ -108,7 +158,7 @@ public class UpdateModule extends ReactContextBaseJavaModule{ String originHash = options.getString("originHash"); updateContext.downloadPatchFromPpk(url, hash, originHash, new UpdateContext.DownloadFileListener() { @Override - public void onDownloadCompleted() { + public void onDownloadCompleted(DownloadTaskParams params) { promise.resolve(null); } diff --git a/android/src/main/res/xml/file_paths.xml b/android/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..a205351 --- /dev/null +++ b/android/src/main/res/xml/file_paths.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/lib/index.d.ts b/lib/index.d.ts index 3f5958f..561e241 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -43,6 +43,14 @@ export function switchVersionLater(hash: string): void; export function markSuccess(): void; +export async function downloadAndInstallApk({ + url, + onDownloadProgress, +}: { + url: string; + onDownloadProgress?: (data: ProgressData) => void; +}): void; + /** * @param {string} main - The main api endpoint * @param {string[]} [backups] - The back up endpoints. @@ -55,7 +63,7 @@ export function setCustomEndpoints({ backupQueryUrl, }: { main: string; - backUps?: string[]; + backups?: string[]; backupQueryUrl?: string; }): void; diff --git a/lib/index.js b/lib/index.js index 59d3f39..2f05597 100644 --- a/lib/index.js +++ b/lib/index.js @@ -143,10 +143,11 @@ export async function downloadUpdate(options, eventListeners) { if (!options.update) { return; } + let progressHandler; if (eventListeners) { if (eventListeners.onDownloadProgress) { const downloadCallback = eventListeners.onDownloadProgress; - eventEmitter.addListener('RCTPushyDownloadProgress', (progressData) => { + progressHandler = eventEmitter.addListener('RCTPushyDownloadProgress', (progressData) => { if (progressData.hash === options.hash) { downloadCallback(progressData); } @@ -167,7 +168,7 @@ export async function downloadUpdate(options, eventListeners) { hash: options.hash, }); } - eventEmitter.removeAllListeners('RCTPushyDownloadProgress'); + progressHandler && progressHandler.remove(); return options.hash; } @@ -188,3 +189,22 @@ export function markSuccess() { logger('markSuccess'); Pushy.markSuccess(); } + +export async function downloadAndInstallApk({ url, onDownloadProgress }) { + logger('downloadAndInstallApk'); + let hash = Date.now().toString(); + let progressHandler; + if (onDownloadProgress) { + progressHandler = eventEmitter.addListener('RCTPushyDownloadProgress', (progressData) => { + if (progressData.hash === hash) { + onDownloadProgress(progressData); + } + }); + } + await Pushy.downloadAndInstallApk({ + url, + target: 'update.apk', + hash, + }); + progressHandler && progressHandler.remove(); +}