From 19f5c73a5dc5fdbe92034c8bc8fa973d775f2970 Mon Sep 17 00:00:00 2001 From: jackd320 Date: Mon, 10 Apr 2023 21:22:44 +0800 Subject: [PATCH 1/2] Signed-off-by: jackd320 --- common/src/main/ets/const/update_const.ts | 464 ++++++++++++++ common/src/main/ets/manager/UpdateManager.ts | 549 ++++++++++++++++ .../src/main/ets/manager/UpgradeInterface.ets | 26 + common/src/main/ets/util/NetUtils.ts | 70 ++ common/src/main/ets/util/UpdateUtils.ets | 23 + .../main/resources/base/element/color.json | 4 - feature/ota/src/main/ets/OtaPage.ets | 9 +- feature/ota/src/main/ets/UpgradeAdapter.ets | 22 +- .../main/ets/components/ChangelogContent.ets | 284 +++++++++ .../main/ets/components/ProgressContent.ets | 91 +++ .../dialog/CountDownInstallDialogBuilder.ets | 188 ++++++ .../ota/src/main/ets/dialog/DialogHelper.ets | 206 ++++++ .../ota/src/main/ets/dialog/DialogUtils.ts | 184 ++++++ .../main/ets/dialog/MessageDialogBuilder.ets | 53 ++ .../src/main/ets/manager/OtaUpdateManager.ets | 456 +++++++++++++ .../ota/src/main/ets/manager/StateManager.ets | 598 ++++++++++++++++++ .../main/ets/notify/NotificationHelper.ets | 225 +++++++ .../main/ets/notify/NotificationManager.ts | 87 +++ .../src/main/ets/util/ChangelogParseUtils.ets | 221 +++++++ feature/ota/src/main/ets/util/RouterUtils.ts | 15 +- .../ota/src/main/ets/util/VersionUtils.ets | 91 +++ .../main/resources/base/element/float.json | 2 +- .../main/resources/base/element/string.json | 151 +++++ .../main/resources/tablet/element/float.json | 24 - .../main/resources/zh_CN/element/string.json | 151 +++++ .../src/main/ets/MainAbility/MainAbility.ts | 43 +- .../src/main/ets/ServiceExtAbility/service.ts | 20 +- .../src/main/ets/pages/currentVersion.ets | 135 ++++ product/oh/base/src/main/ets/pages/index.ets | 105 ++- .../oh/base/src/main/ets/pages/newVersion.ets | 352 +++++++++++ .../resources/base/profile/main_pages.json | 4 +- 31 files changed, 4808 insertions(+), 45 deletions(-) create mode 100644 common/src/main/ets/const/update_const.ts create mode 100644 common/src/main/ets/manager/UpdateManager.ts create mode 100644 common/src/main/ets/util/NetUtils.ts create mode 100644 feature/ota/src/main/ets/components/ChangelogContent.ets create mode 100644 feature/ota/src/main/ets/components/ProgressContent.ets create mode 100644 feature/ota/src/main/ets/dialog/CountDownInstallDialogBuilder.ets create mode 100644 feature/ota/src/main/ets/dialog/DialogHelper.ets create mode 100644 feature/ota/src/main/ets/dialog/DialogUtils.ts create mode 100644 feature/ota/src/main/ets/dialog/MessageDialogBuilder.ets create mode 100644 feature/ota/src/main/ets/manager/OtaUpdateManager.ets create mode 100644 feature/ota/src/main/ets/manager/StateManager.ets create mode 100644 feature/ota/src/main/ets/notify/NotificationHelper.ets create mode 100644 feature/ota/src/main/ets/notify/NotificationManager.ts create mode 100644 feature/ota/src/main/ets/util/ChangelogParseUtils.ets delete mode 100644 feature/ota/src/main/resources/tablet/element/float.json create mode 100644 product/oh/base/src/main/ets/pages/currentVersion.ets create mode 100644 product/oh/base/src/main/ets/pages/newVersion.ets diff --git a/common/src/main/ets/const/update_const.ts b/common/src/main/ets/const/update_const.ts new file mode 100644 index 0000000..9370b9c --- /dev/null +++ b/common/src/main/ets/const/update_const.ts @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import update from '@ohos.update'; + +/** + * 升级状态 + * + * @since 2022-06-05 + */ +export enum UpdateState { + /** + * 初始状态 + */ + INIT = 0, + + /** + * 状态--搜包成功 + */ + CHECK_SUCCESS = 12, + + /** + * 状态--下载中 + */ + DOWNLOADING = 20, + + /** + * 状态--下载暂停 + */ + DOWNLOAD_PAUSE = 21, + + /** + * 状态--取消下载 + */ + DOWNLOAD_CANCEL = 22, + + /** + * 状态--下载失败 + */ + DOWNLOAD_FAILED = 23, + + /** + * 状态--下载成功 + */ + DOWNLOAD_SUCCESS = 24, + + /** + * 状态--安装中 + */ + INSTALLING = 80, + + /** + * 状态--安装失败 + */ + INSTALL_FAILED = 81, + + /** + * 状态--安装成功 + */ + INSTALL_SUCCESS = 82, + + /** + * 状态--升级中 + */ + UPGRADING = 90, + + /** + * 状态--升级失败 + */ + UPGRADE_FAILED = 91, + + /** + * 状态--升级成功 + */ + UPGRADE_SUCCESS = 92, +} + +/** + * 升级接口--状态结构体 + * + * @since 2022-06-05 + */ +export interface OtaStatus { + /** + * 状态 + */ + status: number; + + /** + * 进度 + */ + percent: number; + + /** + * 终止原因 + */ + endReason?: string; +} + +/** + * 升级错误码 + * + * @since 2022-06-05 + */ +export enum ErrorCode { + /** + * 错误码--默认失败 + */ + DEFAULT_ERROR = -1, + + /** + * 搜包结果--网络错误 + */ + CHECK_NETWORK_ERR = -2, + + /** + * 搜包结果--搜包中 + */ + CHECK_SYSTEM_BUSY = -207, + + /** + * 错误码--鉴权失败 + */ + AUTH_FAIL = '-208', + + /** + * 错误码--鉴权失败服务错误 + */ + AUTH_SERVER_ERROR = '-209', + + /** + * 错误码--鉴权失败系统错误 + */ + AUTH_SYSTEM_ERROR = '-210', + + /** + * 错误码--网络错误 + */ + NETWORK_ERROR = '-301', + + /** + * 错误码--空间不足 + */ + NO_ENOUGH_MEMORY = '-304', + + /** + * 错误码--检验失败 + */ + VERIFY_PACKAGE_FAIL = '-305', + + /** + * 错误码--部分升級失敗 + */ + UPDATE_PART_FAIL = '-409', + + /** + * 错误码--电量不足 + */ + NO_ENOUGH_BATTERY = '-830', + + /** + * 错误码--网络不允许 + */ + NETWORK_NOT_ALLOW = '3148800' +} + +/** + * 通用常量 + * + * @since 2022-06-05 + */ +export enum UpdateConstant { + /** + * 搜包重试时间 + */ + CHECKING_RETRY_TIME = 5, + + /** + * 搜包等待间隔 + */ + CHECKING_WAITING_TIME_IN_SECONDS = 3, + + /** + * 安装电量阈值 + */ + UPGRADE_BATTERY_THRESHOLD = 30 +} + +/** + * 更新日志结构体 + * + * @since 2022-06-05 + */ +export interface Changelog { + /** + * 默认语言 + */ + defLanguage?: string; + + /** + * 显示类型 + */ + displayType?: number; + + /** + * 所有语言更新日志 + */ + language: Map; +} + +/** + * 更新日志结构体--语言 + * + * @since 2022-06-05 + */ +export interface Language { + /** + * 日志对应语言 + */ + language?: string; + + /** + * 日志特性数组 + */ + featuresArray: Array; +} + +/** + * 更新日志结构体--特性集合 + * + * @since 2022-06-05 + */ +export interface Features { + /** + * 标题 + */ + title: string; + + /** + * 标识 + */ + id: string; + + /** + * 特性类型 + */ + featureModuleType: string; + + /** + * 特性数组 + */ + featureList: Array; + + /** + * 图标 + */ + icon: Icon; +} + +/** + * 更新日志结构体--特性 + * + * @since 2022-06-05 + */ +export interface Feature { + /** + * 子标题 + */ + subTitle: string; + + /** + * 内容数组 + */ + contents: Array; +} + +/** + * 更新日志结构体--图标 + * + * @since 2022-06-05 + */ +export interface Icon { + /** + * 标识 + */ + id: string; + + /** + * 包名 + */ + pkg: string; + + /** + * 数据流字串 + */ + res: string; +} + +/** + * changelog类型 + * + * @since 2022-08-26 + */ +export enum ChangelogType { + /** + * 文本类型 + */ + TEXT = -1, + + /** + * 图文类型 + */ + PICTURE_AND_TEXT = 0, + + /** + * web类型 + */ + WEB_TYPE = 1, +} + +/** + * 动作常量枚举 + * + * @since 2022-06-05 + */ +export enum Action { + /** + * 动作--跳转主页面搜包 + */ + NOTIFICATION_LATER = 'com.ohos.updateapp.later', + + /** + * 动作--跳转主页面搜包 + */ + NOTIFICATION_CHECK = 'com.ohos.updateapp.check', + + /** + * 动作--下载 + */ + NOTIFICATION_DOWNLOAD = 'com.ohos.updateapp.download', + + /** + * 动作--跳转新版本页面安装 + */ + NOTIFICATION_INSTALL = 'com.ohos.updateapp.install', + + /** + * 动作--跳转新版本页面 + */ + NOTIFICATION_DETAIL = 'com.ohos.updateapp.detail', + + /** + * 动作--升级失败跳转主页面搜包 + */ + NOTIFICATION_HOT_UPGRADE_FAILED = 'com.ohos.updateapp.hot_upgrade_failed' +} + +/** + * 接口执行结果 + * + * @since 2022-07-11 + */ +export interface UpgradeData { + /** + * 接口执行结果 + */ + callResult: UpgradeCallResult; + + /** + * 回调数据 + */ + data?: T; + + /** + * 错误结果 + */ + error?: BusinessError +} + +/** + * 接口执行错误码 + * + * @since 2022-07-11 + */ +export enum UpgradeCallResult { + /** + * 接口执行成功 + */ + OK = 1, + + /** + * 接口执行失败 + */ + ERROR = -1, + + /** + * 接口执行超时 + */ + TIME_OUT = -2 +} + +/** + * 倒计时弹窗类型 + * + * @since 2023-02-08 + */ +export enum CountDownDialogType { + /** + * ota20S倒计时 + */ + OTA = 0, + + /** + * ab升级20S倒计时 + */ + OTA_AB = 0, +} + + +/** + * BusinessError + * + * @since 2023-03-10 + */ +export interface BusinessError { + /** + * 数据 + */ + data?: ErrCode[]; +} + +/** + * ErrCode + * + * @since 2023-03-10 + */ +export interface ErrCode { + /** + * 错误码 + */ + errorCode: ErrorCode; +} + +/** + * 包名 + */ +export const PACKAGE_NAME = 'com.ohos.updateapp'; + +/** + * 主ability名 + */ +export const MAIN_ABILITY_NAME = 'com.ohos.updateapp.MainAbility'; \ No newline at end of file diff --git a/common/src/main/ets/manager/UpdateManager.ts b/common/src/main/ets/manager/UpdateManager.ts new file mode 100644 index 0000000..9b17f0e --- /dev/null +++ b/common/src/main/ets/manager/UpdateManager.ts @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type common from '@ohos.app.ability.common'; +import update from '@ohos.update'; +import { PACKAGE_NAME, UpdateState, UpgradeCallResult, } from '../const/update_const'; +import type { BusinessError, OtaStatus, UpgradeData} from '../const/update_const'; +import { LogUtils } from '../util/LogUtils'; + +/** + * 方法超时控制装饰器 + * + * @param timeout 超时事件ms + */ +export function enableTimeOutCheck(timeout?: number): MethodDecorator { + const TIME = 30000; + let realTimeout: number = timeout ?? TIME; + return function inner(target: Object, propertyKey: string, descriptor: PropertyDescriptor): void { + const original = descriptor.value; + descriptor.value = function (...args): Promise { + return new Promise((resolve, reject) => { + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.OK, + }; + const requestTimeout = setTimeout(() => { + upgradeData.callResult = UpgradeCallResult.TIME_OUT; + resolve(upgradeData); + }, realTimeout); + let result: Promise; + try { + result = original.call(this, ...args); + } catch (error) { + LogUtils.error('UpdateManager', 'error: ' + JSON.stringify(error)); + result = null; + } + if (!result) { + clearTimeout(requestTimeout); + upgradeData.callResult = UpgradeCallResult.ERROR; + resolve(upgradeData); // 不处理错误 + return; + } + result.then(innerRes => { + clearTimeout(requestTimeout); + resolve(innerRes); + }).catch(err => { + LogUtils.error('UpdateManager', 'err: ' + JSON.stringify(err)); + clearTimeout(requestTimeout); + upgradeData.callResult = UpgradeCallResult.ERROR; + upgradeData.error = err; + resolve(upgradeData); // 不处理错误 + }); + }); + }; + }; +} + +export interface IUpdate { + bind(subType: number, callback : Function): void; + getOtaStatus(): Promise>; + getNewVersion(): Promise>; + getNewVersionDescription(descVersionDigest: string, descFormat: update.DescriptionFormat, + descLanguage: string): Promise>>; + getCurrentVersionDescription(descFormat: update.DescriptionFormat, + descLanguage: string): Promise>>; + checkNewVersion(): Promise>; + upgrade(upgradeVersionDigest: string, upgradeOrder: number): Promise; + download(downloadVersionDigest: string, downloadNetwork: number, downloadOrder: number): Promise; + cancel(): void; + getCurrentVersionInfo(): Promise>; +} + +/** + * 升级接口管理类 + * + * @since 2022-06-05 + */ +export class UpdateManager implements IUpdate { + private otaUpdater: update.Updater; + + public constructor() { + } + + /** + * 绑定DUE + * + * @param subType 升级类型 + * @param callback 回调 + */ + bind(subType: number, callback: update.UpgradeTaskCallback): void { + let upgradeInfo: update.UpgradeInfo = { + upgradeApp: PACKAGE_NAME, + businessType: { + vendor: update.BusinessVendor.PUBLIC, + subType: subType + } + }; + + try { + this.otaUpdater = update.getOnlineUpdater(upgradeInfo); + let eventClassifyInfo: update.EventClassifyInfo = { eventClassify: 0x01000000, extraInfo: '' }; + this.otaUpdater?.on(eventClassifyInfo, callback); + } catch (error) { + LogUtils.error('UpdateManager', 'otaUpdater init fail ' + JSON.stringify(error)); + } + } + + /** + * 取升级状态 + * + * @return resolve 状态/reject 错误信息 + */ + @enableTimeOutCheck() + async getOtaStatus(): Promise> { + return new Promise((resolve, reject) => { + this.otaUpdater?.getTaskInfo().then((result: update.TaskInfo) => { + this.log(`getOtaStatus result is ${JSON.stringify(result)}`); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.OK + }; + let taskStatus = result?.existTask ? result?.taskBody?.status : UpdateState.INIT; + let otaStatus: OtaStatus = { + status: taskStatus, + percent: result?.taskBody?.progress ?? 0, + endReason: result?.taskBody?.errorMessages?.[0]?.errorCode?.toString() + }; + upgradeData.data = otaStatus; + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError(`getOtaStatus error is ${JSON.stringify(err)}`); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + /** + * 从due数据库取新版本信息 + * + * @return resolve 新版本信息/reject 错误信息 + */ + @enableTimeOutCheck() + async getNewVersion(): Promise> { + return new Promise((resolve, reject) => { + this.otaUpdater?.getNewVersionInfo().then((result: update.NewVersionInfo) => { + this.log('getNewVersion result:' + JSON.stringify(result)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.OK, + data: result + }; + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError('getNewVersion result:' + JSON.stringify(err)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + /** + * 获取新版本描述文件 + * + * @param descVersionDigest 版本摘要 + * @param descFormat 描述文件格式 + * @param descLanguage 描述文件语言 + * @return 新版本描述文件 + */ + @enableTimeOutCheck() + async getNewVersionDescription(descVersionDigest: string, descFormat: update.DescriptionFormat, + descLanguage: string): Promise>> { + let versionDigestInfo: update.VersionDigestInfo = { + versionDigest: descVersionDigest, // 检测结果中的版本摘要信息 + }; + let descriptionOptions: update.DescriptionOptions = { + format: descFormat, + language: descLanguage + }; + return new Promise((resolve, reject) => { + this.otaUpdater?.getNewVersionDescription(versionDigestInfo, + descriptionOptions).then((result: Array) => { + this.log('getNewVersionDescription result:' + JSON.stringify(result)); + let upgradeData: UpgradeData> = { + callResult: UpgradeCallResult.OK, + data: result + }; + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError('getNewVersionDescription err:' + JSON.stringify(err)); + let upgradeData: UpgradeData> = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + /** + * 获取当前版本升级日志 + * + * @param descFormat 描述文件格式 + * @param descLanguage 描述文件语言 + * @return 当前版本描述文件 + */ + @enableTimeOutCheck() + async getCurrentVersionDescription(descFormat: update.DescriptionFormat, + descLanguage: string): Promise>> { + let options: update.DescriptionOptions = { + format: descFormat, + language: descLanguage + }; + return new Promise((resolve, reject) => { + this.otaUpdater?.getCurrentVersionDescription(options).then((result: Array) => { + this.log('getCurrentVersionDescription result:' + JSON.stringify(result)); + let upgradeData: UpgradeData> = { + callResult: UpgradeCallResult.OK, + data: result + }; + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError('getCurrentVersionDescription err:' + JSON.stringify(err)); + let upgradeData: UpgradeData> = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + /** + * 从服务器取搜索新版本 + * + * @return resolve 新版本信息/reject 错误信息 + */ + @enableTimeOutCheck() + async checkNewVersion(): Promise> { + return new Promise((resolve, reject) => { + this.otaUpdater?.checkNewVersion().then((result: update.CheckResult) => { + this.log('checkNewVersion result:' + JSON.stringify(result)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.OK, + data: result, + }; + if (!result?.isExistNewVersion || !result?.newVersionInfo) { + upgradeData.callResult = UpgradeCallResult.ERROR; + } + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError('checkNewVersion err:' + JSON.stringify(err)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + /** + * 升级 + * + * @param upgradeVersionDigest 版本摘要 + * @param upgradeOrder 升级命令 + * @return 调用结果 + */ + upgrade(upgradeVersionDigest: string, upgradeOrder: number): Promise { + return new Promise((resolve, reject) => { + let versionDigestInfo: update.VersionDigestInfo = { + versionDigest: upgradeVersionDigest + }; + let upgradeOptions: update.UpgradeOptions = { + order: upgradeOrder + }; + this.otaUpdater?.upgrade(versionDigestInfo, upgradeOptions).then(() => { + resolve(); + }).catch((err: BusinessError) => { + this.logError('upgrade err:' + JSON.stringify(err)); + reject(err); + }); + }); + } + + /** + * 下载 + * + * @param upgradeVersionDigest 版本摘要 + * @param downloadNetwork 下载网络 + * @param upgradeOrder 下载命令 + * @return 调用结果 + */ + download(downloadVersionDigest: string, downloadNetwork: number, downloadOrder: number): Promise { + return new Promise((resolve, reject) => { + let versionDigestInfo: update.VersionDigestInfo = { + versionDigest: downloadVersionDigest + }; + let downloadOptions: update.DownloadOptions = { + allowNetwork: downloadNetwork, + order: downloadOrder + }; + this.otaUpdater?.download(versionDigestInfo, downloadOptions).then(() => { + this.log('download succeeded.'); + resolve(); + }).catch((err: BusinessError) => { + this.logError('download err:' + JSON.stringify(err)); + reject(err); + }); + }); + } + + /** + * 继续下载 + * + * @param upgradeVersionDigest 版本摘要 + * @param downloadNetwork 下载网络 + * @return 调用结果 + */ + resumeDownload(downloadVersionDigest: string, downloadNetwork: number): Promise { + return new Promise((resolve, reject) => { + let versionDigestInfo: update.VersionDigestInfo = { + versionDigest: downloadVersionDigest + }; + + let resumeDownloadOptions: update.ResumeDownloadOptions = { + allowNetwork: downloadNetwork + }; + this.otaUpdater?.resumeDownload(versionDigestInfo, resumeDownloadOptions).then(() => { + this.log('download succeeded.'); + resolve(); + }).catch((err: BusinessError) => { + this.logError('resumeDownload err:' + JSON.stringify(err)); + reject(err); + }); + }); + } + + /** + * 取消升级 + */ + cancel(): void { + ( this.otaUpdater).cancel(); + } + + /** + * 取当前版本数据 + * + * @return resolve 当前版本信息/reject 错误信息 + */ + @enableTimeOutCheck() + async getCurrentVersionInfo(): Promise> { + return new Promise((resolve, reject) => { + this.otaUpdater?.getCurrentVersionInfo().then((result: update.CurrentVersionInfo) => { + this.log('getCurrentVersionInfo result:' + JSON.stringify(result)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.OK, + data: result + }; + resolve(upgradeData); + }).catch((err: BusinessError) => { + this.logError('getCurrentVersionInfo err:' + JSON.stringify(err)); + let upgradeData: UpgradeData = { + callResult: UpgradeCallResult.ERROR, + error: err + }; + resolve(upgradeData); + }); + }); + } + + private log(message: string): void { + LogUtils.log('UpdateManager', message); + } + + private logError(message: string): void { + LogUtils.error('UpdateManager', message); + } +} + +/** + * OtaStatus缓存/数据处理 + * + * @since 2022-07-30 + */ +export class OtaStatusHolder { + private lastStatusHolder: StatusHolder; + + /** + * 比较otaStatus与lastStatusHolder,并刷新lastStatusHolder + * + * @param otaStatus otaStatus + * @return otaStatus是否是重复事件 + */ + isStatusChangedAndRefresh(otaStatus: OtaStatus, eventId?: update.EventId): boolean { + const STATUS_ALIVE_TIME = 1000; + const newStatus = this.makeStatusHolder(otaStatus, eventId); + let isChanged: boolean; + + if (this.lastStatusHolder != null && + (newStatus.initTime - this.lastStatusHolder.initTime) < STATUS_ALIVE_TIME && + newStatus.status === this.lastStatusHolder.status) { + isChanged = false; + } else { + isChanged = true; + } + this.lastStatusHolder = newStatus; + return isChanged; + } + + /** + * 序列化otaStatus,保存在StatusHolder + * + * @param otaStatus + * @param isCompareProgress 是否考虑进度标志位 + */ + private makeStatusHolder(otaStatus: OtaStatus, eventId?: update.EventId): StatusHolder { + let otaStatusHolder: StatusHolder = { status: '', initTime: new Date().getTime() }; + if (otaStatus.status == null) { + otaStatusHolder.status = '_'; + } else { + otaStatusHolder.status = otaStatus.status + '_'; + } + let status: number = otaStatus.status; + let isCompareStatusProgress: boolean = this.isCompareStatusProgress(status); + if (otaStatus.percent == null || !isCompareStatusProgress) { + otaStatusHolder.status += '_'; + } else { + otaStatusHolder.status += otaStatus.percent + '_'; + } + otaStatusHolder.status += otaStatus.endReason; + otaStatusHolder.status += eventId; + + return otaStatusHolder; + } + + private isCompareStatusProgress(status: number): boolean { + return status === UpdateState.DOWNLOADING || status === UpdateState.INSTALLING; + } +} + +/** + * 保存每次ota_status的信息 + * + * @since 2022-07-18 + */ +export interface StatusHolder { + /** + * 序列化后的status + */ + status: string; + + /** + * status接收的时间,ms + */ + initTime: number; +} + +/** + * 信息 + * + * @since 2022-10-25 + */ +export interface Message { + /** + * 上下文 + */ + context: common.Context; + + /** + * 事件 + */ + eventInfo: update.EventInfo; +} + +/** + * 通知的消息队列 + * + * @since 2022-08-01 + */ +export class MessageQueue { + private queue: Array; + private handleMessage: (context: common.Context, eventInfo: update.EventInfo) => Promise; + + constructor(handleMessage: (context: common.Context, eventInfo: update.EventInfo) => Promise) { + this.queue = new Array(); + this.handleMessage = handleMessage; + } + + async execute(message: Message): Promise { + if (!message) { + return; + } + this.offer(message); + if (this.queue.length === 1) { + await this.loop(); + } + } + + isEmpty(): boolean { + return this.queue?.length === 0; + } + + private async loop(): Promise { + let message: Message = this.peek(); + if (message) { + await this.handleMessage?.(message.context, message.eventInfo).catch(err => { + LogUtils.error('MessageQueue', 'loop err:' + JSON.stringify(err)); + }); + this.poll(); + await this.loop(); + } + } + + private offer(message: Message): void { + if (!message) { + return; + } + this.queue.push(message); + } + + private poll(): void { + if (this.queue.length !== 0) { + this.queue.shift(); + } + } + + private peek(): Message { + if (this.queue.length !== 0) { + return this.queue[0]; + } + return null; + } +} \ No newline at end of file diff --git a/common/src/main/ets/manager/UpgradeInterface.ets b/common/src/main/ets/manager/UpgradeInterface.ets index 38ec094..4fd8ee7 100644 --- a/common/src/main/ets/manager/UpgradeInterface.ets +++ b/common/src/main/ets/manager/UpgradeInterface.ets @@ -15,6 +15,7 @@ import type common from '@ohos.app.ability.common'; import update from '@ohos.update'; +import type { Features, ChangelogType, CountDownDialogType } from '../const/update_const'; /** * 升级控制接口 @@ -53,6 +54,26 @@ export interface ChangelogInfo { * 更新日志 */ content: string; + + /** + * 更新日志类型 + */ + displayType?: ChangelogType; + + /** + * 更新日志--概述 + */ + headFeatures?: Features; + + /** + * 更新日志--更新注意事项 + */ + endFeatures?: Features; + + /** + * 更新日志--具体内容 + */ + contentFeatures?: Array; } /** @@ -65,6 +86,11 @@ export interface CountDownDialogInfo { * 弹框消息体 */ dialogText: Resource; + + /** + * 弹框类型 + */ + dialogType: CountDownDialogType; } /** diff --git a/common/src/main/ets/util/NetUtils.ts b/common/src/main/ets/util/NetUtils.ts new file mode 100644 index 0000000..c054f52 --- /dev/null +++ b/common/src/main/ets/util/NetUtils.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import connection from '@ohos.net.connection'; +import { LogUtils } from '../util/LogUtils'; + +/** + * 网络判断工具类 + * + * @since 2022-08-25 + */ +export namespace NetUtils { + /** + * 网络是否可用 + * + * @return 网络是否可用 + */ + export async function isNetAvailable(): Promise { + return new Promise((resolve, reject) => { + connection.getDefaultNet().then((netHandle) => { + LogUtils.log('NetUtils', 'getDefaultNet data ' + JSON.stringify(netHandle)); + connection.getNetCapabilities(netHandle).then((info) => { + LogUtils.log('NetUtils', 'getNetCapabilities data ' + JSON.stringify(info)); + resolve(info?.bearerTypes?.length !== 0); + }).catch((err) => { + LogUtils.log('NetUtils', 'getNetCapabilities err ' + JSON.stringify(err)); + resolve(false); + }); + }).catch((err) => { + LogUtils.log('NetUtils', 'getDefaultNet err ' + JSON.stringify(err)); + resolve(false); + }); + }); + } + + /** + * 是否是蜂窝网络 + * + * @return 是否是蜂窝网络 + */ + export async function isCellularNetwork(): Promise { + return new Promise((resolve, reject) => { + connection.getDefaultNet().then((netHandle) => { + LogUtils.log('NetUtils', 'getDefaultNet data ' + JSON.stringify(netHandle)); + connection.getNetCapabilities(netHandle).then((info) => { + LogUtils.log('NetUtils', 'getNetCapabilities data ' + JSON.stringify(info)); + resolve(info?.bearerTypes?.length === 1 && info?.bearerTypes?.[0] === connection.NetBearType.BEARER_CELLULAR); + }).catch((err) => { + LogUtils.log('NetUtils', 'getNetCapabilities err ' + JSON.stringify(err)); + resolve(false); + }); + }).catch((err) => { + LogUtils.log('NetUtils', 'getDefaultNet err ' + JSON.stringify(err)); + resolve(false); + }); + }); + } +} \ No newline at end of file diff --git a/common/src/main/ets/util/UpdateUtils.ets b/common/src/main/ets/util/UpdateUtils.ets index 3024745..6932467 100644 --- a/common/src/main/ets/util/UpdateUtils.ets +++ b/common/src/main/ets/util/UpdateUtils.ets @@ -16,6 +16,11 @@ import update from '@ohos.update'; import { LogUtils } from './LogUtils'; +/** + * 日志TAG + */ +const TAG = 'UpdateUtils'; + /** * 接口工具 * @@ -46,4 +51,22 @@ export namespace UpdateUtils { } return ''; } + /** + * 启动Ability + * + * @param context 要启动Ability的context + * @param want 要启动Ability的want + * @param options 配置项 + */ + export function startAbility(context: any, want, options): void { + if (!context || !want) { + LogUtils.error(TAG, 'Failed to start ability with error: context or want is null.'); + return; + } + context.startAbility(want, options).then((data) => { + LogUtils.info(TAG, 'Succeed to start ability with data: ' + JSON.stringify(data)); + }).catch((error) => { + LogUtils.error(TAG, 'Failed to start ability with error: ' + JSON.stringify(error)); + }); + } } \ No newline at end of file diff --git a/common/src/main/resources/base/element/color.json b/common/src/main/resources/base/element/color.json index 1a374f7..4cade0e 100644 --- a/common/src/main/resources/base/element/color.json +++ b/common/src/main/resources/base/element/color.json @@ -3,10 +3,6 @@ { "name": "card_background", "value": "#FFFFFF" - }, - { - "name": "blue", - "value": "#0A59F7" } ] } \ No newline at end of file diff --git a/feature/ota/src/main/ets/OtaPage.ets b/feature/ota/src/main/ets/OtaPage.ets index c95427b..074f111 100644 --- a/feature/ota/src/main/ets/OtaPage.ets +++ b/feature/ota/src/main/ets/OtaPage.ets @@ -14,6 +14,7 @@ */ import update from '@ohos.update'; +import { ChangelogType, CountDownDialogType } from '@ohos/common/src/main/ets/const/update_const'; import { IPage, VersionPageInfo } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; import { UpdateUtils } from '@ohos/common/src/main/ets/util/UpdateUtils'; import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils'; @@ -42,6 +43,8 @@ export class OtaPage implements IPage { if (componentDescriptions) { description = UpdateUtils.obtainDescription(componentDescriptions, componentId); } + let isABInstall = await VersionUtils.isABInstall(); + const countDownTimes = 20; return { version: component.displayVersion, @@ -50,10 +53,14 @@ export class OtaPage implements IPage { changelog: { version: component.displayVersion, size: FormatUtils.formatFileSize(component.size), + displayType: ChangelogType.PICTURE_AND_TEXT, content: description }, countDownDialogInfo: { - dialogText: $r('app.string.count_down_install_label', component.displayVersion), + dialogText: isABInstall ? + $r('app.string.count_down_message_ab', component.displayVersion, countDownTimes) : + $r('app.string.count_down_message_recovery', component.displayVersion, countDownTimes), + dialogType: isABInstall ? CountDownDialogType.OTA_AB : CountDownDialogType.OTA } }; } diff --git a/feature/ota/src/main/ets/UpgradeAdapter.ets b/feature/ota/src/main/ets/UpgradeAdapter.ets index 3f1a788..99e7131 100644 --- a/feature/ota/src/main/ets/UpgradeAdapter.ets +++ b/feature/ota/src/main/ets/UpgradeAdapter.ets @@ -15,7 +15,8 @@ */ import { OtaPage } from './OtaPage'; -import type { IPage } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; +import { NotificationHelper } from './notify/NotificationHelper'; +import type { INotify, IPage } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; /** * 升级适配器 @@ -23,6 +24,7 @@ import type { IPage } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; * @since 2022-12-01 */ export class UpgradeAdapter { + private _notifyInstance: INotify; private _page: IPage; private constructor() { @@ -44,9 +46,21 @@ export class UpgradeAdapter { * @return 支持的升级类型以及UX实例 */ getPageInstance(): IPage { - if (this._page) { - return this._page; + if (!this._page) { + this._page = new OtaPage(); } - return new OtaPage(); + return this._page; + } + + /** + * 取提醒对象 + * + * @return 提醒对象 + */ + getNotifyInstance(): INotify { + if (!this._notifyInstance) { + this._notifyInstance = new NotificationHelper(); + } + return this._notifyInstance; } } \ No newline at end of file diff --git a/feature/ota/src/main/ets/components/ChangelogContent.ets b/feature/ota/src/main/ets/components/ChangelogContent.ets new file mode 100644 index 0000000..9e17edb --- /dev/null +++ b/feature/ota/src/main/ets/components/ChangelogContent.ets @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChangelogInfo } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; +import { + Changelog, + ChangelogType, + Feature, + Features, + Language +} from '@ohos/common/src/main/ets/const/update_const'; +import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import ChangelogParseUtils from '../util/ChangelogParseUtils'; + +/** + * changelog组件 + * + * @since 2022-06-06 + */ +@Component +export struct ChangelogContent { + @State private changelogInfoList: Array = null; + private isCurrentPage: boolean; + isNeedFold: boolean; + @Prop @Watch('parseChangelog') description: string; + @State private isParseChangelogFinished: boolean = false; + + @Builder buildChangelog() { + Column() { + if (this.changelogInfoList && this.isParseChangelogFinished) { + ForEach(this.changelogInfoList, (item: ChangelogInfo, index?: number) => { + ChangelogCard({ + changelogInfo: item, + index: this.changelogInfoList?.length > 1 ? index + 1 : 0, + isCurrentPage: this.isCurrentPage + }); + }) + } else { + ChangelogCard(); + } + if (this.changelogInfoList?.[0]?.endFeatures && !this.isCurrentPage && this.isParseChangelogFinished) { + Column() { + this.setEndFeature(); + } + .alignItems(HorizontalAlign.Start) + .margin({ + left: $r('app.float.changelog_end_content_margin_horizontal'), + right: $r('app.float.changelog_end_content_margin_horizontal'), + bottom: $r('app.float.changelog_tablet_end_content_margin_bottom') + }) + } + }.width('100%') + } + + @Builder setEndFeature() { + Text(this.changelogInfoList?.[0].endFeatures.title) + .fontSize($r('app.float.changelog_content_end_title')) + .fontColor($r('sys.color.ohos_fa_text_secondary')) + .fontWeight(FontWeight.Medium) + .width('100%') + ForEach(this.changelogInfoList?.[0].endFeatures.featureList, (feature: Feature) => { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Start }) { + ForEach(feature.contents, (content: string) => { + Text(content) + .fontColor($r('sys.color.ohos_fa_icon_secondary')) + .fontSize($r('app.float.changelog_end_word')) + .fontWeight(FontWeight.Regular) + .align(Alignment.Start) + .padding({ top: this.changelogInfoList?.[0].endFeatures.featureList.indexOf(feature) == 0 ? + $r('app.float.changelog_tablet_has_feature_padding_top') : + $r('app.float.changelog_tablet_no_feature_padding_top') + }); + }); + } + }); + } + + aboutToAppear() { + this.logInfo('aboutToAppear'); + this.parseChangelog(); + } + + private parseChangelog(): void { + this.logInfo('parseChangelog'); + let list: Array = null; + if (this.description) { + list = JSON.parse(this.description); + } + if (list) { + this.logInfo('aboutToAppear parseXml'); + let isParseSuccess: boolean = true; + list?.forEach((changelogInfo: ChangelogInfo, index: number) => { + ChangelogParseUtils.parseXml(changelogInfo.content, (data) => { + if (!data) { + isParseSuccess = false; + this.logError('aboutToAppear parseXml error'); + return; + } + let language = DeviceUtils.getSystemLanguage(); + changelogInfo.headFeatures = this.getHeaderFeatures(data, language); + changelogInfo.endFeatures = this.getEndFeatures(data, language); + changelogInfo.contentFeatures = this.getContentFeatures(data, language); + if (data.displayType) { + changelogInfo.displayType = data.displayType; + } + }); + }); + this.isParseChangelogFinished = true; + this.changelogInfoList = isParseSuccess ? list : null; + } else { + this.logInfo('description is null'); + } + } + + private getHeaderFeatures(changelog: Changelog, language: string): Features { + return this.getTargetFeatures(changelog, language, 'header'); + } + + private getEndFeatures(changelog: Changelog, language: string): Features{ + return this.getTargetFeatures(changelog, language, 'end'); + } + + private getContentFeatures(changelog: Changelog, language: string): Array { + let featuresList: Array = []; + if (changelog == null || language == null) { + return featuresList; + } + let lang: Language = changelog.language.get(language); + if (lang == null) { + return featuresList; + } + + for (let index = 0; index < lang.featuresArray.length; index++) { + const features = lang.featuresArray[index]; + if (features != null && features.featureModuleType != 'header' && features.featureModuleType != 'end') { + featuresList.push(features); + } + } + return featuresList; + } + + private getTargetFeatures(changelog: Changelog, language: string, type: string): Features { + if (changelog == null || language == null) { + return null; + } + let lang: Language = changelog.language.get(language); + if (lang == null) { + return null; + } + for (let index = 0; index < lang.featuresArray.length; index++) { + const features = lang.featuresArray[index]; + if (features != null && features.featureModuleType == type) { + return features; + } + } + } + + build(): void{ + this.buildChangelog(); + } + + private logInfo(message: string): void { + LogUtils.info('ChangelogContent', message); + } + + private logError(message: string): void { + LogUtils.error('ChangelogContent', message); + } +} + +/** + * 更新日志卡片 + * + * @since 2022-12-01 + */ +@Component +struct ChangelogCard { + changelogInfo: ChangelogInfo; + index: number; + isCurrentPage: boolean = false; + + aboutToAppear() { + } + + build () { + Column() { + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { + Text($r('app.string.title_change_log')) + .fontSize($r('sys.float.ohos_id_text_size_sub_title2')) + .fontWeight(FontWeight.Medium) + .width('100%') + } + + if (!this.changelogInfo) { + this.setDetailFeature($r('app.string.no_info_now')); + } + + if (this.changelogInfo?.headFeatures) { + this.setStartFeature(this.changelogInfo); + } + if (this.changelogInfo?.contentFeatures) { + this.setContentFeatures(this.changelogInfo); + } + } + .padding({ + left: $r('app.float.changelog_tablet_start_content_padding_horizontal'), + right: $r('app.float.changelog_tablet_start_content_padding_horizontal'), + top: $r('app.float.changelog_tablet_start_content_padding_top'), + bottom: $r('app.float.changelog_tablet_start_content_padding_bottom') + }) + .margin({ + left: $r('app.float.changelog_detail_content_margin_horizontal'), + right: $r('app.float.changelog_detail_content_margin_horizontal'), + bottom: $r('app.float.changelog_tablet_start_content_margin_bottom') + }) + .backgroundColor(Color.White) + .alignItems(HorizontalAlign.Start) + .borderRadius($r('app.float.card_border_radius')) + } + + @Builder setDetailFeature(content: string | Resource) { + Text(content) + .opacity(0.6) + .fontSize($r('sys.float.ohos_id_text_size_sub_title3')) + .fontWeight(FontWeight.Regular) + .align(Alignment.Start) + .padding({ top: $r('app.float.changelog_tablet_start_detail_padding_top') }); + } + + @Builder setStartFeature(changelogInfo: ChangelogInfo) { + if (changelogInfo.headFeatures.featureList) { + this.setFeatureContent(changelogInfo.headFeatures.featureList) + } + } + + @Builder setFeatureContent(featureList: Array) { + ForEach(featureList, (item: Feature) => { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Start }) { + ForEach(item.contents, (content: string) => { + Text(content) + .opacity(0.6) + .fontSize($r('sys.float.ohos_id_text_size_sub_title3')) + .fontWeight(FontWeight.Regular) + .align(Alignment.Start) + .padding({ top: featureList.indexOf(item) == 0 ? + $r('app.float.changelog_tablet_has_feature_padding_top') : + $r('app.float.changelog_tablet_no_feature_padding_top') + }); + }); + } + }); + } + + @Builder setContentFeatures(changelogInfo: ChangelogInfo) { + if (this.changelogInfo?.displayType == ChangelogType.TEXT) { + ForEach(changelogInfo.contentFeatures, (features: Features) => { + Text(features.title) + .opacity(0.6) + .fontSize($r('sys.float.ohos_id_text_size_sub_title3')) + .fontWeight(FontWeight.Regular) + .align(Alignment.Start) + .padding({ top: $r('app.float.changelog_tablet_start_content_padding_top')}) + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + if (features.featureList) { + this.setFeatureContent(features.featureList) + } + } + }) + } + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/components/ProgressContent.ets b/feature/ota/src/main/ets/components/ProgressContent.ets new file mode 100644 index 0000000..3987f41 --- /dev/null +++ b/feature/ota/src/main/ets/components/ProgressContent.ets @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { StateManager, UpdateAction } from '../manager/StateManager'; + +/** + * 获取显示进度值 + * + * @param progress 进度值 + * @return 显示进度值 + */ +function getDisplayProgress(progress): number { + let displayProgress = 0; + if (!isNaN(progress)) { + displayProgress = (progress).toFixed(0); + } + return displayProgress; +} + +/** + * 下载进度组件 + * + * @since 2022-06-06 + */ +@Component +export struct ProgressContent { + @StorageProp('updateStatus') + private updateStatus: number = AppStorage.Get('updateStatus'); + @StorageProp('downloadProgress') + private downloadProgress: number = AppStorage.Get('downloadProgress'); + + @Builder ProgressView() { + Stack({ alignContent: Alignment.Center }) { + Progress({ value: this.downloadProgress, style: ProgressStyle.Ring }) + .style({ strokeWidth: $r('app.float.progress_stroke_width') }) + .color($r('app.color.blue')) + .width($r('app.float.new_version_progress_bar_size')) + Flex({ alignItems: ItemAlign.Baseline, justifyContent: FlexAlign.Center }) { + Text(`${getDisplayProgress(this.downloadProgress)}`) + .fontColor(Color.Black) + .fontSize($r('app.float.text_size_progress_bar')) + .fontWeight(FontWeight.Medium) + .margin({ right: $r('app.float.progress_number_margin_right') }) + Text('%') + .fontColor(Color.Black) + .fontSize($r('app.float.text_size_progress_bar_percent')) + .fontWeight(FontWeight.Medium) + .opacity(0.6) + } + } + .width($r('app.float.new_version_progress_bar_size')) + .height($r('app.float.new_version_progress_bar_size')) + } + + build() { + Column() { + if (StateManager.isAllowExecute(this.updateStatus, UpdateAction.SHOW_PROCESS_VIEW)) { + Flex().height($r('app.float.new_version_progress_bar_margin_top')) + this.ProgressView() + Flex().height($r('app.float.new_version_progress_bar_margin_bottom')) + Text(StateManager.getDownloadStateText(this.updateStatus)) + .fontSize($r('app.float.text_size_body')) + .fontWeight(FontWeight.Regular) + .opacity(0.6) + .margin({ bottom: $r('app.float.new_version_download_status_label_margin_bottom')}) + } else { + Column() { + Image($r('app.media.logo')) + .height($r('app.float.progress_logo_other_height')) + .width($r('app.float.progress_logo_other_width')) + } + .padding({ + top: $r('app.float.progress_logo_other_padding_top'), + bottom: $r('app.float.progress_logo_other_padding_bottom') + }) + } + }.flexShrink(0) + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/dialog/CountDownInstallDialogBuilder.ets b/feature/ota/src/main/ets/dialog/CountDownInstallDialogBuilder.ets new file mode 100644 index 0000000..1602bc5 --- /dev/null +++ b/feature/ota/src/main/ets/dialog/CountDownInstallDialogBuilder.ets @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CountDownDialogType } from '@ohos/common/src/main/ets/const/update_const'; +import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils' + +/** + * 倒计时弹框建造者 + * + * @since 2022-06-05 + */ +@CustomDialog +export struct CountDownInstallDialogBuilder { + @StorageProp('configLanguage') + @Watch('onLanguageChange') private configLanguage: string = AppStorage.Get('configLanguage'); + + /** + * 控制器 + */ + controller: CustomDialogController; + + /** + * 文本显示内容 + */ + textString: Resource | string; + + /** + * 取消按钮显示内容 + */ + @State cancelBtnText: string = ''; + + /** + * 确定按钮显示内容 + */ + @State confirmBtnText: string = ''; + + /** + * 弹窗类型 + */ + dialogType: CountDownDialogType; + + /** + * 倒计时 + */ + @State count: number = 20; + + /** + * 取消回调 + */ + cancel: () => void; + + /** + * 确认回调 + */ + confirm: () => void; + + private intervalID: number = null; + + aboutToAppear() { + this.getCount(); + this.initButtonText(); + } + + aboutToDisappear() { + + } + + private getCount() { + this.intervalID = setInterval(() => { + this.count--; + this.initButtonText(); + if (this.count <= 0) { + clearInterval(this.intervalID); + this.controller.close(); + this.confirm(); + } + }, 1000); + } + + private onLanguageChange(): void { + this.initButtonText(); + } + + private initButtonText(): void { + this.cancelBtnText = FormatUtils.toUpperCase(globalThis.abilityContext, $r("app.string.later")); + let confirmBtnRes = null; + if (this.dialogType == CountDownDialogType.OTA) { + confirmBtnRes = $r('app.string.install_now'); + } else if (this.dialogType == CountDownDialogType.OTA_AB) { + confirmBtnRes = $r('app.string.reboot_now'); + } + this.confirmBtnText = FormatUtils.toUpperCase(globalThis.abilityContext, confirmBtnRes, this.count); + } + + @Styles buttonStyles() { + .onClick(() => { + this.controller.close(); + if (this.intervalID != null) { + clearInterval(this.intervalID); + } + this.confirm(); + }) + .backgroundColor($r('sys.float.ohos_id_corner_radius_button')) + .height($r('app.float.dialog_button_height')) + .layoutWeight(1) + .padding({ + left: '0vp', + right: '0vp' + }) + } + + build() { + Column() { + Column() { + Text($r('app.string.software_update')).width('100%').fontSize($r('app.float.text_size_dialog_title')) + Text(this.textString) + .width('100%') + .fontSize($r('app.float.text_size_dialog_body')) + .margin({ + top: $r('app.float.dialog_margin_top'), + bottom: $r('app.float.dialog_content_margin_vertical') + }) + } + .margin({ + left: $r('app.float.dialog_margin_horizontal'), + right: $r('app.float.dialog_margin_horizontal') + }) + + Row() { + Button(this.cancelBtnText) + .onClick(() => { + this.controller.close(); + if (this.intervalID != null) { + clearInterval(this.intervalID); + } + this.cancel(); + }) + .backgroundColor($r('sys.float.ohos_id_corner_radius_button')) + .fontColor($r('app.color.blue')) + .height($r('app.float.dialog_button_height')) + .fontSize($r('app.float.text_size_btn')) + .layoutWeight(1) + .padding({ + right: '0vp', + left: '0vp' + }) + Divider() + .vertical(true) + .height($r('app.float.dialog_divider_height')) + .strokeWidth('0.8vp') + .color($r('app.color.dialog_divider_color')) + .margin({ + left: $r('app.float.divider_margin'), + right: $r('app.float.divider_margin') + }) + if (this.confirmBtnText) { + Button() { + Text(this.confirmBtnText) + .fontColor($r('app.color.blue')) + .fontSize($r('app.float.text_size_btn')) + .textAlign(TextAlign.Center) + } + .buttonStyles() + } + } + .margin({ + left: $r('app.float.dialog_row_margin_horizontal'), + right: $r('app.float.dialog_row_margin_horizontal') + }) + } + .margin({ + top: $r('app.float.dialog_margin_top'), + bottom: $r('app.float.dialog_margin_bottom'), + }) + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/dialog/DialogHelper.ets b/feature/ota/src/main/ets/dialog/DialogHelper.ets new file mode 100644 index 0000000..0043ebc --- /dev/null +++ b/feature/ota/src/main/ets/dialog/DialogHelper.ets @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils'; + +/** + * 弹框辅助者 + * + * @since 2022-06-05 + */ +export namespace DialogHelper { + /** + * 弹框操作接口 + * + * @since 2022-06-05 + */ + export interface DialogOperator { + /** + * 取消 + */ + onCancel?: () => void; + + /** + * 确认 + */ + onConfirm?: () => void; + } + + /** + * 网络弹框 + * + * @param operator 回调 + */ + export function displayNetworkDialog(operator: DialogOperator): void { + AlertDialog.show( + { + title: $r('app.string.software_update'), + message: $r('app.string.network_request'), + primaryButton: { + value: FormatUtils.toUpperCase(globalThis.abilityContext, $r('app.string.cancel')), + action: () => { + logInfo('Callback when the first button is clicked'); + operator.onCancel?.(); + }, + backgroundColor: $r('sys.float.ohos_id_corner_radius_button') + }, + secondaryButton: { + value: $r('app.string.ok'), + action: () => { + logInfo('Callback when the second button is clicked'); + operator.onConfirm?.(); + }, + backgroundColor: $r('sys.float.ohos_id_corner_radius_button') + }, + cancel: () => { + logInfo('Closed callbacks'); + }, + alignment: DeviceUtils.getDialogLocation(), + offset: ({ + dx: "0vp", + dy: DeviceUtils.getDialogOffsetY() + }), + autoCancel: false + } + ) + } + + /** + * 升级失败弹框 + * + * @param operator 回调 + */ + export function displayUpgradeFailDialog(operator ?: DialogOperator): void { + defaultNoTitleDialog($r('app.string.update_fail'), operator); + } + + /** + * 下载失败弹框 + * + * @param operator 回调 + */ + export function displayDownloadFailDialog(operator ?: DialogOperator): void { + defaultNoTitleDialog($r('app.string.download_fail'), operator); + } + + /** + * 无网弹框 + * + * @param operator 回调 + */ + export function displayNoNetworkDialog(operator ?: DialogOperator): void { + defaultKnowDialog($r('app.string.net_error_title'), $r('app.string.net_error_content'), operator); + } + + /** + * 电量不足弹框 + * + * @param operator 回调 + */ + export function displayNotEnoughBatteryDialog(operator ?: DialogOperator): void { + defaultKnowDialog($r('app.string.battery_not_enough_title'), $r('app.string.battery_not_enough_content', + FormatUtils.getNumberFormat(0.3)), operator); + } + + /** + * 空间不足弹框 + * + * @param operator 回调 + */ + export function displayNotEnoughSpaceDialog(operator ?: DialogOperator): void { + defaultKnowDialog($r('app.string.space_not_enough_title'), $r('app.string.space_not_enough_content'), operator); + } + + /** + * 文件校验失败弹框 + * + * @param operator 回调 + */ + export function displayVerifyFailDialog(operator ?: DialogOperator): void { + defaultNoTitleDialog($r('app.string.package_verify_fail'), operator); + } + + /** + * 默认提示弹框 + * + * @param title 标题 + * @param message 内容 + * @param operator 回调 + */ + function defaultKnowDialog(title: string | Resource, message: string | Resource, operator ?: DialogOperator): void { + showDialog(title, message, $r('app.string.button_know'), operator); + } + + /** + * 默认无标题弹框 + * + * @param message 内容 + * @param operator 回调 + */ + function defaultNoTitleDialog(message: string | Resource, operator ?: DialogOperator): void { + showDialog(null, message, $r('app.string.button_know'), operator); + } + + /** + * 弹框 + * + * @param title 标题 + * @param message 内容 + * @param confirmText 确认按钮显示内容 + * @param operator 回调 + */ + function showDialog(title: string | Resource, message: string | Resource, confirmText?: string | Resource, + operator ?: DialogOperator): void { + AlertDialog.show( + { + title: title, + message: message, + confirm: { + value: confirmText, + action: () => { + logInfo('defaultKnowDialog button is clicked'); + if (operator) { + operator.onConfirm(); + } + }, + backgroundColor: $r('sys.float.ohos_id_corner_radius_button') + }, + cancel: () => { + logInfo('Closed callbacks'); + if(operator) { + operator.onCancel(); + } + }, + alignment: DeviceUtils.getDialogLocation(), + offset: ({ + dx: '0vp', + dy: DeviceUtils.getDialogOffsetY() + }), + autoCancel: false, + } + ) + } + + /** + * info级别日志打印 + * + * @param message 日志内容 + */ + function logInfo(message: string): void { + LogUtils.info('DialogHelper', message); + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/dialog/DialogUtils.ts b/feature/ota/src/main/ets/dialog/DialogUtils.ts new file mode 100644 index 0000000..2d01ff9 --- /dev/null +++ b/feature/ota/src/main/ets/dialog/DialogUtils.ts @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type common from '@ohos.app.ability.common'; +import update from '@ohos.update'; +import type { OtaStatus } from '@ohos/common/src/main/ets/const/update_const'; +import { MAIN_ABILITY_NAME, PACKAGE_NAME, UpdateState } from '@ohos/common/src/main/ets/const/update_const'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { UpdateUtils } from '@ohos/common/src/main/ets/util/UpdateUtils'; +import { OtaUpdateManager } from '../manager/OtaUpdateManager'; +import RouterUtils from '../util/RouterUtils'; +import { DialogHelper } from './DialogHelper'; + +const TIME_OUT_FOR_START_ABILITY = 500; + +/** + * 装饰器--弹框时,前台判断处理 + */ +function foregroundCheck() { + return function inner(target: unknown, propertyKey: string, descriptor: PropertyDescriptor): void { + const original = descriptor.value; + descriptor.value = function (context: common.Context, otaStatus: OtaStatus, + eventId?: update.EventId, ...args): void { + if (globalThis.AbilityStatus !== 'ON_FOREGROUND') { + globalThis.reNotify = true; + globalThis.otaStatusFromService = otaStatus; + globalThis.eventIdFromService = eventId; + LogUtils.log('foregroundCheck', 'do startMainAbilityIndex.'); + + // 应用在后台时,无法弹框,需杀掉ability后,重新拉起界面弹框 + globalThis.abilityContext?.terminateSelf(); + setTimeout(() => { + startMainAbilityIndex(context); + }, TIME_OUT_FOR_START_ABILITY); + return; + } + original.call(this, ...args); + }; + }; +} + +function startMainAbilityIndex(context: common.Context): void { + let want = { + bundleName: PACKAGE_NAME, + abilityName: MAIN_ABILITY_NAME, + uri: 'pages/newVersion', + }; + UpdateUtils.startAbility(context, want, null); +} + +/** + * 重试下载动作 + */ +const retryDownloadAction = { + onConfirm: (): void => { + OtaUpdateManager.getInstance().setUpdateState(UpdateState.CHECK_SUCCESS); + }, + onCancel: (): void => { + OtaUpdateManager.getInstance().setUpdateState(UpdateState.CHECK_SUCCESS); + }, +}; + +/** + * 重试安装动作 + */ +const retryUpgradeAction = { + onConfirm: (): void => { + OtaUpdateManager.getInstance().setUpdateState(UpdateState.DOWNLOAD_SUCCESS); + }, + onCancel: (): void => { + OtaUpdateManager.getInstance().setUpdateState(UpdateState.DOWNLOAD_SUCCESS); + }, +}; + +/** + * 重试检测动作 + */ +const retryCheckAction = { + onConfirm: (): void => { + RouterUtils.singletonHomePage(); + }, onCancel: (): void => { + RouterUtils.singletonHomePage(); + }, +}; + +/** + * 弹框工具类 + * + * @since 2022-12-05 + */ +export class DialogUtils { + /** + * 下载空间不足弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showDownloadNotEnoughSpaceDialog(context: common.Context, otaStatus: OtaStatus, + eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showDownloadNotEnoughSpaceDialog'); + DialogHelper.displayNotEnoughSpaceDialog(retryDownloadAction); + } + + /** + * 下载断网弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showDownloadNoNetworkDialog(context: common.Context, otaStatus: OtaStatus, + eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showDownloadNoNetworkDialog'); + DialogHelper.displayNoNetworkDialog(); + } + + /** + * 校验失败弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showVerifyFailDialog(context: common.Context, otaStatus: OtaStatus, eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showVerifyFailDialog'); + DialogHelper.displayVerifyFailDialog(retryCheckAction); + } + + /** + * 下载失败默认弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showDownloadFailDialog(context: common.Context, otaStatus: OtaStatus, eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showDownloadFailDialog'); + DialogHelper.displayDownloadFailDialog(retryCheckAction); + } + + /** + * 安装空间不足弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showUpgradeNotEnoughSpaceDialog(context: common.Context, otaStatus: OtaStatus, + eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showUpgradeNotEnoughSpaceDialog'); + DialogHelper.displayNotEnoughSpaceDialog(retryUpgradeAction); + } + + /** + * 安装电量不足弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showUpgradeNotEnoughBatteryDialog(context: common.Context, otaStatus: OtaStatus, + eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showUpgradeNotEnoughBatteryDialog'); + DialogHelper.displayNotEnoughBatteryDialog(); + } + + /** + * 安装失败默认弹框 + * + * @param context 上下文 + */ + @foregroundCheck() + static showUpgradeFailDialog(context: common.Context, otaStatus: OtaStatus, eventId?: update.EventId): void { + LogUtils.log('DialogUtils', 'showUpgradeFailDialog'); + DialogHelper.displayUpgradeFailDialog(retryCheckAction); + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/dialog/MessageDialogBuilder.ets b/feature/ota/src/main/ets/dialog/MessageDialogBuilder.ets new file mode 100644 index 0000000..4b659e7 --- /dev/null +++ b/feature/ota/src/main/ets/dialog/MessageDialogBuilder.ets @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 安装流程框构造者 + * + * @since 2022-06-05 + */ +@CustomDialog +export struct MessageDialogBuilder { + /** + * 内容 + */ + message: ResourceStr; + + /** + * 控制器 + */ + controller: CustomDialogController; + + build() { + Column() { + Row() { + LoadingProgress() + .width($r('app.float.message_dialog_loading_progress_width')) + .height($r('app.float.message_dialog_loading_progress_height')) + .color(Color.Gray) + Text(this.message) + .width('100%') + .fontSize($r('app.float.text_size_dialog_body')) + .padding({ left: $r('app.float.message_dialog_text_padding_left') }) + } + } + .margin({ + left: $r('app.float.dialog_margin_horizontal'), + right: $r('app.float.dialog_margin_horizontal'), + top: $r('app.float.message_dialog_margin_vertical'), + bottom: $r('app.float.message_dialog_margin_vertical') + }) + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/manager/OtaUpdateManager.ets b/feature/ota/src/main/ets/manager/OtaUpdateManager.ets new file mode 100644 index 0000000..e617369 --- /dev/null +++ b/feature/ota/src/main/ets/manager/OtaUpdateManager.ets @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type common from '@ohos.app.ability.common'; +import update from '@ohos.update'; +import type Want from '@ohos.app.ability.Want'; +import { ErrorCode } from '@ohos/common/src/main/ets/const/update_const'; +import type { OtaStatus, UpgradeData, } from '@ohos/common/src/main/ets/const/update_const'; +import { UpdateState, UpgradeCallResult, } from '@ohos/common/src/main/ets/const/update_const'; +import type { Message } from '@ohos/common/src/main/ets/manager/UpdateManager'; +import { + UpdateManager, + MessageQueue, + OtaStatusHolder, +} from '@ohos/common/src/main/ets/manager/UpdateManager'; +import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import type { BaseState } from '../manager/StateManager'; +import { StateManager } from '../manager/StateManager'; +import { NotificationManager } from '../notify/NotificationManager'; +import VersionUtils from '../util/VersionUtils'; +import { UpgradeAdapter } from '../UpgradeAdapter'; + +/** + * 升级接口管理类 + * + * @since 2022-06-05 + */ +export class OtaUpdateManager { + private static readonly KEY = 'EventInfo'; + private _updateStatus: number; + private _downloadProgress: number; + private lastStatus: number; + private stateObj: BaseState; + private otaStatusHolder: OtaStatusHolder; + private updateManager: UpdateManager; + private messageQueue: MessageQueue; + + /** + * 单例--升级管理类对象实例 + * + * @return 升级管理类对象实例 + */ + static getInstance(): OtaUpdateManager { + return globalThis.otaUpdateManager ?? new OtaUpdateManager(); + } + + private constructor() { + this.log('OtaUpdateManager init.'); + globalThis.otaUpdateManager = this; + this.otaStatusHolder = new OtaStatusHolder(); + this.messageQueue = new MessageQueue(this.handleMessage.bind(this)); + + this.updateManager = new UpdateManager(); + this.updateManager.bind(update.BusinessSubType.FIRMWARE, this.notifyUpdateStatusRemote.bind(this)); + } + + /** + * 取升级状态 + * + * @return resolve 状态/reject 错误信息 + */ + async getOtaStatus(): Promise> { + return new Promise((resolve, reject) => { + this.updateManager.getOtaStatus().then((result: UpgradeData) => { + if (result?.callResult === UpgradeCallResult.OK) { + this.refreshState(result?.data); + } + resolve(result); + }); + }); + } + + /** + * 从due数据库取新版本信息 + * + * @return resolve 新版本信息/reject 错误信息 + */ + async getNewVersion(): Promise> { + return new Promise((resolve, reject) => { + this.updateManager.getNewVersion().then((result: UpgradeData) => { + if (result?.callResult === UpgradeCallResult.OK) { + globalThis.cachedNewVersionInfo = result?.data; + resolve(result); + } else { + resolve(result); + } + }); + }); + } + + /** + * 获取新版本描述文件 + * + * @return 新版本描述文件 + */ + async getNewVersionDescription(): Promise>> { + let versionDigest: string = await VersionUtils.getNewVersionDigest(); + return this.updateManager.getNewVersionDescription(versionDigest, update.DescriptionFormat.STANDARD, + DeviceUtils.getSystemLanguage()); + } + + /** + * 获取当前版本升级日志 + * + * @return 当前版本描述文件 + */ + async getCurrentVersionDescription(): Promise>> { + return this.updateManager.getCurrentVersionDescription(update.DescriptionFormat.STANDARD, + DeviceUtils.getSystemLanguage()); + } + + /** + * 从服务器取搜索新版本 + * + * @return resolve 新版本信息/reject 错误信息 + */ + async checkNewVersion(): Promise> { + return new Promise((resolve, reject) => { + this.updateManager.checkNewVersion().then((result: UpgradeData) => { + if (result?.callResult === UpgradeCallResult.OK) { + globalThis.cachedNewVersionInfo = result?.data?.newVersionInfo; + resolve(result); + } else { + resolve(result); + } + }); + }); + } + + /** + * 升级 + * + * @param order 安装指令 + */ + async upgrade(order: update.Order = update.Order.INSTALL_AND_APPLY): Promise { + let versionDigest: string = await VersionUtils.getNewVersionDigest(); + return new Promise((resolve, reject) => { + this.updateManager.upgrade(versionDigest, order).then(()=> { + resolve(); + }).catch(err => { + let status: OtaStatus = { + status: order === update.Order.APPLY ? UpdateState.INSTALL_SUCCESS : UpdateState.DOWNLOAD_SUCCESS, + percent: 100, + endReason: err?.data?.[0]?.errorCode?.toString() || ErrorCode.DEFAULT_ERROR, + }; + this.notifyUpdateStatus(status, globalThis.abilityContext); + this.logError('upgrade err:' + JSON.stringify(err)); + reject(err); + }); + }); + } + + /** + * 下载 + * + * @param downloadNetwork 下载网络类型,默认为wifi + */ + async download(downloadNetwork: update.NetType = update.NetType.WIFI): Promise { + UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + let versionDigest: string = await VersionUtils.getNewVersionDigest(); + this.setDownloadProgress(0); + this.updateManager.download(versionDigest, downloadNetwork, update.Order.DOWNLOAD) + .catch(err => { + let status: OtaStatus = { + status: UpdateState.CHECK_SUCCESS, + percent: 0, + endReason: err?.data?.[0]?.errorCode?.toString() || '', + }; + this.notifyUpdateStatus(status, globalThis.abilityContext); + }); + } + + /** + * 继续下载 + */ + async resumeDownload(): Promise { + let versionDigest: string = await VersionUtils.getNewVersionDigest(); + this.setUpdateState(UpdateState.DOWNLOADING); + this.updateManager.resumeDownload(versionDigest, update.NetType.WIFI).then(result => { + this.log('resumeDownload result:' + JSON.stringify(result)); + }).catch(err => { + let status: OtaStatus = { + status: UpdateState.DOWNLOAD_PAUSE, + percent: this.getDownloadProgress(), + endReason: err?.data?.[0]?.errorCode?.toString() || '', + }; + this.notifyUpdateStatus(status, globalThis.abilityContext); + }); + } + + /** + * 取消升级 + */ + async cancel(): Promise { + this.setUpdateState(UpdateState.CHECK_SUCCESS); + this.setDownloadProgress(0); + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + this.updateManager.cancel(); + } + + /** + * 取当前版本数据 + * + * @return resolve 当前版本信息/reject 错误信息 + */ + async getCurrentVersionInfo(): Promise> { + return this.updateManager.getCurrentVersionInfo(); + } + + /** + * 取升级状态缓存数据 + * + * @return 升级状态 + */ + getUpdateState(): number { + return this._updateStatus; + } + + /** + * 设置升级状态缓存数据 + * + * @param value 状态 + */ + setUpdateState(value): void { + if (this._updateStatus !== Number(value) && value !== undefined && value !== null) { + this._updateStatus = Number(value); + AppStorage.Set('updateStatus', this._updateStatus); + } + } + + /** + * 取升级进度 + * + * @return 升级进度 + */ + getDownloadProgress(): number { + return this._downloadProgress; + } + + /** + * 设置进度 + * + * @param value 进度 + */ + setDownloadProgress(value): void { + if (this._downloadProgress !== value && value !== undefined && value !== null) { + this._downloadProgress = value; + AppStorage.Set('downloadProgress', this._downloadProgress); + } + } + + /** + * 取状态对象 + * + * @param status 状态 + */ + getStateObj(status: number): BaseState { + if (this.stateObj?.state === status) { + return this.stateObj; + } else { + return StateManager.createInstance(status); + } + } + + private refreshState(otaStatus: OtaStatus): void { + if (!this.stateObj || this.lastStatus !== otaStatus.status) { + this.stateObj = StateManager.createInstance(otaStatus); + } + this.stateObj.refresh(otaStatus); + this.lastStatus = otaStatus.status; + this.setUpdateState(this.stateObj.state); + this.setDownloadProgress(this.stateObj.percent); + } + + /** + * 状态刷新 + * + * @param otaStatus 状态 + */ + private async notifyUpdateStatusRemote(eventInfo: update.EventInfo): Promise { + this.log(`notifyUpdateStatusRemote ${JSON.stringify(eventInfo)}`); + let message: Message = { + context: globalThis.extensionContext || globalThis.abilityContext, + eventInfo: eventInfo, + }; + + this.messageQueue.execute(message); + } + + private async handleMessage(context: common.Context, eventInfo: update.EventInfo): Promise { + let otaStatus: OtaStatus = this.getFormattedOtaStatus(eventInfo); + if (this.isTerminalState(otaStatus)) { + globalThis.lastVersionName = await VersionUtils.obtainNewVersionName(eventInfo?.taskBody); + } + let versionDigest: string = eventInfo?.taskBody?.versionDigestInfo?.versionDigest ?? ''; + await this.notifyUpdateStatus(otaStatus, context, versionDigest, eventInfo?.eventId); + } + + private async notifyUpdateStatus(otaStatus: OtaStatus, context: common.Context, verDigest?: string, + eventId?: update.EventId): Promise { + this.log('notifyUpdateStatus:' + JSON.stringify(otaStatus)); + this.refreshState(otaStatus); + + if (!this.otaStatusHolder.isStatusChangedAndRefresh(otaStatus, eventId)) { + LogUtils.warn('UpdateManager', 'notifyUpdateStatus is repeating, abandon.'); + return; + } + if (!globalThis.cachedNewVersionInfo && !this.isTerminalState(otaStatus)) { + await this.getNewVersion(); + } + + await StateManager.createInstance(otaStatus).notify(context, eventId); + } + + private isTerminalState(otaStatus: OtaStatus): boolean { + let status = otaStatus?.status ?? UpdateState.INIT; + if (status === UpdateState.INIT || status === UpdateState.DOWNLOAD_FAILED || + status === UpdateState.INSTALL_FAILED || status === UpdateState.UPGRADE_SUCCESS || + status === UpdateState.UPGRADE_FAILED) { + return true; + } + return false; + } + + /** + * 收到推送消息 + * + * @param otaStatus 状态数据 + */ + async onReceivedUpdateServiceMessage(eventInfo: update.EventInfo): Promise { + this.log('receives from onReceivedUpdateServiceMessage:' + JSON.stringify(eventInfo)); + let message: Message = { + context: globalThis.extensionContext, + eventInfo: eventInfo, + }; + await this.messageQueue.execute(message); + } + + /** + * 收到page推送消息 + * + * @param otaStatus 状态数据 + */ + async onReceivedUpdatePageMessage(otaStatus: OtaStatus): Promise { + this.log('receives from onReceivedUpdatePageMessage:' + JSON.stringify(otaStatus)); + this.notifyUpdateStatus(otaStatus, globalThis.abilityContext); + } + + /** + * 处理推送消息 + * + * @param want 推送数据 + * @param context 上下文 + */ + public async handleWant(want: Want, context: common.Context): Promise { + let action: string = want?.action ?? ''; + if (await NotificationManager.handleAction(action, context)) { + return; + } + let eventInfo = this.wantParser(want); + if (!eventInfo?.eventId) { + this.log('eventInfo?.eventId is null'); + return; + } + await this.onReceivedUpdateServiceMessage(eventInfo); + } + + /** + * 是否升级终止 + * + * @return 是否升级终止 + */ + public isTerminal(): boolean { + return this.isTerminalState(this.stateObj?.otaStatus); + } + + private wantParser(want: Want): update.EventInfo { + let eventInfo: update.EventInfo; + let eventInfoStr = want.parameters[OtaUpdateManager.KEY]; + if (typeof eventInfoStr === 'string') { + eventInfo = JSON.parse(eventInfoStr); + } + return eventInfo; + } + + private log(message: string): void { + LogUtils.log('UpdateManager', message); + } + + private logError(message: string): void { + LogUtils.error('UpdateManager', message); + } + + /** + * 通过eventInfo获取OtaStatus + * 同时对status、percent数据进行调整 + * + * @param eventInfo 事件 + * @return OtaStatus 实例 + */ + private getFormattedOtaStatus(eventInfo: update.EventInfo): OtaStatus { + let endReason: string = eventInfo.taskBody?.errorMessages?.[0]?.errorCode?.toString(); + let otaStatus: OtaStatus = { + status: eventInfo.taskBody?.status, + percent: eventInfo.taskBody?.progress, + endReason: !endReason || endReason === '0' ? null : endReason, + }; + if (!otaStatus.status) { + otaStatus.status = this.getUpdateStateFromEventId(eventInfo.eventId); + } + return otaStatus; + } + + private getUpdateStateFromEventId(eventId: update.EventId): UpdateState { + let status: UpdateState; + switch (eventId) { + case update.EventId.EVENT_TASK_RECEIVE: + status = UpdateState.CHECK_SUCCESS; + break; + case update.EventId.EVENT_TASK_CANCEL: + status = UpdateState.INIT; + break; + case update.EventId.EVENT_DOWNLOAD_START: + status = UpdateState.DOWNLOADING; + break; + case update.EventId.EVENT_DOWNLOAD_SUCCESS: + status = UpdateState.DOWNLOAD_SUCCESS; + break; + case update.EventId.EVENT_DOWNLOAD_FAIL: + status = UpdateState.DOWNLOAD_FAILED; + break; + case update.EventId.EVENT_UPGRADE_SUCCESS: + status = UpdateState.UPGRADE_SUCCESS; + break; + case update.EventId.EVENT_UPGRADE_FAIL: + status = UpdateState.UPGRADE_FAILED; + break; + default: + break; + } + return status; + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/manager/StateManager.ets b/feature/ota/src/main/ets/manager/StateManager.ets new file mode 100644 index 0000000..a98abf0 --- /dev/null +++ b/feature/ota/src/main/ets/manager/StateManager.ets @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type common from '@ohos.app.ability.common'; +import update from '@ohos.update'; +import { ErrorCode, OtaStatus, UpdateState } from '@ohos/common/src/main/ets/const/update_const'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { DialogUtils } from '../dialog/DialogUtils'; +import { OtaUpdateManager } from '../manager/OtaUpdateManager'; +import VersionUtils from '../util/VersionUtils'; +import ToastUtils from '../util/ToastUtils'; +import { UpgradeAdapter } from '../UpgradeAdapter'; +import { NotificationManager } from '../notify/NotificationManager'; + +/** + * 状态工厂 + * + * @since 2022-06-10 + */ +export namespace StateManager { + /** + * 是否允许执行该升级行为 + * + * @param status 状态 + * @param action 行为 + * @return 是否允许 + */ + export function isAllowExecute(status: number, action: UpdateAction): boolean { + let stateObj: BaseState = OtaUpdateManager.getInstance().getStateObj(status); + return stateObj.actionSet.indexOf(action) != -1; + } + + /** + * 取下载状态描述 + * + * @param status 状态 + * @return 下载状态描述 + */ + export function getDownloadStateText(status: number): string | Resource { + return OtaUpdateManager.getInstance().getStateObj(status).downloadStateText; + } + + /** + * 取按钮文字 + * + * @param status 状态 + * @return 按钮文字 + */ + export function getButtonText(status: number): string | Resource { + return OtaUpdateManager.getInstance().getStateObj(status).buttonText; + } + + /** + * 按钮点击是否可点击 + * + * @param status 状态 + * @return 是否可点击 + */ + export function isButtonEnable(status: number): boolean { + return OtaUpdateManager.getInstance().getStateObj(status).isButtonClickable; + } + + /** + * 取按钮点击行为 + * + * @param status 状态 + * @return 按钮点击行为 + */ + export function getButtonClickAction(status: number): UpdateAction { + return OtaUpdateManager.getInstance().getStateObj(status).buttonClickAction; + } + + /** + * 创造状态实例 + * + * @param status 状态 + * @return 实例对象 + */ + export function createInstance(status: OtaStatus | number): BaseState { + let state: number = (typeof status === 'number') ? status : status?.status; + let stateObject: BaseState = null; + switch (state) { + case UpdateState.DOWNLOAD_CANCEL: // fall through + case UpdateState.CHECK_SUCCESS: + stateObject = new CheckSuccess(); + break; + case UpdateState.DOWNLOADING: + stateObject = new Downloading(); + break; + case UpdateState.DOWNLOAD_PAUSE: + stateObject = new DownloadPause(); + break; + case UpdateState.DOWNLOAD_SUCCESS: + stateObject = new DownloadSuccess(); + break; + case UpdateState.INSTALLING: + stateObject = new Installing(); + break; + case UpdateState.INSTALL_FAILED: + stateObject = new InstallFailed(); + break; + case UpdateState.DOWNLOAD_FAILED: + stateObject = new DownloadFailed(); + break; + case UpdateState.INSTALL_SUCCESS: // fall through + stateObject = new InstallSuccess(); + break; + case UpdateState.UPGRADING: + stateObject = new Upgrading(); + break; + case UpdateState.UPGRADE_SUCCESS: + stateObject = new UpgradeSuccess(); + break; + case UpdateState.UPGRADE_FAILED: + stateObject = new UpgradeFailed(); + break; + default: + stateObject = new Init(); + break; + } + if (typeof status !== 'number' && status != null) { + stateObject.refresh(status); + } + return stateObject; + } +} + +/** + * 升级行为 + * + * @since 2022-06-10 + */ +export enum UpdateAction { + /** + * 搜索新版本 + */ + CHECK_NEW_VERSION, + + /** + * 下载 + */ + DOWNLOAD, + + /** + * 取消升级 + */ + CANCEL, + + /** + * 继续下载 + */ + RESUME, + + /** + * 安装 + */ + INSTALL, + + /** + * 重启 + */ + REBOOT, + + /** + * 显示新版本页面 + */ + SHOW_NEW_VERSION, + + /** + * 显示进度圆圈 + */ + SHOW_PROCESS_VIEW +} + +/** + * 状态基类 + * + * @since 2022-06-10 + */ +export class BaseState { + /** + * 状态对象 + */ + otaStatus: OtaStatus; + + /** + * 进度 + */ + percent: number = 0; + + /** + * 状态 + */ + state: number = UpdateState.INIT; + + /** + * 升级行为 + */ + actionSet: Array = []; + + /** + * 下载状态描述 + */ + downloadStateText: string | Resource = ""; + + /** + * 下载安装文字描述 + */ + buttonText: string | Resource = $r('app.string.btn_download'); + + /** + * 按钮是否可点击 + */ + isButtonClickable: boolean = true; + + /** + * 按钮对应的升级行为 + */ + buttonClickAction: UpdateAction; + + /** + * 数据刷新 + * + * @param otaStatus 状态 + */ + refresh(otaStatus: OtaStatus): void { + this.otaStatus = otaStatus; + if (this.otaStatus?.percent) { + this.percent = otaStatus?.percent; + } + } + + /** + * 提醒 + */ + async notify(context?: common.Context, eventId?: update.EventId): Promise { + } +} + +/** + * 状态--初始状态 + * + * @since 2022-06-10 + */ +export class Init extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.CHECK_NEW_VERSION); + } + + async notify(): Promise { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + } +} + +/** + * 状态--搜包成功 + * + * @since 2022-06-10 + */ +export class CheckSuccess extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.CHECK_NEW_VERSION); + this.actionSet.push(UpdateAction.DOWNLOAD); + this.state = UpdateState.CHECK_SUCCESS; + this.buttonClickAction = UpdateAction.DOWNLOAD; + } + + async notify(context?: common.Context, eventId?: update.EventId): Promise { + if (this.otaStatus?.endReason) { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + switch (this.otaStatus.endReason) { + case ErrorCode.NETWORK_ERROR: + let message = await context.resourceManager.getString($r("app.string.network_err_toast").id); + ToastUtils.showToast(message); + break; + case ErrorCode.NO_ENOUGH_MEMORY: + DialogUtils.showDownloadNotEnoughSpaceDialog(context, this.otaStatus, eventId); + break; + default: + DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId); + break; + } + } + } +} + +/** + * 状态--下载中 + * + * @since 2022-06-10 + */ +export class Downloading extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.DOWNLOADING; + this.downloadStateText = $r('app.string.download_status_downloading'); + this.buttonText = $r('app.string.cancel'); + this.buttonClickAction = UpdateAction.CANCEL; + } + + async notify(context?: common.Context): Promise { + if (this.percent == 100) { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + return; + } + if (!VersionUtils.isInNewVersionPage()) { + let versionName = await VersionUtils.obtainNewVersionName(globalThis.cachedNewVersionInfo); + await UpgradeAdapter.getInstance().getNotifyInstance()?.showDownloading(versionName, this.percent, context); + } + } +} + +/** + * 状态--下载暂停 + * + * @since 2022-06-10 + */ +export class DownloadPause extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.actionSet.push(UpdateAction.RESUME); + this.state = UpdateState.DOWNLOAD_PAUSE; + this.downloadStateText = $r('app.string.download_status_download_pause'); + this.buttonText = $r('app.string.continue'); + this.buttonClickAction = UpdateAction.RESUME; + } + + async notify(context?: common.Context, eventId?: update.EventId): Promise { + if (!VersionUtils.isInNewVersionPage()) { + return; + } + if (this.otaStatus?.endReason) { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + switch (this.otaStatus?.endReason) { + case ErrorCode.NETWORK_ERROR: + if (eventId == update.EventId.EVENT_DOWNLOAD_PAUSE) { + DialogUtils.showDownloadNoNetworkDialog(context, this.otaStatus, eventId); + } else { + let message = await context.resourceManager.getString($r("app.string.network_err_toast").id); + ToastUtils.showToast(message); + } + break; + case ErrorCode.NETWORK_NOT_ALLOW: + if (eventId == update.EventId.EVENT_DOWNLOAD_PAUSE) { + DialogUtils.showDownloadNoNetworkDialog(context, this.otaStatus, eventId); + } + break; + default: + DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId); + break; + } + } + } +} + +/** + * 状态--下载失败 + * + * @since 2022-06-10 + */ +export class DownloadFailed extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.CHECK_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.DOWNLOAD_FAILED; + this.downloadStateText = $r('app.string.download_status_download_failed'); + this.isButtonClickable = false; + this.buttonText = $r('app.string.cancel'); + this.buttonClickAction = UpdateAction.CANCEL; + } + + async notify(context?: common.Context, eventId?: update.EventId): Promise { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + switch (this.otaStatus.endReason) { + case ErrorCode.VERIFY_PACKAGE_FAIL: + DialogUtils.showVerifyFailDialog(context, this.otaStatus, eventId); + break; + default: + DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId); + break; + } + } +} + +/** + * 状态--下载成功 + * + * @since 2022-06-10 + */ +export class DownloadSuccess extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.INSTALL); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.DOWNLOAD_SUCCESS; + this.percent = 100; + this.downloadStateText = $r('app.string.download_status_download_success'); + this.buttonText = $r('app.string.btn_upgrade'); + this.buttonClickAction = UpdateAction.INSTALL; + } + + async notify(context?: common.Context, eventId?: update.EventId): Promise { + let isABInstall = await VersionUtils.isABInstall(); + if (eventId == update.EventId.EVENT_DOWNLOAD_SUCCESS && isABInstall) { + OtaUpdateManager.getInstance().upgrade(update.Order.INSTALL); + return; + } + + if (eventId == update.EventId.EVENT_UPGRADE_WAIT && !isABInstall) { + LogUtils.info('StateManager', 'manual download complete to count down'); + if (!VersionUtils.isInNewVersionPage()) { + NotificationManager.startToNewVersion(context); + } + AppStorage.Set('isClickInstall', 1); + return; + } + + if (this.otaStatus?.endReason) { + switch (this.otaStatus.endReason) { + case ErrorCode.NO_ENOUGH_MEMORY: + DialogUtils.showUpgradeNotEnoughSpaceDialog(context, this.otaStatus, eventId); + break; + case ErrorCode.NO_ENOUGH_BATTERY: + DialogUtils.showUpgradeNotEnoughBatteryDialog(context, this.otaStatus, eventId); + break; + default: + DialogUtils.showUpgradeFailDialog(context, this.otaStatus, eventId); + break; + } + } + } +} + +/** + * 状态--解压中 + * + * @since 2022-06-10 + */ +export class Installing extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.INSTALLING; + this.buttonText = $r('app.string.btn_upgrade') + this.isButtonClickable = false; + this.downloadStateText = $r('app.string.new_version_status_installing'); + } + + async notify(context?: common.Context): Promise { + if (this.percent == 100) { + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + return; + } + if (!VersionUtils.isInNewVersionPage()) { + let versionName = await VersionUtils.obtainNewVersionName(globalThis.cachedNewVersionInfo); + await UpgradeAdapter.getInstance().getNotifyInstance()?.showInstalling(versionName, this.percent, context); + } + } +} + +/** + * 状态--下载成功 + * + * @since 2022-06-10 + */ +export class InstallSuccess extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.REBOOT); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.INSTALL_SUCCESS; + this.percent = 100; + this.downloadStateText = $r('app.string.new_version_status_install_success'); + this.buttonText = $r('app.string.btn_reboot'); + this.buttonClickAction = UpdateAction.REBOOT; + } + + async notify(context?: common.Context, eventId?: update.EventId): Promise { + if (eventId == update.EventId.EVENT_APPLY_WAIT) { + LogUtils.info('StateManager', 'ab install complete to count down'); + if (!VersionUtils.isInNewVersionPage()) { + NotificationManager.startToNewVersion(context); + } + AppStorage.Set('isClickInstall', 1); + } + } +} + +/** + * 状态--升级失败 + * + * @since 2022-06-10 + */ +export class InstallFailed extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.CHECK_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.INSTALL_FAILED; + this.percent = 100; + this.buttonText = $r('app.string.btn_upgrade'); + this.isButtonClickable = false; + } + + async notify(context?: common.Context): Promise { + AppStorage.Set('installStatusRefresh', JSON.stringify(this.otaStatus)); + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + if (VersionUtils.isInNewVersionPage()) { + DialogUtils.showUpgradeFailDialog(context, this.otaStatus); + } else { + let versionName = globalThis.lastVersionName; + LogUtils.log('StateManager', "InstallFailed versionName is " + versionName); + await UpgradeAdapter.getInstance().getNotifyInstance()?.showUpgradeFailed(versionName, context); + } + + } +} + +/** + * 状态--升级中 + * + * @since 2022-06-10 + */ +export class Upgrading extends Installing { + constructor() { + super(); + this.actionSet.push(UpdateAction.SHOW_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.UPGRADING; + this.percent = 100; + this.isButtonClickable = false; + this.downloadStateText = $r('app.string.new_version_status_installing'); + } + + async notify(context?: common.Context): Promise { + AppStorage.Set('installStatusRefresh', JSON.stringify(this.otaStatus)); + } +} + +/** + * 状态--升级成功 + * + * @since 2022-06-10 + */ +export class UpgradeSuccess extends BaseState { + constructor() { + super(); + this.actionSet.push(UpdateAction.CHECK_NEW_VERSION); + this.actionSet.push(UpdateAction.SHOW_PROCESS_VIEW); + this.state = UpdateState.UPGRADE_SUCCESS; + this.percent = 100; + this.buttonText = $r('app.string.btn_upgrade'); + this.isButtonClickable = false; + } + + async notify(context?: common.Context): Promise { + AppStorage.Set('installStatusRefresh', JSON.stringify(this.otaStatus)); + await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll(); + let versionName = globalThis.lastVersionName; + LogUtils.log('StateManager', "UpgradeSuccess versionName is " + versionName); + await UpgradeAdapter.getInstance().getNotifyInstance()?.showUpgradeSuccess(versionName, context); + } +} + +/** + * 状态--升级失败 + * + * @since 2022-06-10 + */ +export class UpgradeFailed extends InstallFailed { + constructor() { + super(); + this.state = UpdateState.UPGRADE_FAILED; + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/notify/NotificationHelper.ets b/feature/ota/src/main/ets/notify/NotificationHelper.ets new file mode 100644 index 0000000..6536f3d --- /dev/null +++ b/feature/ota/src/main/ets/notify/NotificationHelper.ets @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import notification from '@ohos.notificationManager'; +import wantAgent from '@ohos.app.ability.wantAgent'; +import type common from '@ohos.app.ability.common'; +import { Action, PACKAGE_NAME } from '@ohos/common/src/main/ets/const/update_const'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { INotify } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; + +/** + * 通知辅助者 + * + * @since 2022-06-05 + */ +export class NotificationHelper implements INotify { + /** + * 跳转信息--跳转到搜包页面 + */ + private checkWantAgentInfo = { + wants: [{ + bundleName: PACKAGE_NAME, + abilityName: 'ServiceExtAbility', + action: Action.NOTIFICATION_CHECK + }], + operationType: wantAgent.OperationType.START_ABILITY, + requestCode: 0, + wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG], + }; + + /** + * 跳转信息--下载中拉起界面 + */ + private downloadingWantAgentInfo = { + wants: [{ + bundleName: PACKAGE_NAME, + abilityName: 'ServiceExtAbility', + action: Action.NOTIFICATION_DETAIL + }], + operationType: wantAgent.OperationType.START_ABILITY, + requestCode: 0, + wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG], + }; + + /** + * 下载进度通知 + * + * @param version 版本号 + * @param progress 进度 + * @param context 上下文 + */ + async showDownloading(version, progress, context): Promise { + let templateName: string = 'downloadTemplate'; + if (!globalThis.isSupportTemplate) { + globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch(err => { + this.logError('showDownloading isSupportTemplate failed because ' + JSON.stringify(err)); + return false; + }); + } + if (!globalThis.isSupportTemplate) { + this.logError('showDownloading is not supportTemplate'); + return; + } + var notificationRequest = { + content: { + contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, + normal: { + title: await context.resourceManager.getString($r('app.string.software_update').id), + text: await context.resourceManager.getString($r('app.string.software_download_progress').id) + }, + }, + template: { + name: 'downloadTemplate', + data: { + title: await context.resourceManager.getString($r('app.string.software_download_progress').id), + fileName: version, + progressValue: progress + } + }, + wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo), + id: 5, + label: '111', + slotType: notification.SlotType.SERVICE_INFORMATION, + deliveryTime: new Date().getTime() + } + await notification.publish(notificationRequest).catch((err) => { + this.logError('showDownloading notification publish failed because ' + JSON.stringify(err)); + }); + } + + /** + * 下载进度通知 + * + * @param version 版本号 + * @param progress 进度 + * @param context 上下文 + */ + async showInstalling(version, progress, context): Promise { + let templateName: string = 'installTemplate'; + if (!globalThis.isSupportTemplate) { + globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch(err => { + this.logError('showInstalling isSupportTemplate failed because ' + JSON.stringify(err)); + return false; + }); + } + if (!globalThis.isSupportTemplate) { + this.logError('showInstalling is not supportTemplate'); + return; + } + var notificationRequest = { + content: { + contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, + normal: { + title: await context.resourceManager.getString($r('app.string.software_update').id), + text: await context.resourceManager.getString($r('app.string.software_install_progress').id) + }, + }, + template: { + name: 'installTemplate', + data: { + title: await context.resourceManager.getString($r('app.string.software_install_progress').id), + fileName: version, + progressValue: progress + } + }, + wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo), + id: 5, + label: '111', + slotType: notification.SlotType.SERVICE_INFORMATION, + deliveryTime: new Date().getTime() + } + await notification.publish(notificationRequest).catch((err) => { + this.logError('showInstalling publish failed because ' + JSON.stringify(err)); + }); + } + + /** + * 弹安装失败通知 + * + * @param context 实上下文 + */ + async showUpgradeFailed(versionName: string, context: common.Context): Promise { + let request = { + content: { + contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, + normal: { + title: await context.resourceManager.getString($r('app.string.install_fail_message').id), + text: versionName + } + }, + wantAgent: await wantAgent.getWantAgent(this.checkWantAgentInfo), + id: 3, + slotType: notification.SlotType.SERVICE_INFORMATION + } + await notification.publish(request).then(() => { + this.logInfo('showUpgradeFailed publish promise success.'); + }).catch((err) => { + this.logError('showUpgradeFailed publish promise failed because ' + JSON.stringify(err)); + }); + } + + /** + * 弹安装成功通知 + * + * @param context 实上下文 + */ + async showUpgradeSuccess(versionName: string, context: common.Context): Promise { + let request = { + content: { + contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, + normal: { + title: await context.resourceManager.getString($r('app.string.install_success_message').id), + text: versionName + } + }, + wantAgent: await wantAgent.getWantAgent(this.checkWantAgentInfo), + id: 4, + slotType: notification.SlotType.SERVICE_INFORMATION + } + await notification.publish(request).then(() => { + this.logInfo('showUpgradeSuccess publish promise success.'); + }).catch((err) => { + this.logError('showUpgradeSuccess publish promise failed because ' + JSON.stringify(err)); + }); + } + + /** + * 取消所有通知 + */ + async cancelAll(): Promise { + await notification.cancelAll().then(() => { + this.logInfo('cancelAll notification success'); + }); + } + + /** + * info级别日志打印 + * + * @param message 日志内容 + */ + logInfo(message: string): void { + LogUtils.info('NotificationHelper', message); + } + + /** + * error级别日志打印 + * + * @param message 日志内容 + */ + logError(message: string): void { + LogUtils.error('NotificationHelper', message); + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/notify/NotificationManager.ts b/feature/ota/src/main/ets/notify/NotificationManager.ts new file mode 100644 index 0000000..f008773 --- /dev/null +++ b/feature/ota/src/main/ets/notify/NotificationManager.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type common from '@ohos.app.ability.common'; +import { + Action, + PACKAGE_NAME, + MAIN_ABILITY_NAME +} from '@ohos/common/src/main/ets/const/update_const'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { UpdateUtils } from '@ohos/common/src/main/ets/util/UpdateUtils'; +import RouterUtils from '../util/RouterUtils'; + +/** + * 日志TAG + */ +const TAG = 'NotificationManager'; + +/** + * 通知点击事件管理类 + * + * @since 2022-06-05 + */ +export class NotificationManager { + /** + * 处理通知点击动作 + * + * @param action 具体动作 + */ + static async handleAction(action: string, context: common.Context): Promise { + switch (action) { + case Action.NOTIFICATION_CHECK: + this.handleCheckAction(context); + return true; + case Action.NOTIFICATION_DETAIL: + await this.handleDetailAction(context); + return true; + default: + return false; + } + } + + private static handleCheckAction(context: common.Context): void { + LogUtils.log(TAG, 'handleCheckAction'); + this.startAbility('pages/index', context); + } + + private static async handleDetailAction(context: common.Context): Promise { + LogUtils.log(TAG, 'handleDetailAction'); + if (await RouterUtils.isCanToNewVersion()) { + this.startAbility('pages/newVersion', context); + } else { + this.startAbility('pages/index', context); + } + } + + public static async startToNewVersion(context: common.Context): Promise { + if (await RouterUtils.isCanToNewVersion()) { + this.startAbility('pages/newVersion', context); + } + } + + private static startAbility(uri: string, context: common.Context): void { + let want = { + bundleName: PACKAGE_NAME, + abilityName: MAIN_ABILITY_NAME, + uri: uri + }; + let options = { + windowMode: 0, + displayId: 2 + }; + UpdateUtils.startAbility(context, want, options); + } +} \ No newline at end of file diff --git a/feature/ota/src/main/ets/util/ChangelogParseUtils.ets b/feature/ota/src/main/ets/util/ChangelogParseUtils.ets new file mode 100644 index 0000000..0f232f1 --- /dev/null +++ b/feature/ota/src/main/ets/util/ChangelogParseUtils.ets @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import convertXml from '@ohos.convertxml'; +import type { Features, Language, Feature, Changelog, Icon } from '@ohos/common/src/main/ets/const/update_const'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; + +let conv = new convertXml.ConvertXML(); + +let options: convertXml.ConvertOptions = { + trim: false, + declarationKey: '_declaration', + instructionKey: '_instruction', + attributesKey: '_attributes', + textKey: '_text', + cdataKey: '_cdata', + doctypeKey: '_doctype', + commentKey: '_comment', + parentKey: '_parent', + typeKey: '_type', + nameKey: '_name', + elementsKey: '_elements', +}; + +let changelog: Changelog; + +/** + * Changelog解析工具 + * + * @since 2022-06-06 + */ +namespace ChangelogParseUtils { + /** + * Changelog解析 + * + * @param res 待解析的string + * @param callback 解析结果回调 + */ + export function parseXml(res: string, callback: (data: Changelog) => void): void { + logInfo('read file parse start'); + changelog = { language: new Map() }; + if (!res) { + logError('res is null!'); + callback(null); + return; + } + let xmlResult: Object = conv.convert(res, options); + if (!xmlResult || !xmlResult['_elements'] || xmlResult['_elements'].length <= 0) { + logError('xmlResult is null! ' + JSON.stringify(xmlResult)); + callback(null); + return; + } + let root = xmlResult['_elements'][0]; + if (!root || !root['_elements'] || root['_elements'].length <= 0) { + logError('root is null! ' + JSON.stringify(root)); + callback(null); + return; + } + parseRoot(root); + logInfo('read file parse end'); + callback(changelog); + } + + function parseRoot(root: any): void { + // 倒序解析,先解析Icon + let icons = new Object(); + logInfo('root length ' + root['_elements'].length); + for (let index = root['_elements'].length; index > 0; index--) { + let element = root['_elements'][index - 1]; + if (!element) { + logError('element is null! ' + JSON.stringify(element)); + continue; + } + if (element['_type'] === 'element' && element['_name'] === 'default-language') { + let attribute = element['_attributes']['name']; + logInfo('default-language :' + attribute); + changelog.defLanguage = attribute; + } + if (element['_type'] === 'element' && element['_name'] === 'displayType') { + let attribute = element['_elements']?.[0]?.['_text']; + logInfo('displayType :' + attribute); + changelog.displayType = attribute; + } + if (element['_type'] === 'element' && element['_name'] === 'icons') { + let iconsElement = element['_elements']; + parseIcons(iconsElement, icons); + } + parseLanguage(element, icons); + } + } + + function parseLanguage(element: any, icons: Object): void { + let language: Language = { featuresArray: [] }; + if (element['_type'] === 'element' && element['_name'] === 'language') { + let attribute: string; + if (!element['_attributes']) { + logError('language is null :' + JSON.stringify(element['_attributes'])); + return; + } + attribute = element['_attributes']['name']; + let languageElement = element['_elements']; + language.language = attribute; + changelog.language.set(language.language, language); + + if (!languageElement || languageElement.length <= 0) { + logError('features is null' + JSON.stringify(languageElement)); + return; + } + parseFeatures(languageElement, language, icons); + } + } + + function parseIcons(iconsElement: any, icons: Object): void { + let icon: Icon; + for (let index = 0; index < iconsElement.length; index++) { + let iconElement = iconsElement[index]; + if (!iconElement) { + logError('iconElement is null :' + JSON.stringify(iconElement)); + continue; + } + if (iconElement['_type'] === 'element' && iconElement['_name'] === 'icon') { + icon = { + id: iconElement['_attributes']['id'], + pkg: iconElement['_attributes']['pkg'], + res: iconElement['_attributes']['res'], + }; + icons[icon.id] = icon; + } + } + } + + function parseFeatures(languageElement: any, language: Language, icons: Object): void { + let features: Features; + for (let index = 0; index < languageElement.length; index++) { + let featuresElement = languageElement[index]; + if (!featuresElement) { + logInfo('featuresElement is null' + JSON.stringify(featuresElement)); + continue; + } + + if (featuresElement['_type'] === 'element' && featuresElement['_name'] === 'features') { + let moduleString = featuresElement['_attributes']['module']; + let typeString = featuresElement['_attributes']['type']; + let id = featuresElement['_attributes']['id']; + let featureElement = featuresElement['_elements']; + let featureList = new Array(); + features = { + title: moduleString, + id: id, + featureModuleType: typeString, + featureList: featureList, + icon: icons[id], + }; + language.featuresArray.push(features); + + if (!featureElement || featureElement.length <= 0) { + logError('featureElement is null: ' + JSON.stringify(featureElement)); + continue; + } + parseFeature(featureElement, featureList); + } + } + } + + function parseFeature(featureElement: any, featureList: Array): void { + let feature: Feature; + for (let index = 0; index < featureElement.length; index++) { + let featureItem = featureElement[index]; + if (featureItem['_type'] !== 'element' || featureItem['_name'] !== 'feature') { + continue; + } + if (!featureItem) { + logError('featureItem is null: ' + JSON.stringify(featureItem)); + continue; + } + + let title: string; + if (featureItem['_attributes']) { + title = featureItem['_attributes']['title']; + } + let content: string; + if (featureItem['_elements']) { + content = featureItem['_elements'][0]['_text']; + } + if (title) { + feature = { subTitle: title, contents: [] }; + featureList.push(feature); + } else { + if (!feature) { + feature = { subTitle: title, contents: [] }; + featureList.push(feature); + } + if (content) { + feature.contents.push(content); + } + } + } + } + + function logInfo(message: string): void { + LogUtils.info('ChangelogParseUtils', message); + } + + function logError(message: string): void { + LogUtils.error('ChangelogParseUtils', message); + } +} + +export default ChangelogParseUtils; \ No newline at end of file diff --git a/feature/ota/src/main/ets/util/RouterUtils.ts b/feature/ota/src/main/ets/util/RouterUtils.ts index dfbab2f..266c60c 100644 --- a/feature/ota/src/main/ets/util/RouterUtils.ts +++ b/feature/ota/src/main/ets/util/RouterUtils.ts @@ -14,6 +14,9 @@ */ import router from '@ohos.router'; +import { UpdateState, UpgradeCallResult } from '@ohos/common/src/main/ets/const/update_const'; +import { StateManager, UpdateAction } from '../manager/StateManager'; +import { OtaUpdateManager } from '../manager/OtaUpdateManager'; /** * 一秒对应的时间(1000) @@ -74,7 +77,17 @@ namespace RouterUtils { */ export async function isCanToNewVersion(): Promise { return new Promise((resolve, reject) => { - resolve(true); + OtaUpdateManager.getInstance().getOtaStatus().then((upgradeData) => { + if (upgradeData?.callResult === UpgradeCallResult.OK) { + if (upgradeData.data?.status === UpdateState.UPGRADING) { + resolve(false); + } else { + resolve(StateManager.isAllowExecute(upgradeData.data?.status, UpdateAction.SHOW_NEW_VERSION)); + } + } else { + resolve(false); + } + }); }); } } diff --git a/feature/ota/src/main/ets/util/VersionUtils.ets b/feature/ota/src/main/ets/util/VersionUtils.ets index 9644395..9352341 100644 --- a/feature/ota/src/main/ets/util/VersionUtils.ets +++ b/feature/ota/src/main/ets/util/VersionUtils.ets @@ -13,7 +13,16 @@ * limitations under the License. */ +import update from '@ohos.update'; +import type { UpgradeData } from '@ohos/common/src/main/ets/const/update_const'; +import { UpgradeCallResult } from '@ohos/common/src/main/ets/const/update_const'; import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { UpdateUtils } from '@ohos/common/src/main/ets/util/UpdateUtils'; +import { OtaUpdateManager } from '../manager/OtaUpdateManager'; +import { UpgradeAdapter } from '../UpgradeAdapter'; + +const TAG = 'VersionUtils'; /** * 版本工具 @@ -21,6 +30,32 @@ import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; * @since 2022-06-06 */ export namespace VersionUtils { + /** + * 计算升级包大小 + * + * @param newVersionInfo 新版本信息 + * @return 升级包大小 + */ + export function calculatePackageSize(newVersion: update.NewVersionInfo): number { + let totalSize: number = 0; + for (let index = 0; index < newVersion?.versionComponents?.length; index++) { + totalSize += Number(newVersion?.versionComponents?.[index]?.size); + } + LogUtils.info(TAG, 'calculatePackageSize, totalSize: ' + totalSize); + return totalSize; + } + + /** + * 获取新版本名称 + * + * @param info 新版本信息 + * @return 新版本名称 + */ + export async function obtainNewVersionName(info: update.NewVersionInfo | update.TaskBody): Promise { + let component: update.VersionComponent = sortComponents(info?.versionComponents)?.[0]; + return component?.displayVersion ?? ''; + } + /** * 获取当前版本号 * @@ -38,6 +73,62 @@ export namespace VersionUtils { export function getDisplayVersionForIndex(): string { return DeviceUtils.getDisplayVersion(); } + + /** + * 排序 + * + * @param components 升级包集合 + * @return 升级包集合 + */ + export function sortComponents(components: Array): Array { + if (components) { + return components.sort((component: update.VersionComponent, nextComponent: update.VersionComponent) => { + return component.componentType - nextComponent.componentType; + }); + } + return null; + } + + export async function getNewVersionDigest(): Promise { + let newVersionInfo: update.NewVersionInfo = globalThis.cachedNewVersionInfo || + await OtaUpdateManager.getInstance().getNewVersion().then((upgradeData: UpgradeData) => { + return upgradeData.callResult === UpgradeCallResult.OK ? upgradeData.data : null; + }); + + if (newVersionInfo) { + LogUtils.info(TAG, 'getNewVersionDigest, versionDigestInfo: ' + newVersionInfo.versionDigestInfo.versionDigest); + return newVersionInfo.versionDigestInfo.versionDigest; + } + + return ''; + } + + /** + * 是否在新版本界面 + * + * @return 是否在新版本界面 + */ + export function isInNewVersionPage(): boolean { + return globalThis.AbilityStatus === 'ON_FOREGROUND' && globalThis.currentPage === 'pages/newVersion'; + } + + /** + * 是否是AB升级 + * + * @return 是否是AB升级 + */ + export async function isABInstall(): Promise { + let newVersionInfo = globalThis.cachedNewVersionInfo || await OtaUpdateManager.getInstance().getNewVersion() + .then(upgradeData => { + this.log(`isABInstall upgradeData: ${upgradeData}`); + return upgradeData.callResult == UpgradeCallResult.OK ? upgradeData.data : null; + }); + let components: Array = VersionUtils.sortComponents(newVersionInfo?.versionComponents); + let component: update.VersionComponent = components?.filter((component: update.VersionComponent) => { + return component.componentType == update.ComponentType.OTA; + })?.[0]; + return component?.effectiveMode === update.EffectiveMode.LIVE_AND_COLD; + } } export default VersionUtils; \ No newline at end of file diff --git a/feature/ota/src/main/resources/base/element/float.json b/feature/ota/src/main/resources/base/element/float.json index ab8ca23..8512504 100644 --- a/feature/ota/src/main/resources/base/element/float.json +++ b/feature/ota/src/main/resources/base/element/float.json @@ -242,7 +242,7 @@ }, { "name": "dialog_margin_top", - "value": "12vp" + "value": "16vp" }, { "name": "message_dialog_margin_vertical", diff --git a/feature/ota/src/main/resources/base/element/string.json b/feature/ota/src/main/resources/base/element/string.json index dc0d5f8..ccbc52e 100644 --- a/feature/ota/src/main/resources/base/element/string.json +++ b/feature/ota/src/main/resources/base/element/string.json @@ -4,10 +4,102 @@ "name":"btn_check_new_version", "value":"Check for updates" }, + { + "name":"btn_download", + "value":"Download & install" + }, + { + "name":"btn_upgrade", + "value":"Install" + }, + { + "name":"title_new_version", + "value":"New version" + }, + { + "name":"title_current_version", + "value":"Current version" + }, + { + "name":"title_change_log", + "value":"Changelog" + }, + { + "name":"button_know", + "value":"OK" + }, + { + "name":"network_request", + "value":"No WLAN connection. Downloading with mobile data may result in additional fees. Continue?" + }, + { + "name":"ok", + "value":"OK" + }, + { + "name":"cancel", + "value":"Cancel" + }, + { + "name":"continue", + "value":"Resume" + }, + { + "name":"net_error_title", + "value":"Network error" + }, + { + "name":"net_error_content", + "value":"Download paused. Check your network connection and try again." + }, + { + "name":"space_not_enough_title", + "value":"Insufficient storage" + }, + { + "name":"space_not_enough_content", + "value":"Insufficient storage available. Free up some space and try again." + }, + { + "name":"package_verify_fail", + "value":"Update verification failed." + }, + { + "name":"later", + "value":"Later" + }, + { + "name":"install_now", + "value":"Install now (%d)" + }, { "name":"software_update", "value":"Software update" }, + { + "name":"software_download_progress", + "value":"Downloading" + }, + { + "name":"reboot_wait", + "value":"Restarting" + }, + { + "name":"battery_not_enough_title", + "value":"Low battery" + }, + { + "name":"battery_not_enough_content", + "value":"Battery level too low. Charge your device to at least %s and try again." + }, + { + "name":"download_fail", + "value":"Download failed. Please check for updates again." + }, + { + "name":"update_fail", + "value":"Installation failed. Please check for updates again." + }, { "name":"check_version_status_has_new", "value":"Update available" @@ -19,6 +111,65 @@ { "name":"check_version_status_checking", "value":"Checking for updates" + }, + { + "name":"download_status_downloading", + "value":"Downloading" + }, + { + "name":"download_status_download_pause", + "value":"Download paused" + }, + { + "name":"download_status_download_failed", + "value":"Download failed" + }, + { + "name":"download_status_download_success", + "value":"Downloaded" + }, + { + "name":"network_err_toast", + "value":"Network error. Please try again." + }, + { + "name":"no_info_now", + "value":"No content" + }, + { + "name":"install_success_message", + "value":"Update successfully installed" + }, + { + "name":"install_fail_message", + "value":"Installation failed" + }, + { + "name":"software_install_progress", + "value":"installing" + }, + { + "name":"new_version_status_installing", + "value":"Installing..." + }, + { + "name":"new_version_status_install_success", + "value":"Installed" + }, + { + "name":"btn_reboot", + "value":"reboot" + }, + { + "name":"count_down_message_recovery", + "value":"%s will start installation in %d seconds." + },{ + "name":"count_down_message_ab", + "value":"%s will reboot in %d seconds." + }, + { + "name":"reboot_now", + "value":"Reboot Now (%d)" } ] } \ No newline at end of file diff --git a/feature/ota/src/main/resources/tablet/element/float.json b/feature/ota/src/main/resources/tablet/element/float.json deleted file mode 100644 index 10f6851..0000000 --- a/feature/ota/src/main/resources/tablet/element/float.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "float": [ - { - "name": "home_logo_bottom_margin", - "value": "32vp" - }, - { - "name": "home_new_version_label_top_margin", - "value": "8vp" - }, - { - "name": "text_size_progress_bar_percent", - "value": "18fp" - }, - { - "name": "text_size_version_name", - "value": "18fp" - }, - { - "name": "home_text_size_version_name", - "value": "16fp" - } - ] -} \ No newline at end of file diff --git a/feature/ota/src/main/resources/zh_CN/element/string.json b/feature/ota/src/main/resources/zh_CN/element/string.json index 9b1b83d..2342f44 100644 --- a/feature/ota/src/main/resources/zh_CN/element/string.json +++ b/feature/ota/src/main/resources/zh_CN/element/string.json @@ -4,10 +4,102 @@ "name":"btn_check_new_version", "value":"检查更新" }, + { + "name":"btn_download", + "value":"下载并安装" + }, + { + "name":"btn_upgrade", + "value":"安装" + }, + { + "name":"title_new_version", + "value":"新版本" + }, + { + "name":"title_current_version", + "value":"当前版本" + }, + { + "name":"title_change_log", + "value":"更新日志" + }, + { + "name":"button_know", + "value":"知道了" + }, + { + "name":"network_request", + "value":"当前未连接 WLAN,下载将会产生流量费用。是否开启 WLAN 下载?" + }, + { + "name":"ok", + "value":"确认" + }, + { + "name":"cancel", + "value":"取消" + }, + { + "name":"continue", + "value":"继续" + }, + { + "name":"net_error_title", + "value":"网络异常" + }, + { + "name":"net_error_content", + "value":"检测到网络异常,下载已暂停。请检查网络连接后重试。" + }, + { + "name":"space_not_enough_title", + "value":"空间不足" + }, + { + "name":"space_not_enough_content", + "value":"升级所需存储空间不足,请清理设备存储空间后重试。" + }, + { + "name":"package_verify_fail", + "value":"升级包校验失败。" + }, + { + "name":"later", + "value":"稍后" + }, + { + "name":"install_now", + "value":"现在安装 (%d)" + }, { "name":"software_update", "value":"软件更新" }, + { + "name":"software_download_progress", + "value":"下载进度" + }, + { + "name":"reboot_wait", + "value":"重启中请稍候…" + }, + { + "name":"battery_not_enough_title", + "value":"电量不足" + }, + { + "name":"battery_not_enough_content", + "value":"电量过低,请充电至 %s 后再次尝试更新。" + }, + { + "name":"download_fail", + "value":"下载失败,请重新搜索更新包。" + }, + { + "name":"update_fail", + "value":"安装失败,请重新搜索更新包。" + }, { "name":"check_version_status_has_new", "value":"发现新版本" @@ -19,6 +111,65 @@ { "name":"check_version_status_checking", "value":"正在检查更新" + }, + { + "name":"download_status_downloading", + "value":"正在下载…" + }, + { + "name":"download_status_download_pause", + "value":"下载暂停" + }, + { + "name":"download_status_download_failed", + "value":"下载失败" + }, + { + "name":"download_status_download_success", + "value":"下载完成" + }, + { + "name":"network_err_toast", + "value":"网络异常,请重试" + }, + { + "name":"no_info_now", + "value":"没有内容" + }, + { + "name":"install_success_message", + "value":"新版本安装成功" + }, + { + "name":"install_fail_message", + "value":"新版本安装失败" + }, + { + "name":"software_install_progress", + "value":"安装进度" + }, + { + "name":"new_version_status_installing", + "value":"正在安装..." + }, + { + "name":"new_version_status_install_success", + "value":"安装完成" + }, + { + "name":"btn_reboot", + "value":"重启" + }, + { + "name":"count_down_message_recovery", + "value":"%s 将在 %d 秒后开始安装。" + },{ + "name":"count_down_message_ab", + "value":"%s 将在 %d 秒后开始重启。" + }, + { + "name":"reboot_now", + "value":"现在重启 (%d)" } ] } \ No newline at end of file diff --git a/product/oh/base/src/main/ets/MainAbility/MainAbility.ts b/product/oh/base/src/main/ets/MainAbility/MainAbility.ts index 90a3c44..1ec61a1 100644 --- a/product/oh/base/src/main/ets/MainAbility/MainAbility.ts +++ b/product/oh/base/src/main/ets/MainAbility/MainAbility.ts @@ -16,9 +16,14 @@ import Ability from '@ohos.app.ability.UIAbility'; import type Want from '@ohos.app.ability.Want'; import type AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import router from '@ohos.router'; +import update from '@ohos.update'; import type window from '@ohos.window'; import type { Configuration } from '@ohos.app.ability.Configuration'; import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import type { OtaStatus } from '@ohos/common/src/main/ets/const/update_const'; +import { StateManager } from '@ohos/ota/src/main/ets/manager/StateManager'; +import { NotificationHelper } from '@ohos/ota/src/main/ets/notify/NotificationHelper'; /** * 主Ability @@ -46,12 +51,21 @@ export default class MainAbility extends Ability { onWindowStageCreate(windowStage: window.WindowStage): void { globalThis.AbilityStatus = null; - windowStage.loadContent('pages/index', null); + if (globalThis.abilityWant?.uri === 'pages/newVersion') { + windowStage.loadContent('pages/newVersion', null); + } else if (globalThis.abilityWant?.uri === 'pages/setting') { + windowStage.loadContent('pages/setting', null); + } else { + windowStage.loadContent('pages/index', null); + } } onNewWant(want: Want): void { this.log('BaseAbility onNewWant:' + JSON.stringify(want)); globalThis.newPage = want.uri; + if (globalThis.AbilityStatus === 'ON_FOREGROUND') { + this.routePage(); + } } onConfigurationUpdate(config: Configuration): void { @@ -70,7 +84,12 @@ export default class MainAbility extends Ability { onForeground(): void { this.log('BaseAbility onForeground'); + new NotificationHelper().cancelAll(); globalThis.AbilityStatus = 'ON_FOREGROUND'; + setTimeout(() => { + this.routePage(); + this.handleReceivedUpdatePageMessage(); + }, MainAbility.WAITING_PREPARE_TIME); // for env prepare } onBackground(): void { @@ -78,6 +97,28 @@ export default class MainAbility extends Ability { this.log('BaseAbility onBackground'); } + private routePage(): void { + if (globalThis.newPage && globalThis.currentPage) { + if (globalThis.currentPage !== globalThis.newPage) { + this.log('router.push page: ' + globalThis.newPage); + router.pushUrl({ + url: globalThis.newPage, + }); + } + globalThis.newPage = null; + } + } + + private handleReceivedUpdatePageMessage(): void { + if (globalThis.reNotify) { // page页面弹出对话框 + let otaStatus: OtaStatus = globalThis.otaStatusFromService; + let eventId: update.EventId = globalThis.eventIdFromService; + this.log('handleReceivedUpdatePageMessage otaStatus ' + JSON.stringify(otaStatus) + 'eventId is ' + eventId); + StateManager.createInstance(otaStatus).notify(globalThis.abilityContext, eventId); + globalThis.reNotify = undefined; + } + } + protected log(message: string): void { LogUtils.log('BaseAbility', message); } diff --git a/product/oh/base/src/main/ets/ServiceExtAbility/service.ts b/product/oh/base/src/main/ets/ServiceExtAbility/service.ts index 41acbad..cc0fe00 100644 --- a/product/oh/base/src/main/ets/ServiceExtAbility/service.ts +++ b/product/oh/base/src/main/ets/ServiceExtAbility/service.ts @@ -16,6 +16,7 @@ import Extension from '@ohos.app.ability.ServiceExtensionAbility'; import type Want from '@ohos.app.ability.Want'; import type rpc from '@ohos.rpc'; +import { OtaUpdateManager } from '@ohos/ota/src/main/ets/manager/OtaUpdateManager'; import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; /** @@ -37,6 +38,7 @@ export default class ServiceExtAbility extends Extension { LogUtils.log(ServiceExtAbility.TAG, `onRequest, want: ${want.abilityName}`); this.startIdArray.push(startId); globalThis.extensionContext = this.context; + await OtaUpdateManager.getInstance().handleWant(want, globalThis.extensionContext); this.stopSelf(startId); } @@ -45,14 +47,22 @@ export default class ServiceExtAbility extends Extension { return null; } + private isTerminal(): boolean { + let isTerminal: boolean = OtaUpdateManager.getInstance().isTerminal(); + return isTerminal; + } + private stopSelf(startId: number): void { this.startIdArray.splice(this.startIdArray.indexOf(startId), 1); LogUtils.info(ServiceExtAbility.TAG, 'stopSelf length ' + this.startIdArray.length); - if (this.startIdArray.length === 0) { - LogUtils.info(ServiceExtAbility.TAG, 'stopSelf'); - this.context?.terminateSelf().catch((err) => { - LogUtils.error(ServiceExtAbility.TAG, 'stopSelf err is ' + JSON.stringify(err)); - }); + if (this.startIdArray.length === 0 && this.isTerminal()) { + const terminateDelayTime = 2000; + setTimeout(()=> { + LogUtils.info(ServiceExtAbility.TAG, 'stopSelf'); + this.context?.terminateSelf().catch((err) => { + LogUtils.error(ServiceExtAbility.TAG, 'stopSelf err is ' + JSON.stringify(err)); + }); + }, terminateDelayTime); } } } \ No newline at end of file diff --git a/product/oh/base/src/main/ets/pages/currentVersion.ets b/product/oh/base/src/main/ets/pages/currentVersion.ets new file mode 100644 index 0000000..9118765 --- /dev/null +++ b/product/oh/base/src/main/ets/pages/currentVersion.ets @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import update from '@ohos.update'; +import { UpgradeCallResult } from '@ohos/common/src/main/ets/const/update_const'; +import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { VersionPageInfo } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; +import { UpgradeAdapter } from '@ohos/ota/src/main/ets/UpgradeAdapter'; +import { ChangelogContent } from '@ohos/ota/src/main/ets/components/ChangelogContent'; +import { OtaUpdateManager } from '@ohos/ota/src/main/ets/manager/OtaUpdateManager'; +import { VersionUtils } from '@ohos/ota/src/main/ets/util/VersionUtils'; + +/** + * 当前版本页面 + * + * @since 2022-06-06 + */ +@Entry +@Component +struct CurrentVersion { + + @State private versionArray: Array = null; + + aboutToAppear() { + globalThis.currentVersionThis = this; + } + + public onLanguageChange(): void { + this.initCurrentVersionPageInfo(); + } + + private async initCurrentVersionPageInfo(): Promise { + let upgradeData = await OtaUpdateManager.getInstance().getCurrentVersionInfo(); + let componentDescription = await OtaUpdateManager.getInstance().getCurrentVersionDescription(); + if (upgradeData.callResult == UpgradeCallResult.OK) { + if (!upgradeData.data) { + return; + } + let components: Array = VersionUtils.sortComponents( + upgradeData.data?.versionComponents); + this.versionArray = components.map((component: update.VersionComponent) => { + return UpgradeAdapter.getInstance() + .getPageInstance()?.getCurrentVersionPageInfo(components, componentDescription?.data); + }).filter((versionPageInfo: VersionPageInfo) => { + return versionPageInfo != null; + }); + } + } + + onPageShow(): void { + this.logInfo('onPageShow CurrentVersionPage'); + globalThis.currentPage = 'pages/currentVersion'; + this.initCurrentVersionPageInfo(); + } + + onPageHide(): void { + this.logInfo('onPageHide CurrentVersionPage'); + } + + + onBackPress() { + return false; + } + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Column() { + TitleBar({ title: $r('app.string.title_current_version'), onBack: this.onBackPress.bind(this) }) + }.flexShrink(0) + + Scroll() { + Column() { + Column() { + Column() { + Image($r('app.media.logo')) + .height($r('app.float.progress_logo_other_height')) + .width($r('app.float.progress_logo_other_width')) + } + .padding({ + top: $r('app.float.progress_logo_other_padding_top'), + bottom: $r('app.float.progress_logo_other_padding_bottom') + }) + }.flexShrink(0) + + if (this.versionArray) { + Column() { + ForEach(this.versionArray, (pageInfo: VersionPageInfo) => { + Text(pageInfo.version) + .fontSize($r('app.float.text_size_version_name')) + .maxLines(5) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .fontWeight(FontWeight.Medium) + .width('100%') + .textAlign(this.versionArray.length > 1 ? TextAlign.Start : TextAlign.Center) + .padding({ + right: $r('app.float.version_padding'), + left: $r('app.float.version_padding'), + bottom: this.versionArray.length > 1 ? $r('app.float.current_version_name_margin_bottom') : + $r('app.float.current_version_name_margin_bottom_single') + }) + ChangelogContent({ + isCurrentPage: true, + isNeedFold: this.versionArray?.length > 1, + description: JSON.stringify([pageInfo.changelog]) + }) + }) + }.flexShrink(0).margin({ bottom: $r('app.float.current_version_changelog_margin_bottom') }) + } + }.width('100%').flexShrink(0) + }.width('100%') + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Auto) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.page_background')) + } + + private logInfo(message: string): void { + LogUtils.info('CurrentVersion', message); + } +} \ No newline at end of file diff --git a/product/oh/base/src/main/ets/pages/index.ets b/product/oh/base/src/main/ets/pages/index.ets index c67efdc..e78742b 100644 --- a/product/oh/base/src/main/ets/pages/index.ets +++ b/product/oh/base/src/main/ets/pages/index.ets @@ -13,6 +13,15 @@ * limitations under the License. */ +import update from '@ohos.update'; +import { + ErrorCode, + OtaStatus, + UpdateConstant, + UpdateState, + UpgradeCallResult, + UpgradeData +} from '@ohos/common/src/main/ets/const/update_const'; import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar'; import { HomeCardView } from '@ohos/common/src/main/ets/component/HomeCardView'; import { CheckingDots } from '@ohos/common/src/main/ets/component/CheckingDots'; @@ -20,6 +29,9 @@ import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils'; import RouterUtils from '@ohos/ota/src/main/ets/util/RouterUtils'; import { VersionUtils } from '@ohos/ota/src/main/ets/util/VersionUtils'; +import { OtaUpdateManager } from '@ohos/ota/src/main/ets/manager/OtaUpdateManager'; +import { StateManager, UpdateAction } from '@ohos/ota/src/main/ets/manager/StateManager'; +import ToastUtils from '@ohos/ota/src/main/ets/util/ToastUtils'; /** * 搜包的状态 @@ -43,8 +55,11 @@ struct Index { @State newVersionActionText: Resource = $r('app.string.check_version_status_no_new'); @State dotTextPlay: boolean = false; @State newVersionStatus: string = NewVersionStatus.NO_NEW_VERSION; + @StorageProp('updateStatus') + private updateStatus: number = AppStorage.Get('updateStatus'); private displayVersion: string = VersionUtils.getDisplayVersionForIndex(); private videoController: VideoController = new VideoController(); + private initUpdateStatus: number = OtaUpdateManager.getInstance().getUpdateState(); private actionCallBack: Array<() => void> = []; private checkTimes: number= 0; private checkIntervalId: number; @@ -103,9 +118,60 @@ struct Index { this.uiCheckLoop(true); this.checkLoopTimes = 0; this.notifyNewVersionStatus(NewVersionStatus.CHECKING); - setTimeout(() => { - this.notifyNewVersionStatus(NewVersionStatus.NO_NEW_VERSION); - }, 2000) + let upgradeData: UpgradeData = await OtaUpdateManager.getInstance().getOtaStatus(); + let otaStatus: OtaStatus = upgradeData.callResult == UpgradeCallResult.OK ? upgradeData.data : null; + this.initUpdateStatus = otaStatus?.status; + if (StateManager.isAllowExecute(this.updateStatus, UpdateAction.CHECK_NEW_VERSION)) { + this.performCheckNewVersion(); + } else { + OtaUpdateManager.getInstance().getNewVersion().then(upgradeData => { + if (upgradeData.callResult == UpgradeCallResult.OK) { + this.log('found new version.'); + RouterUtils.isCanToNewVersion().then((isCan: boolean) => { + this.notifyNewVersionStatus(isCan ? NewVersionStatus.HAS_NEW_VERSION : NewVersionStatus.NO_NEW_VERSION); + }); + } else { + this.log('no new version found.'); + this.notifyNewVersionStatus(NewVersionStatus.NO_NEW_VERSION); + } + }); + } + } + + /** + * 开始进行手动新版本检测 + * 通过checkNewVersion()方法 + */ + private async performCheckNewVersion(): Promise { + this.log('checkNewVersion starting manually'); + let upgradeData: UpgradeData = await OtaUpdateManager.getInstance().checkNewVersion(); + if (upgradeData.callResult == UpgradeCallResult.OK) { + this.log('handle checkNewVersion:' + JSON.stringify(upgradeData.data?.newVersionInfo)); + if (await RouterUtils.isCanToNewVersion()) { + this.notifyNewVersionStatus(NewVersionStatus.HAS_NEW_VERSION); + return; + } + } + let errorCode: ErrorCode = upgradeData.error?.data?.[0]?.errorCode; + if (errorCode == ErrorCode.CHECK_SYSTEM_BUSY) { + setTimeout(() => { + if (this.checkLoopTimes >= UpdateConstant.CHECKING_RETRY_TIME) { + this.notifyNewVersionStatus(NewVersionStatus.NO_NEW_VERSION); + } else { + this.checkLoopTimes++; + this.performCheckNewVersion(); + } + }, UpdateConstant.CHECKING_WAITING_TIME_IN_SECONDS * 1000); + return; + } + + if (errorCode == ErrorCode.CHECK_NETWORK_ERR) { + this.actionCallBack.push(async () => { + let message = await globalThis.abilityContext.resourceManager.getString($r('app.string.network_err_toast').id); + ToastUtils.showToast(message); + }) + } + this.notifyNewVersionStatus(NewVersionStatus.NO_NEW_VERSION); } /** @@ -134,6 +200,7 @@ struct Index { aboutToAppear() { this.log('aboutToAppear'); + globalThis.indexThis = this; this.checkNewVersionIfNeed(); } @@ -151,6 +218,11 @@ struct Index { if (this.newVersionStatus === NewVersionStatus.CHECKING) { this.uiCheckLoop(true); } + if (this.isNeedCheckNewVersion()) { //控制页面回退时是否执行搜包 + this.log('isNeedCheckNewVersion, start checkNewVersionIfNeed.'); + this.initUpdateStatus = OtaUpdateManager.getInstance().getUpdateState(); + this.checkNewVersionIfNeed(); + } } public onLanguageChange(): void { @@ -166,6 +238,22 @@ struct Index { this.uiCheckLoop(false); } + /** + * onpage show时判断,当前状态是否需要自动触发搜包 + */ + private isNeedCheckNewVersion(): boolean { + if (this.newVersionStatus == NewVersionStatus.CHECKING) { + return false; + } + let currentStatus = OtaUpdateManager.getInstance().getUpdateState(); + this.log(`checkNewVersion, currentStatus = ${currentStatus} `); + if (this.initUpdateStatus != currentStatus) { + const checkedStatus = [UpdateState.INIT, UpdateState.UPGRADE_FAILED, UpdateState.UPGRADE_SUCCESS]; + return currentStatus in checkedStatus; + } + return false; + } + build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { TitleBar({ @@ -245,5 +333,16 @@ struct Index { private async handleStateClicked() { this.log('handleStateClicked.'); + if (this.newVersionStatus == NewVersionStatus.HAS_NEW_VERSION) { + if (await RouterUtils.isCanToNewVersion()) { + RouterUtils.openNewVersionPage(); + } else { + this.checkNewVersionIfNeed(); + } + return; + } + if (this.newVersionStatus == NewVersionStatus.NO_NEW_VERSION) { + RouterUtils.openCurrentVersion(); + } } } \ No newline at end of file diff --git a/product/oh/base/src/main/ets/pages/newVersion.ets b/product/oh/base/src/main/ets/pages/newVersion.ets new file mode 100644 index 0000000..660fbc3 --- /dev/null +++ b/product/oh/base/src/main/ets/pages/newVersion.ets @@ -0,0 +1,352 @@ +import { DialogUtils } from '../../../../../../../feature/ota/src/main/ets/dialog/DialogUtils'; +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CountDownDialogType, + OtaStatus, + UpdateState, + UpgradeCallResult, + UpgradeData +} from '@ohos/common/src/main/ets/const/update_const'; +import update from '@ohos.update'; +import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar'; +import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils'; +import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; +import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils'; +import { ChangelogInfo, IPage, VersionPageInfo } from '@ohos/common/src/main/ets/manager/UpgradeInterface'; +import { NetUtils } from '@ohos/common/src/main/ets/util/NetUtils'; +import { DialogHelper } from '@ohos/ota/src/main/ets/dialog/DialogHelper'; +import { ChangelogContent } from '@ohos/ota/src/main/ets/components/ChangelogContent'; +import { ProgressContent } from '@ohos/ota/src/main/ets/components/ProgressContent'; +import { OtaUpdateManager } from '@ohos/ota/src/main/ets/manager/OtaUpdateManager'; +import { CountDownInstallDialogBuilder } from '@ohos/ota/src/main/ets/dialog/CountDownInstallDialogBuilder'; +import { MessageDialogBuilder, } from '@ohos/ota/src/main/ets/dialog/MessageDialogBuilder'; +import { StateManager, UpdateAction } from '@ohos/ota/src/main/ets/manager/StateManager'; +import { UpgradeAdapter } from '@ohos/ota/src/main/ets/UpgradeAdapter'; +import { VersionUtils } from '@ohos/ota/src/main/ets/util/VersionUtils'; +import { NotificationHelper } from '@ohos/ota/src/main/ets/notify/NotificationHelper'; + +/** + * 新版本页面 + * + * @since 2022-06-06 + */ +@Entry +@Component +struct NewVersion { + @State private displayFileSize: string = ''; + @State @Watch('refreshDescription') private changelogArray: Array = []; + @State private description: string = ''; + @State private displayNewVersionName: string = ''; + @State private isButtonEnable: boolean = true; + @State private isButtonVisible: boolean = true; + @StorageProp('updateStatus') + @Watch('initDataByStatus') private updateStatus: number = AppStorage.Get('updateStatus'); + @StorageProp('isClickInstall') @Watch('onInstallClick') + private isClickInstall: number = AppStorage.Get('isClickInstall'); + private dialogText: string | Resource = ''; + @State private isInitComplete: boolean = false; + private effectiveMode: update.EffectiveMode = update.EffectiveMode.COLD; + private dialogType: CountDownDialogType = CountDownDialogType.OTA; + @State private buttonText: string = ''; + @StorageProp('installStatusRefresh') @Watch('refresh') + private installStatusRefresh: string = AppStorage.Get('installStatusRefresh'); + + private countdownDialogController = new CustomDialogController({ + builder: CountDownInstallDialogBuilder({ + textString: this.dialogText, + dialogType: this.dialogType, + cancel: () => { + globalThis.displayCountdownDialog = false; + }, + confirm: () => { + globalThis.displayCountdownDialog = false; + this.upgrade(); + } + }), + autoCancel: false, + alignment: DeviceUtils.getDialogLocation(), + offset: ({ + dx: '0vp', + dy: DeviceUtils.getDialogOffsetY() + }) + }); + + private restartDialogController = new CustomDialogController({ + builder: MessageDialogBuilder({ message: $r('app.string.reboot_wait') }), + autoCancel: false, + alignment: DeviceUtils.getDialogLocation(), + offset: ({ + dx: '0vp', + dy: DeviceUtils.getDialogOffsetY() + }) + }); + + private refreshDescription(): void { + this.log('refreshDescription'); + this.description = JSON.stringify(this.changelogArray); + } + + aboutToAppear() { + this.log('aboutToAppear'); + globalThis.newVersionThis = this; + } + + onPageShow() { + this.log('onPageShow NewVersionPage.'); + globalThis.currentPage = 'pages/newVersion'; + this.initDataByStatus(); + new NotificationHelper().cancelAll(); + this.handleAbnormalState(); + } + + private async handleAbnormalState(): Promise { + let upgradeData: UpgradeData = await OtaUpdateManager.getInstance().getOtaStatus(); + let otaStatus: OtaStatus = upgradeData.callResult == UpgradeCallResult.OK ? upgradeData.data : null; + let state = otaStatus?.status; + if (!state || state === UpdateState.INIT || state === UpdateState.UPGRADE_SUCCESS || + state === UpdateState.UPGRADE_FAILED || state === UpdateState.INSTALL_FAILED) { + this.closeUpgradeDialog(); + return; + } + } + + onPageHide() { + this.log('onPageHide NewVersionPage'); + } + + onBackPress() { + return globalThis.displayCountdownDialog; + } + + /** + * raise by notify installWantAgentInfo + */ + private raiseCountDialogByClick(): void { + this.showCountdownDialog(UpdateState.DOWNLOAD_SUCCESS); + globalThis.abilityWant = null; + } + + private onInstallClick(): void { + this.log('onInstallClick'); + if (this.isClickInstall) { + this.raiseCountDialogByClick(); + } + } + + private async showCountdownDialog(updateStatus: number): Promise { + this.log('showCountdownDialog'); + if (StateManager.isAllowExecute(updateStatus, UpdateAction.INSTALL)) { + globalThis.displayCountdownDialog = true; // dialog display + AppStorage.Set('isClickInstall', 0); // reset + this.countdownDialogController.open(); + } + } + + public onLanguageChange(): void { + this.initDataByStatus(); + } + + private async initDataByStatus(): Promise { + this.isButtonEnable = StateManager.isButtonEnable(this.updateStatus); + await this.initNewVersionPageInfo(); + if (this.isClickInstall) { + this.raiseCountDialogByClick(); + } + let stateButtonText = StateManager.getButtonText(this.updateStatus); + this.buttonText = FormatUtils.toUpperCase(globalThis.abilityContext, stateButtonText); + this.isButtonVisible = this.updateStatus !== UpdateState.INSTALLING + && this.updateStatus !== UpdateState.INSTALL_FAILED + && this.updateStatus !== UpdateState.INSTALL_SUCCESS + && this.updateStatus !== UpdateState.UPGRADING + && this.updateStatus !== UpdateState.UPGRADE_FAILED + && this.updateStatus !== UpdateState.UPGRADE_SUCCESS; + } + + public async initNewVersionPageInfo(): Promise { + let newVersionInfo = globalThis.cachedNewVersionInfo || await OtaUpdateManager.getInstance().getNewVersion() + .then(upgradeData => { + this.log(`initDataByStatus upgradeData: ${upgradeData}`); + return upgradeData.callResult == UpgradeCallResult.OK ? upgradeData.data : null; + }); + let componentDescription = await OtaUpdateManager.getInstance().getNewVersionDescription(); + if (!newVersionInfo) { + return; + } + let components: Array = VersionUtils.sortComponents(newVersionInfo?.versionComponents); + let pageInfo: VersionPageInfo = await UpgradeAdapter.getInstance() + .getPageInstance()?.getNewVersionPageInfo(components); + this.displayNewVersionName = pageInfo?.version; + this.dialogText = pageInfo?.countDownDialogInfo?.dialogText; + this.dialogType = pageInfo?.countDownDialogInfo?.dialogType; + this.effectiveMode = pageInfo?.effectiveMode; + let size: number = 0; + let array :Array = []; + + for (let index = 0; index < components?.length; index ++) { + let page: IPage = UpgradeAdapter.getInstance().getPageInstance(); + if (!page) { + continue; + } + let versionPageInfo: VersionPageInfo = await page.getNewVersionPageInfo(components, componentDescription.data); + size += versionPageInfo.size; + array.push(versionPageInfo.changelog); + } + this.changelogArray = array; + this.displayFileSize = FormatUtils.formatFileSize(size); + this.isInitComplete = true; + } + + private async upgrade(): Promise { + this.log('upgrade effectiveMode ' + this.effectiveMode); + this.isButtonEnable = false; + let order = this.isABInstall() ? update.Order.INSTALL: update.Order.INSTALL_AND_APPLY + OtaUpdateManager.getInstance().upgrade(order).catch(err => { + this.isButtonEnable = true; + }); + } + + private async reboot(): Promise { + this.log('reboot!'); + if (this.isABInstall()) { + OtaUpdateManager.getInstance().upgrade(update.Order.APPLY); + } + } + + private async handleButtonClick(): Promise { + this.log('handleButtonClick'); + switch (StateManager.getButtonClickAction(this.updateStatus)) { + case UpdateAction.DOWNLOAD: + if (await NetUtils.isCellularNetwork()) { + DialogHelper.displayNetworkDialog({ + onConfirm: () => { + OtaUpdateManager.getInstance().download(update.NetType.CELLULAR); + } }); + } else { + OtaUpdateManager.getInstance().download(); + } + break; + case UpdateAction.INSTALL: + this.upgrade(); + break; + case UpdateAction.REBOOT: + this.reboot(); + break; + case UpdateAction.CANCEL: + OtaUpdateManager.getInstance().cancel(); + break; + case UpdateAction.RESUME: + OtaUpdateManager.getInstance().resumeDownload(); + break; + default: + break; + } + } + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Column() { + TitleBar({ title: $r('app.string.title_new_version'), onBack: this.onBackPress.bind(this)}); + }.flexShrink(0); + Column() { + Scroll() { + Column() { + ProgressContent(); + if (this.isInitComplete) { + Text(this.displayNewVersionName) + .fontSize($r('app.float.text_size_version_name')) + .maxLines(5) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .padding({ + right: $r('app.float.changelog_detail_content_padding_horizontal'), + left: $r('app.float.changelog_detail_content_padding_horizontal'), + }) + .fontWeight(FontWeight.Medium); + Text(this.displayFileSize) + .fontSize($r('app.float.text_size_version_size')).fontWeight(FontWeight.Regular).opacity(0.6) + .margin({ + top: $r('app.float.new_version_size_margin_top'), + bottom: $r('app.float.new_version_size_margin_bottom') + }) + ChangelogContent({ + isCurrentPage: false, + isNeedFold: this.changelogArray?.length > 1, + description: this.description + }) + } + }.width('100%').flexShrink(0) + } + .width('100%') + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Auto) + }.flexGrow(1) + + Column() { + Button() { + Text(this.buttonText) + .fontSize($r('app.float.text_size_btn')).fontColor(Color.White).fontWeight(FontWeight.Medium) + .margin({ left: $r('app.float.custom_button_text_margin_left'), + right: $r('app.float.custom_button_text_margin_right') }) + } + .type(ButtonType.Capsule) + .constraintSize({ minWidth: $r('app.float.custom_button_width') }) + .height($r('app.float.custom_button_height')) + .backgroundColor($r('app.color.blue')) + .opacity(this.isButtonEnable ? 1 : 0.4) + .visibility(this.isButtonVisible ? Visibility.Visible : Visibility.None) + .onClick(() => { + if(this.isButtonEnable) { + this.handleButtonClick(); + } + }) + } + .flexShrink(0) + .padding({ + top: $r('app.float.new_version_button_padding_top'), + bottom: $r('app.float.new_version_button_padding_bottom') + }) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.page_background')) + } + + private log(message: string): void { + LogUtils.log('NewVersion', message); + } + + private closeUpgradeDialog() { + this.restartDialogController.close(); + } + + private isABInstall(): boolean { + return this.effectiveMode === update.EffectiveMode.LIVE_AND_COLD; + } + + private refresh() { + if (this.isABInstall()) { + return; + } + let otaStatus: OtaStatus = JSON.parse(this.installStatusRefresh); + let status: UpdateState = otaStatus?.status; + if (status === UpdateState.INSTALL_FAILED || status === UpdateState.UPGRADE_FAILED + || status === UpdateState.UPGRADE_SUCCESS) { + this.closeUpgradeDialog(); + } else if (status === UpdateState.UPGRADING) { + this.restartDialogController.open(); + } + } +} \ No newline at end of file diff --git a/product/oh/base/src/main/resources/base/profile/main_pages.json b/product/oh/base/src/main/resources/base/profile/main_pages.json index 4f0fe13..aea3512 100644 --- a/product/oh/base/src/main/resources/base/profile/main_pages.json +++ b/product/oh/base/src/main/resources/base/profile/main_pages.json @@ -1,5 +1,7 @@ { "src": [ - "pages/index" + "pages/index", + "pages/newVersion", + "pages/currentVersion" ] } \ No newline at end of file -- Gitee From 8dc8309881692a8ff3cc317d572ae35dc9078cb5 Mon Sep 17 00:00:00 2001 From: jackd320 Date: Mon, 10 Apr 2023 21:51:23 +0800 Subject: [PATCH 2/2] Signed-off-by: jackd320 --- common/index.ets | 8 ++++++++ feature/ota/src/main/ets/dialog/DialogUtils.ts | 2 +- feature/ota/src/main/ets/util/RouterUtils.ts | 5 +++-- product/oh/base/src/main/ets/MainAbility/MainAbility.ts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/common/index.ets b/common/index.ets index 4e55e90..3be6d4e 100644 --- a/common/index.ets +++ b/common/index.ets @@ -17,7 +17,15 @@ export { CheckingDots } from './src/main/ets/component/CheckingDots'; export { HomeCardView } from './src/main/ets/component/HomeCardView'; export { TitleBar } from './src/main/ets/component/TitleBar'; +export { UpdateState, OtaStatus, ErrorCode, UpdateConstant, Changelog, Language, + Features, Feature, Icon, ChangelogType, Action, UpgradeData, UpgradeCallResult, PACKAGE_NAME, + MAIN_ABILITY_NAME +} from './src/main/ets/const/update_const'; + +export { UpdateManager } from './src/main/ets/manager/UpdateManager'; + export { UpdateUtils } from './src/main/ets/util/UpdateUtils'; export { DeviceUtils } from './src/main/ets/util/DeviceUtils'; export { FormatUtils } from './src/main/ets/util/FormatUtils'; export { LogUtils } from './src/main/ets/util/LogUtils'; +export { NetUtils } from './src/main/ets/util/NetUtils'; \ No newline at end of file diff --git a/feature/ota/src/main/ets/dialog/DialogUtils.ts b/feature/ota/src/main/ets/dialog/DialogUtils.ts index 2d01ff9..3525711 100644 --- a/feature/ota/src/main/ets/dialog/DialogUtils.ts +++ b/feature/ota/src/main/ets/dialog/DialogUtils.ts @@ -14,7 +14,7 @@ */ import type common from '@ohos.app.ability.common'; -import update from '@ohos.update'; +import type update from '@ohos.update'; import type { OtaStatus } from '@ohos/common/src/main/ets/const/update_const'; import { MAIN_ABILITY_NAME, PACKAGE_NAME, UpdateState } from '@ohos/common/src/main/ets/const/update_const'; import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; diff --git a/feature/ota/src/main/ets/util/RouterUtils.ts b/feature/ota/src/main/ets/util/RouterUtils.ts index 266c60c..d622959 100644 --- a/feature/ota/src/main/ets/util/RouterUtils.ts +++ b/feature/ota/src/main/ets/util/RouterUtils.ts @@ -80,9 +80,10 @@ namespace RouterUtils { OtaUpdateManager.getInstance().getOtaStatus().then((upgradeData) => { if (upgradeData?.callResult === UpgradeCallResult.OK) { if (upgradeData.data?.status === UpdateState.UPGRADING) { - resolve(false); + resolve(false); } else { - resolve(StateManager.isAllowExecute(upgradeData.data?.status, UpdateAction.SHOW_NEW_VERSION)); + let isAllow: boolean = StateManager.isAllowExecute(upgradeData.data?.status, UpdateAction.SHOW_NEW_VERSION); + resolve(isAllow); } } else { resolve(false); diff --git a/product/oh/base/src/main/ets/MainAbility/MainAbility.ts b/product/oh/base/src/main/ets/MainAbility/MainAbility.ts index 1ec61a1..ca592d6 100644 --- a/product/oh/base/src/main/ets/MainAbility/MainAbility.ts +++ b/product/oh/base/src/main/ets/MainAbility/MainAbility.ts @@ -17,7 +17,7 @@ import Ability from '@ohos.app.ability.UIAbility'; import type Want from '@ohos.app.ability.Want'; import type AbilityConstant from '@ohos.app.ability.AbilityConstant'; import router from '@ohos.router'; -import update from '@ohos.update'; +import type update from '@ohos.update'; import type window from '@ohos.window'; import type { Configuration } from '@ohos.app.ability.Configuration'; import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils'; -- Gitee