1
0
mirror of https://gitcode.com/gh_mirrors/re/react-native-pushy.git synced 2025-10-07 19:05:13 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee

react-native-pushy适配harmony (#461)

* init

* update

* udpate

* update

* update

* update

* add pushy build time logic
This commit is contained in:
波仔糕
2024-12-26 12:11:52 +08:00
committed by GitHub
parent 1ad0926a24
commit 822602add9
151 changed files with 25412 additions and 3 deletions

View File

@@ -0,0 +1,492 @@
import http from '@ohos.net.http';
import fileIo from '@ohos.file.fs';
import util from '@ohos.util';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
import zip from '@ohos.zlib';
import { EventHub } from './EventHub';
import { DownloadTaskParams } from './DownloadTaskParams';
import Pushy from 'librnupdate.so';
interface ZipEntry {
filename: string;
content: ArrayBuffer;
}
interface ZipFile {
entries: ZipEntry[];
}
export class DownloadTask {
private context: common.Context;
private hash: string;
private readonly DOWNLOAD_CHUNK_SIZE = 4096;
private eventHub: EventHub;
constructor(context: common.Context) {
this.context = context;
this.eventHub = EventHub.getInstance();
}
private async removeDirectory(path: string): Promise<void> {
try {
const res = fileIo.accessSync(path);
if (res) {
const stat = await fileIo.stat(path);
if (stat.isDirectory()) {
const files = await fileIo.listFile(path);
for (const file of files) {
if (file === '.' || file === '..') continue;
await this.removeDirectory(`${path}/${file}`);
}
await fileIo.rmdir(path);
} else {
await fileIo.unlink(path);
}
}
} catch (error) {
console.error('Failed to delete directory:', error);
throw error;
}
}
private async downloadFile(params: DownloadTaskParams): Promise<void> {
const httpRequest = http.createHttp();
this.hash = params.hash;
try {
try {
const exists = fileIo.accessSync(params.targetFile);
if (exists) {
await fileIo.unlink(params.targetFile);
}else{
const targetDir = params.targetFile.substring(
0,
params.targetFile.lastIndexOf('/'),
);
await fileIo.mkdir(targetDir);
}
} catch (error) {
}
const response = await httpRequest.request(params.url, {
method: http.RequestMethod.GET,
readTimeout: 60000,
connectTimeout: 60000,
header: {
'Content-Type': 'application/octet-stream',
},
});
if (response.responseCode > 299) {
throw new Error(`Server error: ${response.responseCode}`);
}
const contentLength = parseInt(response.header['Content-Length'] || '0');
const writer = await fileIo.open(
params.targetFile,
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
);
let received = 0;
const data = response.result as ArrayBuffer;
const chunks = Math.ceil(data.byteLength / this.DOWNLOAD_CHUNK_SIZE);
for (let i = 0; i < chunks; i++) {
const start = i * this.DOWNLOAD_CHUNK_SIZE;
const end = Math.min(start + this.DOWNLOAD_CHUNK_SIZE, data.byteLength);
const chunk = data.slice(start, end);
await fileIo.write(writer.fd, chunk);
received += chunk.byteLength;
this.onProgressUpdate(received, contentLength);
}
await fileIo.close(writer);
const stat = await fileIo.stat(params.targetFile);
const fileSize = stat.size;
} catch (error) {
console.error('Download failed:', error);
throw error;
} finally {
httpRequest.destroy();
}
}
private onProgressUpdate(received: number, total: number): void {
this.eventHub.emit('downloadProgress', {
received,
total,
hash: this.hash,
});
}
private async copyFile(from: string, to: string): Promise<void> {
let reader;
let writer;
try {
reader = fileIo.openSync(from, fileIo.OpenMode.READ_ONLY);
writer = fileIo.openSync(
to,
fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
);
const arrayBuffer = new ArrayBuffer(4096);
let bytesRead: number;
do {
bytesRead = await fileIo
.read(reader.fd, arrayBuffer)
.catch((err: BusinessError) => {
throw new Error(
`Error reading file: ${err.message}, code: ${err.code}`,
);
});
if (bytesRead > 0) {
const buf = buffer.from(arrayBuffer, 0, bytesRead);
await fileIo
.write(writer.fd, buf.buffer, {
offset: 0,
length: bytesRead,
})
.catch((err: BusinessError) => {
throw new Error(
`Error writing file: ${err.message}, code: ${err.code}`,
);
});
}
} while (bytesRead > 0);
console.info('File copied successfully');
} catch (error) {
console.error('Copy file failed:', error);
throw error;
} finally {
if (reader !== undefined) {
fileIo.closeSync(reader);
}
if (writer !== undefined) {
fileIo.closeSync(writer);
}
}
}
private async doFullPatch(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
try {
await zip.decompressFile(params.targetFile, params.unzipDirectory);
} catch (error) {
console.error('Unzip failed:', error);
throw error;
}
}
private async processUnzippedFiles(directory: string): Promise<ZipFile> {
const entries: ZipEntry[] = [];
try {
const files = await fileIo.listFile(directory);
for (const file of files) {
if (file === '.' || file === '..') continue;
const filePath = `${directory}/${file}`;
const stat = await fileIo.stat(filePath);
if (!stat.isDirectory()) {
const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const fileSize = stat.size;
const content = new ArrayBuffer(fileSize);
try {
await fileIo.read(reader.fd, content);
entries.push({
filename: file,
content: content,
});
} finally {
await fileIo.close(reader);
}
}
}
return { entries };
} catch (error) {
console.error('Failed to process unzipped files:', error);
throw error;
}
}
private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
let foundDiff = false;
let foundBundlePatch = false;
const copyList: Map<string, Array<any>> = new Map();
await zip.decompressFile(params.targetFile, params.unzipDirectory);
const zipFile = await this.processUnzippedFiles(params.unzipDirectory);
for (const entry of zipFile.entries) {
const fn = entry.filename;
if (fn === '__diff.json') {
foundDiff = true;
let jsonContent = '';
const bufferArray = new Uint8Array(entry.content);
for (let i = 0; i < bufferArray.length; i++) {
jsonContent += String.fromCharCode(bufferArray[i]);
}
const obj = JSON.parse(jsonContent);
const copies = obj.copies;
for (const to in copies) {
let from = copies[to];
if (from === '') {
from = to;
}
if (!copyList.has(from)) {
copyList.set(from, []);
}
const target = copyList.get(from);
if (target) {
const toFile = `${params.unzipDirectory}/${to}`;
target.push(toFile);
}
}
continue;
}
if (fn === 'bundle.harmony.js.patch') {
foundBundlePatch = true;
try {
const resourceManager = this.context.resourceManager;
const originContent = await resourceManager.getRawFileContent(
'bundle.harmony.js',
);
const patched = await Pushy.hdiffPatch(
new Uint8Array(originContent.buffer),
new Uint8Array(entry.content),
);
const outputFile = `${params.unzipDirectory}/bundle.harmony.js`;
const writer = await fileIo.open(
outputFile,
fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
);
const chunkSize = 4096;
let bytesWritten = 0;
const totalLength = patched.byteLength;
while (bytesWritten < totalLength) {
const chunk = patched.slice(bytesWritten, bytesWritten + chunkSize);
await fileIo.write(writer.fd, chunk);
bytesWritten += chunk.byteLength;
}
await fileIo.close(writer);
continue;
} catch (error) {
console.error('Failed to process bundle patch:', error);
throw error;
}
}
await zip.decompressFile(entry.filename, params.unzipDirectory);
}
if (!foundDiff) {
throw new Error('diff.json not found');
}
if (!foundBundlePatch) {
throw new Error('bundle patch not found');
}
await this.copyFromResource(copyList);
}
private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
let foundDiff = false;
let foundBundlePatch = false;
const copyList: Map<string, Array<any>> = new Map();
await zip.decompressFile(params.targetFile, params.unzipDirectory);
const zipFile = await this.processUnzippedFiles(params.unzipDirectory);
for (const entry of zipFile.entries) {
const fn = entry.filename;
if (fn === '__diff.json') {
foundDiff = true;
let jsonContent = '';
const bufferArray = new Uint8Array(entry.content);
for (let i = 0; i < bufferArray.length; i++) {
jsonContent += String.fromCharCode(bufferArray[i]);
}
const obj = JSON.parse(jsonContent);
const copies = obj.copies;
for (const to in copies) {
let from = copies[to];
if (from === '') {
from = to;
}
if (!copyList.has(from)) {
copyList.set(from, []);
}
const target = copyList.get(from);
if (target) {
const toFile = `${params.unzipDirectory}/${to}`;
target.push(toFile);
}
}
continue;
}
if (fn === 'bundle.harmony.js.patch') {
foundBundlePatch = true;
const filePath = params.originDirectory + '/bundle.harmony.js';
const res = fileIo.accessSync(filePath);
if (res) {
const stat = await fileIo.stat(filePath);
const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const fileSize = stat.size;
const originContent = new ArrayBuffer(fileSize);
try {
await fileIo.read(reader.fd, originContent);
const patched = await Pushy.hdiffPatch(
new Uint8Array(originContent),
new Uint8Array(entry.content),
);
const outputFile = `${params.unzipDirectory}/bundle.harmony.js`;
const writer = await fileIo.open(outputFile, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
const chunkSize = 4096;
let bytesWritten = 0;
const totalLength = patched.byteLength;
while (bytesWritten < totalLength) {
const chunk = patched.slice(bytesWritten, bytesWritten + chunkSize);
await fileIo.write(writer.fd, chunk);
bytesWritten += chunk.byteLength;
}
await fileIo.close(writer);
continue;
} finally {
await fileIo.close(reader);
}
}
}
await zip.decompressFile(entry.filename, params.unzipDirectory);
}
if (!foundDiff) {
throw new Error('diff.json not found');
}
if (!foundBundlePatch) {
throw new Error('bundle patch not found');
}
console.info('Patch from PPK completed');
}
private async copyFromResource(
copyList: Map<string, Array<string>>,
): Promise<void> {
try {
const bundlePath = this.context.bundleCodeDir;
const files = await fileIo.listFile(bundlePath);
for (const file of files) {
if (file === '.' || file === '..') continue;
const targets = copyList.get(file);
if (targets) {
let lastTarget: string | undefined;
for (const target of targets) {
console.info(`Copying from resource ${file} to ${target}`);
if (lastTarget) {
await this.copyFile(lastTarget, target);
} else {
const sourcePath = `${bundlePath}/${file}`;
await this.copyFile(sourcePath, target);
lastTarget = target;
}
}
}
}
} catch (error) {
console.error('Copy from resource failed:', error);
throw error;
}
}
private async doCleanUp(params: DownloadTaskParams): Promise<void> {
const DAYS_TO_KEEP = 7;
const now = Date.now();
const maxAge = DAYS_TO_KEEP * 24 * 60 * 60 * 1000;
try {
const files = await fileIo.listFile(params.unzipDirectory);
for (const file of files) {
if (file.startsWith('.')) continue;
const filePath = `${params.unzipDirectory}/${file}`;
const stat = await fileIo.stat(filePath);
if (
now - stat.mtime > maxAge &&
file !== params.hash &&
file !== params.originHash
) {
if (stat.isDirectory()) {
await this.removeDirectory(filePath);
} else {
await fileIo.unlink(filePath);
}
}
}
} catch (error) {
console.error('Cleanup failed:', error);
throw error;
}
}
public async execute(params: DownloadTaskParams): Promise<void> {
try {
switch (params.type) {
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
await this.doFullPatch(params);
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP:
await this.doPatchFromApp(params);
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
await this.doPatchFromPpk(params);
break;
case DownloadTaskParams.TASK_TYPE_CLEANUP:
await this.doCleanUp(params);
break;
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
await this.downloadFile(params);
break;
default:
throw new Error(`Unknown task type: ${params.type}`);
}
params.listener?.onDownloadCompleted(params);
} catch (error) {
console.error('Task execution failed:', error);
if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
try {
if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
await fileIo.unlink(params.targetFile);
} else {
await this.removeDirectory(params.unzipDirectory);
}
} catch (cleanupError) {
console.error('Cleanup after error failed:', cleanupError);
}
}
params.listener?.onDownloadFailed(error);
}
}
}

