@simplysm/capacitor-plugin-auto-update 12.15.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +9 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/kr/co/simplysm/capacitor/apkinstaller/ApkInstallerPlugin.java +125 -0
- package/dist/ApkInstaller.d.ts +29 -0
- package/dist/ApkInstaller.js +47 -0
- package/dist/AutoUpdate.d.ts +16 -0
- package/dist/AutoUpdate.js +185 -0
- package/dist/IApkInstallerPlugin.d.ts +17 -0
- package/dist/IApkInstallerPlugin.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/web/ApkInstallerWeb.d.ts +15 -0
- package/dist/web/ApkInstallerWeb.js +24 -0
- package/package.json +31 -0
- package/src/ApkInstaller.ts +54 -0
- package/src/AutoUpdate.ts +228 -0
- package/src/IApkInstallerPlugin.ts +12 -0
- package/src/index.ts +4 -0
- package/src/web/ApkInstallerWeb.ts +30 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package kr.co.simplysm.capacitor.apkinstaller;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.Intent;
|
|
5
|
+
import android.content.pm.PackageInfo;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.net.Uri;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.provider.Settings;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
|
|
12
|
+
import com.getcapacitor.JSObject;
|
|
13
|
+
import com.getcapacitor.Plugin;
|
|
14
|
+
import com.getcapacitor.PluginCall;
|
|
15
|
+
import com.getcapacitor.PluginMethod;
|
|
16
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
17
|
+
|
|
18
|
+
@CapacitorPlugin(name = "ApkInstaller")
|
|
19
|
+
public class ApkInstallerPlugin extends Plugin {
|
|
20
|
+
|
|
21
|
+
private static final String TAG = "ApkInstallerPlugin";
|
|
22
|
+
|
|
23
|
+
@PluginMethod
|
|
24
|
+
public void install(PluginCall call) {
|
|
25
|
+
String uriStr = call.getString("uri");
|
|
26
|
+
if (uriStr == null) {
|
|
27
|
+
call.reject("uri is required");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
Uri apkUri = Uri.parse(uriStr);
|
|
33
|
+
|
|
34
|
+
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
35
|
+
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
|
|
36
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
37
|
+
|
|
38
|
+
getContext().startActivity(intent);
|
|
39
|
+
call.resolve();
|
|
40
|
+
} catch (Exception e) {
|
|
41
|
+
Log.e(TAG, "install failed", e);
|
|
42
|
+
call.reject("Install failed: " + e.getMessage());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@PluginMethod
|
|
47
|
+
public void hasPermission(PluginCall call) {
|
|
48
|
+
boolean granted;
|
|
49
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
50
|
+
granted = getContext().getPackageManager().canRequestPackageInstalls();
|
|
51
|
+
} else {
|
|
52
|
+
granted = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
JSObject ret = new JSObject();
|
|
56
|
+
ret.put("granted", granted);
|
|
57
|
+
call.resolve(ret);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@PluginMethod
|
|
61
|
+
public void requestPermission(PluginCall call) {
|
|
62
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
63
|
+
Context context = getContext();
|
|
64
|
+
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
|
|
65
|
+
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
|
66
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
67
|
+
context.startActivity(intent);
|
|
68
|
+
}
|
|
69
|
+
call.resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@PluginMethod
|
|
73
|
+
public void hasPermissionManifest(PluginCall call) {
|
|
74
|
+
try {
|
|
75
|
+
Context context = getContext();
|
|
76
|
+
String targetPermission = "android.permission.REQUEST_INSTALL_PACKAGES";
|
|
77
|
+
|
|
78
|
+
String[] requestedPermissions = context.getPackageManager()
|
|
79
|
+
.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS)
|
|
80
|
+
.requestedPermissions;
|
|
81
|
+
|
|
82
|
+
boolean declared = false;
|
|
83
|
+
if (requestedPermissions != null) {
|
|
84
|
+
for (String perm : requestedPermissions) {
|
|
85
|
+
if (targetPermission.equals(perm)) {
|
|
86
|
+
declared = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
JSObject ret = new JSObject();
|
|
93
|
+
ret.put("declared", declared);
|
|
94
|
+
call.resolve(ret);
|
|
95
|
+
} catch (Exception e) {
|
|
96
|
+
Log.e(TAG, "hasPermissionManifest failed", e);
|
|
97
|
+
call.reject("Manifest check failed: " + e.getMessage());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@PluginMethod
|
|
102
|
+
public void getVersionInfo(PluginCall call) {
|
|
103
|
+
try {
|
|
104
|
+
Context context = getContext();
|
|
105
|
+
PackageManager pm = context.getPackageManager();
|
|
106
|
+
PackageInfo info = pm.getPackageInfo(context.getPackageName(), 0);
|
|
107
|
+
|
|
108
|
+
String versionName = info.versionName;
|
|
109
|
+
long versionCode;
|
|
110
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
111
|
+
versionCode = info.getLongVersionCode();
|
|
112
|
+
} else {
|
|
113
|
+
versionCode = info.versionCode;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
JSObject ret = new JSObject();
|
|
117
|
+
ret.put("versionName", versionName);
|
|
118
|
+
ret.put("versionCode", String.valueOf(versionCode));
|
|
119
|
+
call.resolve(ret);
|
|
120
|
+
} catch (Exception e) {
|
|
121
|
+
Log.e(TAG, "getVersionInfo failed", e);
|
|
122
|
+
call.reject("getVersionInfo failed: " + e.getMessage());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IVersionInfo } from "./IApkInstallerPlugin";
|
|
2
|
+
/**
|
|
3
|
+
* APK 설치 플러그인
|
|
4
|
+
* - Android: APK 설치 인텐트 실행, REQUEST_INSTALL_PACKAGES 권한 관리
|
|
5
|
+
* - Browser: alert으로 안내 후 정상 반환
|
|
6
|
+
*/
|
|
7
|
+
export declare abstract class ApkInstaller {
|
|
8
|
+
/**
|
|
9
|
+
* Manifest에 REQUEST_INSTALL_PACKAGES 권한이 선언되어 있는지 확인
|
|
10
|
+
*/
|
|
11
|
+
static hasPermissionManifest(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* REQUEST_INSTALL_PACKAGES 권한이 허용되어 있는지 확인
|
|
14
|
+
*/
|
|
15
|
+
static hasPermission(): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* REQUEST_INSTALL_PACKAGES 권한 요청 (설정 화면으로 이동)
|
|
18
|
+
*/
|
|
19
|
+
static requestPermission(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* APK 설치
|
|
22
|
+
* @param apkUri content:// URI (FileProvider URI)
|
|
23
|
+
*/
|
|
24
|
+
static install(apkUri: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* 앱 버전 정보 가져오기
|
|
27
|
+
*/
|
|
28
|
+
static getVersionInfo(): Promise<IVersionInfo>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
const ApkInstallerPlugin = registerPlugin("ApkInstaller", {
|
|
3
|
+
web: async () => {
|
|
4
|
+
const { ApkInstallerWeb } = await import("./web/ApkInstallerWeb");
|
|
5
|
+
return new ApkInstallerWeb();
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* APK 설치 플러그인
|
|
10
|
+
* - Android: APK 설치 인텐트 실행, REQUEST_INSTALL_PACKAGES 권한 관리
|
|
11
|
+
* - Browser: alert으로 안내 후 정상 반환
|
|
12
|
+
*/
|
|
13
|
+
export class ApkInstaller {
|
|
14
|
+
/**
|
|
15
|
+
* Manifest에 REQUEST_INSTALL_PACKAGES 권한이 선언되어 있는지 확인
|
|
16
|
+
*/
|
|
17
|
+
static async hasPermissionManifest() {
|
|
18
|
+
const result = await ApkInstallerPlugin.hasPermissionManifest();
|
|
19
|
+
return result.declared;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* REQUEST_INSTALL_PACKAGES 권한이 허용되어 있는지 확인
|
|
23
|
+
*/
|
|
24
|
+
static async hasPermission() {
|
|
25
|
+
const result = await ApkInstallerPlugin.hasPermission();
|
|
26
|
+
return result.granted;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* REQUEST_INSTALL_PACKAGES 권한 요청 (설정 화면으로 이동)
|
|
30
|
+
*/
|
|
31
|
+
static async requestPermission() {
|
|
32
|
+
await ApkInstallerPlugin.requestPermission();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* APK 설치
|
|
36
|
+
* @param apkUri content:// URI (FileProvider URI)
|
|
37
|
+
*/
|
|
38
|
+
static async install(apkUri) {
|
|
39
|
+
await ApkInstallerPlugin.install({ uri: apkUri });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 앱 버전 정보 가져오기
|
|
43
|
+
*/
|
|
44
|
+
static async getVersionInfo() {
|
|
45
|
+
return await ApkInstallerPlugin.getVersionInfo();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SdServiceClient } from "@simplysm/sd-service-client";
|
|
2
|
+
export declare abstract class AutoUpdate {
|
|
3
|
+
private static _throwAboutReinstall;
|
|
4
|
+
private static _checkPermissionAsync;
|
|
5
|
+
private static _installApkAsync;
|
|
6
|
+
private static _getErrorMessage;
|
|
7
|
+
private static _freezeApp;
|
|
8
|
+
static runAsync(opt: {
|
|
9
|
+
log: (messageHtml: string) => void;
|
|
10
|
+
serviceClient: SdServiceClient;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
static runByExternalStorageAsync(opt: {
|
|
13
|
+
log: (messageHtml: string) => void;
|
|
14
|
+
dirPath: string;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
2
|
+
import { html, NetUtils, Wait } from "@simplysm/sd-core-common";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import { ApkInstaller } from "./ApkInstaller";
|
|
5
|
+
import path from "path";
|
|
6
|
+
export class AutoUpdate {
|
|
7
|
+
static _throwAboutReinstall(code, targetHref) {
|
|
8
|
+
const downloadHtml = targetHref != null
|
|
9
|
+
? html `
|
|
10
|
+
<style>
|
|
11
|
+
._button {
|
|
12
|
+
all: unset;
|
|
13
|
+
color: blue;
|
|
14
|
+
width: 100%;
|
|
15
|
+
padding: 10px;
|
|
16
|
+
line-height: 1.5em;
|
|
17
|
+
font-size: 20px;
|
|
18
|
+
position: fixed;
|
|
19
|
+
bottom: 0;
|
|
20
|
+
left: 0;
|
|
21
|
+
border-top: 1px solid lightgrey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
._button:active {
|
|
25
|
+
background: lightgrey;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
28
|
+
<a
|
|
29
|
+
class="_button"
|
|
30
|
+
href="intent://${targetHref.replace(/^https?:\/\//, "")}#Intent;scheme=http;end"
|
|
31
|
+
>
|
|
32
|
+
다운로드
|
|
33
|
+
</a>
|
|
34
|
+
`
|
|
35
|
+
: "";
|
|
36
|
+
throw new Error(html `
|
|
37
|
+
APK파일을 다시 다운로드 받아, 설치해야 합니다(${code}). ${downloadHtml}
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
static async _checkPermissionAsync(log, targetHref) {
|
|
41
|
+
if (!navigator.userAgent.toLowerCase().includes("android")) {
|
|
42
|
+
throw new Error(`안드로이드만 지원합니다.`);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
if (!(await ApkInstaller.hasPermissionManifest())) {
|
|
46
|
+
this._throwAboutReinstall(1, targetHref);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
this._throwAboutReinstall(2, targetHref);
|
|
51
|
+
}
|
|
52
|
+
const hasPerm = await ApkInstaller.hasPermission();
|
|
53
|
+
if (!hasPerm) {
|
|
54
|
+
log(html `
|
|
55
|
+
설치권한이 설정되어야합니다.
|
|
56
|
+
<style>
|
|
57
|
+
button {
|
|
58
|
+
all: unset;
|
|
59
|
+
color: blue;
|
|
60
|
+
width: 100%;
|
|
61
|
+
padding: 10px;
|
|
62
|
+
line-height: 1.5em;
|
|
63
|
+
font-size: 20px;
|
|
64
|
+
position: fixed;
|
|
65
|
+
bottom: 0;
|
|
66
|
+
left: 0;
|
|
67
|
+
border-top: 1px solid lightgrey;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
button:active {
|
|
71
|
+
background: lightgrey;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
<button onclick="location.reload()">재시도</button>
|
|
75
|
+
`);
|
|
76
|
+
await ApkInstaller.requestPermission();
|
|
77
|
+
await Wait.until(async () => {
|
|
78
|
+
return await ApkInstaller.hasPermission();
|
|
79
|
+
}, 1000);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
static async _installApkAsync(log, apkFilePath) {
|
|
83
|
+
log(html `
|
|
84
|
+
최신버전을 설치한 후 재시작하세요.
|
|
85
|
+
<style>
|
|
86
|
+
button {
|
|
87
|
+
all: unset;
|
|
88
|
+
color: blue;
|
|
89
|
+
width: 100%;
|
|
90
|
+
padding: 10px;
|
|
91
|
+
line-height: 1.5em;
|
|
92
|
+
font-size: 20px;
|
|
93
|
+
position: fixed;
|
|
94
|
+
bottom: 0;
|
|
95
|
+
left: 0;
|
|
96
|
+
border-top: 1px solid lightgrey;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
button:active {
|
|
100
|
+
background: lightgrey;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
<button onclick="location.reload()">재시도</button>
|
|
104
|
+
`);
|
|
105
|
+
const apkFileUri = await FileSystem.getFileUriAsync(apkFilePath);
|
|
106
|
+
await ApkInstaller.install(apkFileUri);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
static _getErrorMessage(err) {
|
|
110
|
+
return html `
|
|
111
|
+
업데이트 중 오류 발생:
|
|
112
|
+
<br />
|
|
113
|
+
${err instanceof Error ? err.message : String(err)}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
static async _freezeApp() {
|
|
117
|
+
await new Promise(() => { }); // 무한대기
|
|
118
|
+
}
|
|
119
|
+
static async runAsync(opt) {
|
|
120
|
+
try {
|
|
121
|
+
opt.log(`최신버전 확인 중...`);
|
|
122
|
+
// 서버의 버전 및 다운로드링크 가져오기
|
|
123
|
+
const autoUpdateServiceClient = opt.serviceClient.getService("SdAutoUpdateService");
|
|
124
|
+
const serverVersionInfo = await autoUpdateServiceClient.getLastVersion("android");
|
|
125
|
+
if (!serverVersionInfo) {
|
|
126
|
+
throw new Error("서버에서 최신버전 정보를 가져오지 못했습니다.");
|
|
127
|
+
}
|
|
128
|
+
opt.log(`권한 확인 중...`);
|
|
129
|
+
await this._checkPermissionAsync(opt.log, opt.serviceClient.hostUrl + serverVersionInfo.downloadPath);
|
|
130
|
+
// 최신버전이면 반환
|
|
131
|
+
if (process.env["SD_VERSION"] === serverVersionInfo.version) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
opt.log(`최신버전 파일 다운로드중...`);
|
|
135
|
+
const buffer = await NetUtils.downloadBufferAsync(opt.serviceClient.hostUrl + serverVersionInfo.downloadPath, (progress) => {
|
|
136
|
+
const progressText = ((progress.receivedLength * 100) / progress.contentLength).toFixed(2);
|
|
137
|
+
opt.log(`최신버전 파일 다운로드중...(${progressText}%)`);
|
|
138
|
+
});
|
|
139
|
+
const storagePath = await FileSystem.getStoragePathAsync("appCache");
|
|
140
|
+
const apkFilePath = path.join(storagePath, `latest.apk`);
|
|
141
|
+
await FileSystem.writeFileAsync(apkFilePath, buffer);
|
|
142
|
+
await this._installApkAsync(opt.log, apkFilePath);
|
|
143
|
+
await this._freezeApp();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
opt.log(this._getErrorMessage(err));
|
|
147
|
+
await this._freezeApp();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
static async runByExternalStorageAsync(opt) {
|
|
151
|
+
try {
|
|
152
|
+
opt.log(`권한 확인 중...`);
|
|
153
|
+
await this._checkPermissionAsync(opt.log);
|
|
154
|
+
opt.log(`최신버전 확인 중...`);
|
|
155
|
+
// 버전 가져오기
|
|
156
|
+
const externalPath = await FileSystem.getStoragePathAsync("external");
|
|
157
|
+
const fileInfos = await FileSystem.readdirAsync(path.join(externalPath, opt.dirPath));
|
|
158
|
+
const versions = fileInfos
|
|
159
|
+
.filter((fileInfo) => !fileInfo.isDirectory)
|
|
160
|
+
.map((fileInfo) => ({
|
|
161
|
+
fileName: fileInfo.name,
|
|
162
|
+
version: path.basename(fileInfo.name, path.extname(fileInfo.name)),
|
|
163
|
+
extName: path.extname(fileInfo.name),
|
|
164
|
+
}))
|
|
165
|
+
.filter((item) => {
|
|
166
|
+
return item.extName === ".apk" && /^[0-9.]*$/.test(item.version);
|
|
167
|
+
});
|
|
168
|
+
// 버전파일 저장된것 없으면 반환
|
|
169
|
+
if (versions.length === 0)
|
|
170
|
+
return;
|
|
171
|
+
const latestVersion = semver.maxSatisfying(versions.map((item) => item.version), "*");
|
|
172
|
+
// 최신버전이면 반환
|
|
173
|
+
if (process.env["SD_VERSION"] === latestVersion) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const apkFilePath = path.join(externalPath, opt.dirPath, latestVersion + ".apk");
|
|
177
|
+
await this._installApkAsync(opt.log, apkFilePath);
|
|
178
|
+
await this._freezeApp();
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
opt.log(this._getErrorMessage(err));
|
|
182
|
+
await this._freezeApp();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface IVersionInfo {
|
|
2
|
+
versionName: string;
|
|
3
|
+
versionCode: string;
|
|
4
|
+
}
|
|
5
|
+
export interface IApkInstallerPlugin {
|
|
6
|
+
install(options: {
|
|
7
|
+
uri: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
hasPermission(): Promise<{
|
|
10
|
+
granted: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
requestPermission(): Promise<void>;
|
|
13
|
+
hasPermissionManifest(): Promise<{
|
|
14
|
+
declared: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
getVersionInfo(): Promise<IVersionInfo>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import { IApkInstallerPlugin, IVersionInfo } from "../IApkInstallerPlugin";
|
|
3
|
+
export declare class ApkInstallerWeb extends WebPlugin implements IApkInstallerPlugin {
|
|
4
|
+
install(_options: {
|
|
5
|
+
uri: string;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
hasPermission(): Promise<{
|
|
8
|
+
granted: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
requestPermission(): Promise<void>;
|
|
11
|
+
hasPermissionManifest(): Promise<{
|
|
12
|
+
declared: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
getVersionInfo(): Promise<IVersionInfo>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
export class ApkInstallerWeb extends WebPlugin {
|
|
3
|
+
async install(_options) {
|
|
4
|
+
alert("[ApkInstaller] 웹 환경에서는 APK 설치를 지원하지 않습니다.");
|
|
5
|
+
await Promise.resolve();
|
|
6
|
+
}
|
|
7
|
+
async hasPermission() {
|
|
8
|
+
// 웹에서는 권한 체크 스킵
|
|
9
|
+
return await Promise.resolve({ granted: true });
|
|
10
|
+
}
|
|
11
|
+
async requestPermission() {
|
|
12
|
+
// no-op
|
|
13
|
+
}
|
|
14
|
+
async hasPermissionManifest() {
|
|
15
|
+
// 웹에서는 매니페스트 체크 스킵
|
|
16
|
+
return await Promise.resolve({ declared: true });
|
|
17
|
+
}
|
|
18
|
+
async getVersionInfo() {
|
|
19
|
+
return await Promise.resolve({
|
|
20
|
+
versionName: process.env["SD_VERSION"] ?? "0.0.0",
|
|
21
|
+
versionCode: "0",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simplysm/capacitor-plugin-auto-update",
|
|
3
|
+
"version": "12.15.69",
|
|
4
|
+
"description": "심플리즘 패키지 - Capacitor Plugin Auto Update",
|
|
5
|
+
"author": "김석래",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/kslhunter/simplysm.git",
|
|
9
|
+
"directory": "packages/capacitor-plugin-auto-update"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"capacitor": {
|
|
16
|
+
"android": {
|
|
17
|
+
"src": "android"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@capacitor/core": "^7.4.4",
|
|
22
|
+
"@simplysm/capacitor-plugin-file-system": "12.15.69",
|
|
23
|
+
"@simplysm/sd-core-common": "12.15.69",
|
|
24
|
+
"@simplysm/sd-service-client": "12.15.69",
|
|
25
|
+
"@simplysm/sd-service-common": "12.15.69",
|
|
26
|
+
"semver": "^7.7.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/semver": "^7.7.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
import { IApkInstallerPlugin, IVersionInfo } from "./IApkInstallerPlugin";
|
|
3
|
+
|
|
4
|
+
const ApkInstallerPlugin = registerPlugin<IApkInstallerPlugin>("ApkInstaller", {
|
|
5
|
+
web: async () => {
|
|
6
|
+
const { ApkInstallerWeb } = await import("./web/ApkInstallerWeb");
|
|
7
|
+
return new ApkInstallerWeb();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* APK 설치 플러그인
|
|
13
|
+
* - Android: APK 설치 인텐트 실행, REQUEST_INSTALL_PACKAGES 권한 관리
|
|
14
|
+
* - Browser: alert으로 안내 후 정상 반환
|
|
15
|
+
*/
|
|
16
|
+
export abstract class ApkInstaller {
|
|
17
|
+
/**
|
|
18
|
+
* Manifest에 REQUEST_INSTALL_PACKAGES 권한이 선언되어 있는지 확인
|
|
19
|
+
*/
|
|
20
|
+
static async hasPermissionManifest(): Promise<boolean> {
|
|
21
|
+
const result = await ApkInstallerPlugin.hasPermissionManifest();
|
|
22
|
+
return result.declared;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* REQUEST_INSTALL_PACKAGES 권한이 허용되어 있는지 확인
|
|
27
|
+
*/
|
|
28
|
+
static async hasPermission(): Promise<boolean> {
|
|
29
|
+
const result = await ApkInstallerPlugin.hasPermission();
|
|
30
|
+
return result.granted;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* REQUEST_INSTALL_PACKAGES 권한 요청 (설정 화면으로 이동)
|
|
35
|
+
*/
|
|
36
|
+
static async requestPermission(): Promise<void> {
|
|
37
|
+
await ApkInstallerPlugin.requestPermission();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* APK 설치
|
|
42
|
+
* @param apkUri content:// URI (FileProvider URI)
|
|
43
|
+
*/
|
|
44
|
+
static async install(apkUri: string): Promise<void> {
|
|
45
|
+
await ApkInstallerPlugin.install({ uri: apkUri });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 앱 버전 정보 가져오기
|
|
50
|
+
*/
|
|
51
|
+
static async getVersionInfo(): Promise<IVersionInfo> {
|
|
52
|
+
return await ApkInstallerPlugin.getVersionInfo();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
2
|
+
import { html, NetUtils, Wait } from "@simplysm/sd-core-common";
|
|
3
|
+
import { SdServiceClient } from "@simplysm/sd-service-client";
|
|
4
|
+
import { ISdAutoUpdateService } from "@simplysm/sd-service-common";
|
|
5
|
+
import semver from "semver";
|
|
6
|
+
import { ApkInstaller } from "./ApkInstaller";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
export abstract class AutoUpdate {
|
|
10
|
+
private static _throwAboutReinstall(code: number, targetHref?: string) {
|
|
11
|
+
const downloadHtml =
|
|
12
|
+
targetHref != null
|
|
13
|
+
? html`
|
|
14
|
+
<style>
|
|
15
|
+
._button {
|
|
16
|
+
all: unset;
|
|
17
|
+
color: blue;
|
|
18
|
+
width: 100%;
|
|
19
|
+
padding: 10px;
|
|
20
|
+
line-height: 1.5em;
|
|
21
|
+
font-size: 20px;
|
|
22
|
+
position: fixed;
|
|
23
|
+
bottom: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
border-top: 1px solid lightgrey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
._button:active {
|
|
29
|
+
background: lightgrey;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
<a
|
|
33
|
+
class="_button"
|
|
34
|
+
href="intent://${targetHref.replace(/^https?:\/\//, "")}#Intent;scheme=http;end"
|
|
35
|
+
>
|
|
36
|
+
다운로드
|
|
37
|
+
</a>
|
|
38
|
+
`
|
|
39
|
+
: "";
|
|
40
|
+
|
|
41
|
+
throw new Error(html`
|
|
42
|
+
APK파일을 다시 다운로드 받아, 설치해야 합니다(${code}). ${downloadHtml}
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static async _checkPermissionAsync(
|
|
47
|
+
log: (messageHtml: string) => void,
|
|
48
|
+
targetHref?: string,
|
|
49
|
+
) {
|
|
50
|
+
if (!navigator.userAgent.toLowerCase().includes("android")) {
|
|
51
|
+
throw new Error(`안드로이드만 지원합니다.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
if (!(await ApkInstaller.hasPermissionManifest())) {
|
|
56
|
+
this._throwAboutReinstall(1, targetHref);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
this._throwAboutReinstall(2, targetHref);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hasPerm = await ApkInstaller.hasPermission();
|
|
63
|
+
if (!hasPerm) {
|
|
64
|
+
log(html`
|
|
65
|
+
설치권한이 설정되어야합니다.
|
|
66
|
+
<style>
|
|
67
|
+
button {
|
|
68
|
+
all: unset;
|
|
69
|
+
color: blue;
|
|
70
|
+
width: 100%;
|
|
71
|
+
padding: 10px;
|
|
72
|
+
line-height: 1.5em;
|
|
73
|
+
font-size: 20px;
|
|
74
|
+
position: fixed;
|
|
75
|
+
bottom: 0;
|
|
76
|
+
left: 0;
|
|
77
|
+
border-top: 1px solid lightgrey;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
button:active {
|
|
81
|
+
background: lightgrey;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
84
|
+
<button onclick="location.reload()">재시도</button>
|
|
85
|
+
`);
|
|
86
|
+
await ApkInstaller.requestPermission();
|
|
87
|
+
await Wait.until(async () => {
|
|
88
|
+
return await ApkInstaller.hasPermission();
|
|
89
|
+
}, 1000);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static async _installApkAsync(log: (messageHtml: string) => void, apkFilePath: string) {
|
|
94
|
+
log(html`
|
|
95
|
+
최신버전을 설치한 후 재시작하세요.
|
|
96
|
+
<style>
|
|
97
|
+
button {
|
|
98
|
+
all: unset;
|
|
99
|
+
color: blue;
|
|
100
|
+
width: 100%;
|
|
101
|
+
padding: 10px;
|
|
102
|
+
line-height: 1.5em;
|
|
103
|
+
font-size: 20px;
|
|
104
|
+
position: fixed;
|
|
105
|
+
bottom: 0;
|
|
106
|
+
left: 0;
|
|
107
|
+
border-top: 1px solid lightgrey;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
button:active {
|
|
111
|
+
background: lightgrey;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
114
|
+
<button onclick="location.reload()">재시도</button>
|
|
115
|
+
`);
|
|
116
|
+
const apkFileUri = await FileSystem.getFileUriAsync(apkFilePath);
|
|
117
|
+
await ApkInstaller.install(apkFileUri);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private static _getErrorMessage(err: unknown) {
|
|
122
|
+
return html`
|
|
123
|
+
업데이트 중 오류 발생:
|
|
124
|
+
<br />
|
|
125
|
+
${err instanceof Error ? err.message : String(err)}
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static async _freezeApp() {
|
|
130
|
+
await new Promise(() => {}); // 무한대기
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static async runAsync(opt: {
|
|
134
|
+
log: (messageHtml: string) => void;
|
|
135
|
+
serviceClient: SdServiceClient;
|
|
136
|
+
}) {
|
|
137
|
+
try {
|
|
138
|
+
opt.log(`최신버전 확인 중...`);
|
|
139
|
+
|
|
140
|
+
// 서버의 버전 및 다운로드링크 가져오기
|
|
141
|
+
const autoUpdateServiceClient =
|
|
142
|
+
opt.serviceClient.getService<ISdAutoUpdateService>("SdAutoUpdateService");
|
|
143
|
+
|
|
144
|
+
const serverVersionInfo = await autoUpdateServiceClient.getLastVersion("android");
|
|
145
|
+
if (!serverVersionInfo) {
|
|
146
|
+
throw new Error("서버에서 최신버전 정보를 가져오지 못했습니다.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
opt.log(`권한 확인 중...`);
|
|
150
|
+
await this._checkPermissionAsync(
|
|
151
|
+
opt.log,
|
|
152
|
+
opt.serviceClient.hostUrl + serverVersionInfo.downloadPath,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// 최신버전이면 반환
|
|
156
|
+
if (process.env["SD_VERSION"] === serverVersionInfo.version) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
opt.log(`최신버전 파일 다운로드중...`);
|
|
161
|
+
const buffer = await NetUtils.downloadBufferAsync(
|
|
162
|
+
opt.serviceClient.hostUrl + serverVersionInfo.downloadPath,
|
|
163
|
+
(progress) => {
|
|
164
|
+
const progressText = ((progress.receivedLength * 100) / progress.contentLength).toFixed(
|
|
165
|
+
2,
|
|
166
|
+
);
|
|
167
|
+
opt.log(`최신버전 파일 다운로드중...(${progressText}%)`);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
const storagePath = await FileSystem.getStoragePathAsync("appCache");
|
|
171
|
+
const apkFilePath = path.join(storagePath, `latest.apk`);
|
|
172
|
+
await FileSystem.writeFileAsync(apkFilePath, buffer);
|
|
173
|
+
|
|
174
|
+
await this._installApkAsync(opt.log, apkFilePath);
|
|
175
|
+
await this._freezeApp();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
opt.log(this._getErrorMessage(err));
|
|
178
|
+
await this._freezeApp();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static async runByExternalStorageAsync(opt: {
|
|
183
|
+
log: (messageHtml: string) => void;
|
|
184
|
+
dirPath: string;
|
|
185
|
+
}) {
|
|
186
|
+
try {
|
|
187
|
+
opt.log(`권한 확인 중...`);
|
|
188
|
+
await this._checkPermissionAsync(opt.log);
|
|
189
|
+
|
|
190
|
+
opt.log(`최신버전 확인 중...`);
|
|
191
|
+
|
|
192
|
+
// 버전 가져오기
|
|
193
|
+
const externalPath = await FileSystem.getStoragePathAsync("external");
|
|
194
|
+
const fileInfos = await FileSystem.readdirAsync(path.join(externalPath, opt.dirPath));
|
|
195
|
+
|
|
196
|
+
const versions = fileInfos
|
|
197
|
+
.filter((fileInfo) => !fileInfo.isDirectory)
|
|
198
|
+
.map((fileInfo) => ({
|
|
199
|
+
fileName: fileInfo.name,
|
|
200
|
+
version: path.basename(fileInfo.name, path.extname(fileInfo.name)),
|
|
201
|
+
extName: path.extname(fileInfo.name),
|
|
202
|
+
}))
|
|
203
|
+
.filter((item) => {
|
|
204
|
+
return item.extName === ".apk" && /^[0-9.]*$/.test(item.version);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 버전파일 저장된것 없으면 반환
|
|
208
|
+
if (versions.length === 0) return;
|
|
209
|
+
|
|
210
|
+
const latestVersion = semver.maxSatisfying(
|
|
211
|
+
versions.map((item) => item.version),
|
|
212
|
+
"*",
|
|
213
|
+
)!;
|
|
214
|
+
|
|
215
|
+
// 최신버전이면 반환
|
|
216
|
+
if (process.env["SD_VERSION"] === latestVersion) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const apkFilePath = path.join(externalPath, opt.dirPath, latestVersion + ".apk");
|
|
221
|
+
await this._installApkAsync(opt.log, apkFilePath);
|
|
222
|
+
await this._freezeApp();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
opt.log(this._getErrorMessage(err));
|
|
225
|
+
await this._freezeApp();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IVersionInfo {
|
|
2
|
+
versionName: string;
|
|
3
|
+
versionCode: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface IApkInstallerPlugin {
|
|
7
|
+
install(options: { uri: string }): Promise<void>;
|
|
8
|
+
hasPermission(): Promise<{ granted: boolean }>;
|
|
9
|
+
requestPermission(): Promise<void>;
|
|
10
|
+
hasPermissionManifest(): Promise<{ declared: boolean }>;
|
|
11
|
+
getVersionInfo(): Promise<IVersionInfo>;
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import { IApkInstallerPlugin, IVersionInfo } from "../IApkInstallerPlugin";
|
|
3
|
+
|
|
4
|
+
export class ApkInstallerWeb extends WebPlugin implements IApkInstallerPlugin {
|
|
5
|
+
async install(_options: { uri: string }): Promise<void> {
|
|
6
|
+
alert("[ApkInstaller] 웹 환경에서는 APK 설치를 지원하지 않습니다.");
|
|
7
|
+
await Promise.resolve();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async hasPermission(): Promise<{ granted: boolean }> {
|
|
11
|
+
// 웹에서는 권한 체크 스킵
|
|
12
|
+
return await Promise.resolve({ granted: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async requestPermission(): Promise<void> {
|
|
16
|
+
// no-op
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async hasPermissionManifest(): Promise<{ declared: boolean }> {
|
|
20
|
+
// 웹에서는 매니페스트 체크 스킵
|
|
21
|
+
return await Promise.resolve({ declared: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getVersionInfo(): Promise<IVersionInfo> {
|
|
25
|
+
return await Promise.resolve({
|
|
26
|
+
versionName: process.env["SD_VERSION"] ?? "0.0.0",
|
|
27
|
+
versionCode: "0",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|