View File

@@ -0,0 +1,25 @@
export interface DownloadTaskListener {
onDownloadCompleted(params: DownloadTaskParams): void;
onDownloadFailed(error: Error): void;
}
/**
* 下载任务参数类
*/
export class DownloadTaskParams {
// 任务类型常量
static readonly TASK_TYPE_CLEANUP: number = 0; // 保留hash和originHash
static readonly TASK_TYPE_PATCH_FULL: number = 1; // 全量补丁
static readonly TASK_TYPE_PATCH_FROM_APP: number = 2; // 从APP补丁
static readonly TASK_TYPE_PATCH_FROM_PPK: number = 3; // 从PPK补丁
static readonly TASK_TYPE_PLAIN_DOWNLOAD: number = 4; // 普通下载
type: number; // 任务类型
url: string; // 下载URL
hash: string; // 文件哈希值
originHash: string; // 原始文件哈希值
targetFile: string; // 目标文件路径
unzipDirectory: string; // 解压目录路径
originDirectory: string; // 原始文件目录路径
listener: DownloadTaskListener; // 下载监听器
}

View File

@@ -0,0 +1,38 @@
type EventCallback = (data: any) => void;
export class EventHub {
private static instance: EventHub;
private listeners: Map<string, Set<EventCallback>>;
private constructor() {
this.listeners = new Map();
}
public static getInstance(): EventHub {
if (!EventHub.instance) {
EventHub.instance = new EventHub();
}
return EventHub.instance;
}
public on(event: string, callback: EventCallback): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)?.add(callback);
}
public off(event: string, callback: EventCallback): void {
this.listeners.get(event)?.delete(callback);
}
public emit(event: string, data: any): void {
this.listeners.get(event)?.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in event listener for ${event}:`, error);
}
});
}
}

View File

@@ -0,0 +1,59 @@
import { HotReloadConfig, JSBundleProvider, JSBundleProviderError, JSPackagerClientConfig } from 'rnoh';
import fileIo from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import { UpdateContext } from './UpdateContext';
export class FileJSBundleProvider extends JSBundleProvider {
private updateContext: UpdateContext;
private filePath: string = ''
constructor(context: common.UIAbilityContext) {
super();
this.updateContext = new UpdateContext(context);
}
getURL(): string {
return this.updateContext.getBundleUrl();
}
async getBundle(): Promise<ArrayBuffer> {
try {
this.filePath = this.updateContext.getBundleUrl();
const res = fileIo.accessSync(this.filePath);
if (res) {
const file = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_ONLY);
try {
const stat = await fileIo.stat(this.filePath);
const fileSize = stat.size;
const buffer = new ArrayBuffer(fileSize);
const bytesRead = fileIo.readSync(file.fd, buffer, {
offset: 0,
length: fileSize
});
if (bytesRead !== fileSize) {
throw new Error(`Failed to read entire file: read ${bytesRead} of ${fileSize} bytes`);
}
return buffer;
} finally {
fileIo.closeSync(file.fd);
}
}
throw new Error('Update bundle not found');
} catch (error) {
throw new JSBundleProviderError(`Couldn't load JSBundle from ${this.filePath}`, error)
}
}
getAppKeys(): string[] {
return [];
}
getHotReloadConfig(): HotReloadConfig | null {
return null;
}
getJSPackagerClientConfig(): JSPackagerClientConfig | null {
return null;
}
}

View File

@@ -0,0 +1,35 @@
import hilog from '@ohos.hilog';
class Logger {
private domain: number;
private prefix: string;
private format: string = '%{public}s,%{public}s';
private isDebug: boolean;
constructor(prefix: string = 'MyApp', domain: number = 0xFF00, isDebug = false) {
this.prefix = prefix;
this.domain = domain;
this.isDebug = isDebug;
}
debug(...args: string[]): void {
if (this.isDebug) {
hilog.debug(this.domain, this.prefix, this.format, args);
}
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args);
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args);
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args);
}
}
export default new Logger('geolocation', 0xFF00, false)

View File

@@ -0,0 +1,22 @@
import { RNPackage, TurboModulesFactory } from 'rnoh/ts';
import type { TurboModule, TurboModuleContext } from 'rnoh/ts';
import { PushyTurboModule } from './PushyTurboModule';
class PushyTurboModulesFactory extends TurboModulesFactory {
createTurboModule(name: string): TurboModule | null {
if (name === 'Pushy') {
return new PushyTurboModule(this.ctx);
}
return null;
}
hasTurboModule(name: string): boolean {
return name === 'Pushy';
}
}
export class PushyPackage extends RNPackage {
createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory {
return new PushyTurboModulesFactory(ctx);
}
}

View File

@@ -0,0 +1,123 @@
import { TurboModule, TurboModuleContext } from 'rnoh/ts';
import common from '@ohos.app.ability.common';
import dataPreferences from '@ohos.data.preferences';
import { bundleManager } from '@kit.AbilityKit';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import { BusinessError } from '@ohos.base';
import logger from './Logger';
import { UpdateModuleImpl } from './UpdateModuleImpl';
import { UpdateContext } from './UpdateContext';
const TAG = "PushyTurboModule"
export class PushyTurboModule extends TurboModule {
mUiCtx: common.UIAbilityContext
context: UpdateContext
constructor(protected ctx: TurboModuleContext) {
super(ctx);
logger.debug(TAG, ",PushyTurboModule constructor");
this.mUiCtx = ctx.uiAbilityContext
let rnInstance = ctx.rnInstance
this.context = new UpdateContext(this.mUiCtx)
// rnInstance.emitDeviceEvent("Pushy",{code: err.code, message: err.message});
}
getConstants(): Object {
logger.debug(TAG, ",call getConstants");
const context = this.mUiCtx;
const preferencesManager = dataPreferences.getPreferencesSync(context,{ name: 'update' });
const isFirstTime = preferencesManager.getSync("isFirstTime", false) as boolean;
const rolledBackVersion = preferencesManager.getSync("rolledBackVersion", "") as string;
const uuid = preferencesManager.getSync("uuid", "") as string;
const currentVersion = preferencesManager.getSync("currentVersion", "") as string;
const buildTime = preferencesManager.getSync("buildTime", "") as string;
const isUsingBundleUrl = this.context.getIsUsingBundleUrl();
let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
let packageVersion = '';
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags);
packageVersion = bundleInfo?.versionName || "Unknown"
} catch (error) {
console.error("Failed to get bundle info:", error);
}
if (isFirstTime) {
preferencesManager.deleteSync("isFirstTime");
}
if (rolledBackVersion) {
preferencesManager.deleteSync("rolledBackVersion");
}
return {
downloadRootDir: `${context.filesDir}/_update`,
packageVersion,
currentVersion,
buildTime,
isUsingBundleUrl,
isFirstTime,
rolledBackVersion,
uuid,
}
}
async setLocalHashInfo(hash: string, info: string): Promise<boolean> {
logger.debug(TAG, ",call setLocalHashInfo");
return UpdateModuleImpl.setLocalHashInfo(this.context,hash,info);
}
async getLocalHashInfo(hash: string): Promise<string> {
return UpdateModuleImpl.getLocalHashInfo(this.context,hash);
}
async setUuid(uuid: string): Promise<boolean> {
logger.debug(TAG, `,call setUuid`);
return UpdateModuleImpl.setUuid(this.context,uuid);
}
async reloadUpdate(options: { hash: string }): Promise<void> {
logger.debug(TAG, `,call reloadUpdate`);
return UpdateModuleImpl.reloadUpdate(this.context, this.mUiCtx, options);
}
async setNeedUpdate(options: { hash: string }): Promise<boolean> {
logger.debug(TAG, `,call setNeedUpdate`);
return UpdateModuleImpl.setNeedUpdate(this.context, options);
}
async markSuccess(): Promise<boolean> {
logger.debug(TAG, `,call markSuccess`);
return UpdateModuleImpl.markSuccess(this.context);
}
async downloadPatchFromPpk(options: { updateUrl: string; hash: string; originHash: string }): Promise<void> {
logger.debug(TAG, `,call downloadPatchFromPpk`);
return UpdateModuleImpl.downloadPatchFromPpk(this.context, options);
}
async downloadPatchFromPackage(options: { updateUrl: string; hash: string }): Promise<void> {
logger.debug(TAG, `,call downloadPatchFromPackage`);
return UpdateModuleImpl.downloadPatchFromPackage(this.context, options);
}
async downloadFullUpdate(options: { updateUrl: string; hash: string }): Promise<void> {
logger.debug(TAG, `,call downloadFullUpdate`);
return UpdateModuleImpl.downloadFullUpdate(this.context, options);
}
async downloadAndInstallApk(options: { url: string; target: string; hash: string }): Promise<void> {
logger.debug(TAG, `,call downloadAndInstallApk`);
return UpdateModuleImpl.downloadAndInstallApk(this.mUiCtx, options);
}
addListener(eventName: string): void {
logger.debug(TAG, `,call addListener`);
}
removeListeners(count: number): void {
logger.debug(TAG, `,call removeListeners`);
}
}

View File

@@ -0,0 +1,251 @@
import preferences from '@ohos.data.preferences';
import bundleManager from '@ohos.bundle.bundleManager';
import fileIo from '@ohos.file.fs';
import { DownloadTask } from './DownloadTask';
import common from '@ohos.app.ability.common';
import { DownloadTaskParams } from './DownloadTaskParams';
export class UpdateContext {
private context: common.UIAbilityContext;
private rootDir: string;
private preferences: preferences.Preferences;
private static DEBUG: boolean = false;
private static isUsingBundleUrl: boolean = false;
constructor(context: common.UIAbilityContext) {
this.context = context;
this.rootDir = context.filesDir + '/_update';
try {
if (!fileIo.accessSync(this.rootDir)) {
fileIo.mkdirSync(this.rootDir);
}
} catch (e) {
console.error('Failed to create root directory:', e);
}
this.initPreferences();
}
private async initPreferences() {
try {
this.preferences = await preferences.getPreferences(this.context, 'update');
const packageVersion = await this.getPackageVersion();
const storedVersion = await this.preferences.get('packageVersion', '');
if (packageVersion !== storedVersion) {
await this.preferences.clear();
await this.preferences.put('packageVersion', packageVersion);
await this.preferences.flush();
this.cleanUp();
}
} catch (e) {
console.error('Failed to init preferences:', e);
}
}
public async setKv(key: string, value: string): Promise<void> {
await this.preferences.put(key, value);
await this.preferences.flush();
}
public async getKv(key: string): Promise<string> {
return await this.preferences.get(key, '') as string;
}
public async isFirstTime(): Promise<boolean> {
return await this.preferences.get('firstTime', false) as boolean;
}
public async rolledBackVersion(): Promise<string> {
return await this.preferences.get('rolledBackVersion', '') as string;
}
public async markSuccess(): Promise<void> {
await this.preferences.put('firstTimeOk', true);
const lastVersion = await this.preferences.get('lastVersion', '') as string;
const curVersion = await this.preferences.get('currentVersion', '') as string;
if (lastVersion && lastVersion !== curVersion) {
await this.preferences.delete('lastVersion');
await this.preferences.delete(`hash_${lastVersion}`);
}
await this.preferences.flush();
this.cleanUp();
}
public clearFirstTime(): void {
this.preferences.putSync('firstTime', false);
this.preferences.flush();
this.cleanUp();
}
public clearRollbackMark(): void {
this.preferences.putSync('rolledBackVersion', null);
this.preferences.flush();
this.cleanUp();
}
public async downloadFullUpdate(url: string, hash: string, listener: DownloadFileListener): Promise<void> {
try {
const params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
params.url = url;
params.hash = hash;
params.listener = listener;
params.targetFile = `${this.rootDir}/${hash}.ppk`;
const downloadTask = new DownloadTask(this.context);
await downloadTask.execute(params);
} catch (e) {
console.error('Failed to download full update:', e);
}
}
public async downloadFile(url: string, hash: string, fileName: string, listener: DownloadFileListener): Promise<void> {
const params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
params.url = url;
params.hash = hash;
params.listener = listener;
params.targetFile = this.rootDir + '/' + fileName;
const downloadTask = new DownloadTask(this.context);
await downloadTask.execute(params);
}
public async downloadPatchFromPpk(url: string, hash: string, originHash: string, listener: DownloadFileListener): Promise<void> {
const params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
params.url = url;
params.hash = hash;
params.originHash = originHash;
params.listener = listener;
params.targetFile = `${this.rootDir}/${originHash}_${hash}.ppk.patch`;
params.unzipDirectory = `${this.rootDir}/${hash}`;
params.originDirectory = `${this.rootDir}/${params.originHash}`;
const downloadTask = new DownloadTask(this.context);
await downloadTask.execute(params);
}
public async downloadPatchFromPackage(url: string, hash: string, listener: DownloadFileListener): Promise<void> {
try {
const params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP;
params.url = url;
params.hash = hash;
params.listener = listener;
params.targetFile = `${this.rootDir}/${hash}.app.patch`;
params.unzipDirectory = `${this.rootDir}/${hash}`;
const downloadTask = new DownloadTask(this.context);
await downloadTask.execute(params);
} catch (e) {
console.error('Failed to download APK patch:', e);
}
}
public async switchVersion(hash: string): Promise<void> {
try {
const bundlePath = `${this.rootDir}/${hash}/bundle.harmony.js`;
if (!fileIo.accessSync(bundlePath)) {
throw new Error(`Bundle version ${hash} not found.`);
}
const lastVersion = await this.getKv('currentVersion');
await this.setKv('currentVersion', hash);
if (lastVersion && lastVersion !== hash) {
await this.setKv('lastVersion', lastVersion);
}
await this.setKv('firstTime', 'true');
await this.setKv('firstTimeOk', 'false');
await this.setKv('rolledBackVersion', null);
} catch (e) {
console.error('Failed to switch version:', e);
}
}
public static getBundleUrl(context: common.UIAbilityContext, defaultAssetsUrl?: string): string {
return new UpdateContext(context).getBundleUrl(defaultAssetsUrl);
}
public getBundleUrl(defaultAssetsUrl?: string): string {
UpdateContext.isUsingBundleUrl = true;
const currentVersion = this.getCurrentVersion();
if (!currentVersion) {
return defaultAssetsUrl;
}
if (!this.isFirstTime()) {
if (!this.preferences.get('firstTimeOk', true)) {
return this.rollBack();
}
}
let version = currentVersion;
while (version) {
const bundleFile = `${this.rootDir}/${version}/bundle.harmony.js`;
try {
if (!fileIo.accessSync(bundleFile)) {
console.error(`Bundle version ${version} not found.`);
version = this.rollBack();
continue;
}
return bundleFile;
} catch (e) {
console.error('Failed to access bundle file:', e);
version = this.rollBack();
}
}
return defaultAssetsUrl;
}
getPackageVersion(): string {
let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
let packageVersion = '';
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags);
packageVersion = bundleInfo?.versionName || "Unknown";
} catch (error) {
console.error("获取包信息失败:", error);
}
return packageVersion;
}
public getCurrentVersion() : string {
const currentVersion = this.preferences.getSync('currentVersion', '') as string;
return currentVersion;
}
private rollBack(): string {
const lastVersion = this.preferences.getSync('lastVersion', '') as string;
const currentVersion = this.preferences.getSync('currentVersion', '') as string;
if (!lastVersion) {
this.preferences.deleteSync('currentVersion');
} else {
this.preferences.putSync('currentVersion', lastVersion);
}
this.preferences.putSync('firstTimeOk', true);
this.preferences.putSync('firstTime', false);
this.preferences.putSync('rolledBackVersion', currentVersion);
this.preferences.flush();
return lastVersion;
}
private cleanUp(): void {
const params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
params.hash = this.preferences.getSync('currentVersion', '') as string;
params.originHash = this.preferences.getSync('lastVersion', '') as string;
params.unzipDirectory = this.rootDir;
const downloadTask = new DownloadTask(this.context);
downloadTask.execute(params);
}
public getIsUsingBundleUrl(): boolean {
return UpdateContext.isUsingBundleUrl;
}
}
export interface DownloadFileListener {
onDownloadCompleted(params: DownloadTaskParams): void;
onDownloadFailed(error: Error): void;
}

View File

@@ -0,0 +1,200 @@
import { TurboModuleContext } from 'rnoh/ts';
import dataPreferences from '@ohos.data.preferences';
import bundleManager from '@ohos.bundle.bundleManager';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import { UpdateContext } from './UpdateContext';
import { DownloadTaskParams } from './DownloadTaskParams';
import logger from './Logger';
const TAG = "UpdateModuleImpl";
export class UpdateModuleImpl {
static readonly NAME = "Pushy";
static async downloadFullUpdate(
updateContext: UpdateContext,
options: { updateUrl: string; hash: string }
): Promise<void> {
try {
await updateContext.downloadFullUpdate(options.updateUrl, options.hash, {
onDownloadCompleted: (params: DownloadTaskParams) => {
return Promise.resolve();
},
onDownloadFailed: (error: Error) => {
return Promise.reject(error);
}
});
} catch (error) {
logger.error(TAG, `downloadFullUpdate failed: ${error}`);
throw error;
}
}
static async downloadAndInstallApk(
context: common.UIAbilityContext,
options: { url: string; hash: string; target: string }
): Promise<void> {
try {
const want = {
action: 'action.system.home',
parameters: {
uri: 'appmarket://details'
}
};
if (!context) {
throw new Error('获取context失败');
}
await context.startAbility(want);
} catch (error) {
logger.error(TAG, `installApk failed: ${error}`);
throw error;
}
}
static async downloadPatchFromPackage(
updateContext: UpdateContext,
options: { updateUrl: string; hash: string }
): Promise<void> {
try {
await updateContext.downloadPatchFromPackage(options.updateUrl, options.hash, {
onDownloadCompleted: (params: DownloadTaskParams) => {
return Promise.resolve();
},
onDownloadFailed: (error: Error) => {
return Promise.reject(error);
}
});
} catch (error) {
logger.error(TAG, `downloadPatchFromPackage failed: ${error}`);
throw error;
}
}
static async downloadPatchFromPpk(
updateContext: UpdateContext,
options: { updateUrl: string; hash: string; originHash: string }
): Promise<void> {
try {
await updateContext.downloadPatchFromPpk(
options.updateUrl,
options.hash,
options.originHash,
{
onDownloadCompleted: (params: DownloadTaskParams) => {
return Promise.resolve();
},
onDownloadFailed: (error: Error) => {
return Promise.reject(error);
}
}
);
} catch (error) {
logger.error(TAG, `downloadPatchFromPpk failed: ${error}`);
throw new Error(`执行报错: ${error.message}`);
}
}
static async reloadUpdate(
updateContext: UpdateContext,
context: common.UIAbilityContext,
options: { hash: string }
): Promise<void> {
const hash = options.hash;
if (!hash) {
throw new Error('hash不能为空');
}
try {
await updateContext.switchVersion(hash);
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION
);
await context.terminateSelf();
const want = {
bundleName: bundleInfo.name,
abilityName: context.abilityInfo?.name
};
await context.startAbility(want);
} catch (error) {
logger.error(TAG, `reloadUpdate failed: ${error}`);
throw new Error(`pushy:switchVersion failed ${error.message}`);
}
}
static async setNeedUpdate(
updateContext: UpdateContext,
options: { hash: string }
): Promise<boolean> {
const hash = options.hash;
if (!hash) {
throw new Error('hash不能为空');
}
try {
await updateContext.switchVersion(hash);
return true;
} catch (error) {
logger.error(TAG, `setNeedUpdate failed: ${error}`);
throw new Error(`switchVersionLater failed: ${error.message}`);
}
}
static async markSuccess(updateContext: UpdateContext): Promise<boolean> {
try {
await updateContext.markSuccess();
return true;
} catch (error) {
logger.error(TAG, `markSuccess failed: ${error}`);
throw new Error(`执行报错: ${error.message}`);
}
}
static async setUuid(
updateContext: UpdateContext,
uuid: string
): Promise<boolean> {
try {
await updateContext.setKv('uuid', uuid);
return true;
} catch (error) {
logger.error(TAG, `setUuid failed: ${error}`);
throw new Error(`执行报错: ${error.message}`);
}
}
static checkJson(json: string): boolean {
try {
JSON.parse(json);
return true;
} catch {
return false;
}
}
static async setLocalHashInfo(
updateContext: UpdateContext,
hash: string,
info: string
): Promise<boolean> {
if (!this.checkJson(info)) {
await updateContext.setKv(`hash_${hash}`, info);
throw new Error('校验报错:json字符串格式错误');
}
await updateContext.setKv(`hash_${hash}`, info);
return true;
}
static async getLocalHashInfo(
updateContext: UpdateContext,
hash: string
): Promise<string> {
const value = await updateContext.getKv(`hash_${hash}`);
if (!this.checkJson(value)) {
throw new Error('校验报错:json字符串格式错误');
}
return value;
}
}