已开启
新增自动下载和夜间升级开关、UX页面重写及多项问题修复 #88
新增自动下载和夜间升级开关、UX页面重写及多项问题修复 #88
已开启
若水随风创建于 22 天前
42 个文件变更+1784-176
@@ -0,0 +1,12 @@
1+node_modules/
2+oh_modules/
3+local.properties
4+.idea/
5+build/
6+.hvigor/
7+.cxx
8+/.clangd
9+/.clang-format
10+/.clang-tidy
11+**/.test
12+/.appanalyzer
@@ -4,8 +4,9 @@
4 {4 {
5 "name": "default",5 "name": "default",
6 "signingConfig": "release",6 "signingConfig": "release",
7- "compileSdkVersion": 20,7+ "targetSdkVersion": 23,
8- "compatibleSdkVersion": 20,8+ "compatibleSdkVersion": 23,
9+ "compileSdkVersion": "26.0.0",
9 "runtimeOS": "OpenHarmony",10 "runtimeOS": "OpenHarmony",
10 "buildOption": {11 "buildOption": {
11 "strictMode": {12 "strictMode": {
@@ -14,6 +15,20 @@
14 }15 }
15 }16 }
16 }17 }
18+ ],
19+ "signingConfigs": [
20+ {
21+ "name": "release",
22+ "material": {
23+ "storeFile": "signature/OpenHarmony.p12",
24+ "storePassword": "00000016F657C3513233C0691A4CB761368F8674780E0FA1E5115D2CD38E1A378203F9A232CD",
25+ "keyAlias": "OpenHarmony Application Release",
26+ "keyPassword": "0000001618FE1FC8EE919CBDD32E6D48440BB900C030258644853134C4EE40B4665C6BB37BB3",
27+ "signAlg": "SHA256withECDSA",
28+ "profile": "signature/updateapp.p7b",
29+ "certpath": "signature/OpenHarmony.cer"
30+ }
31+ }
17 ]32 ]
18 },33 },
19 "modules": [34 "modules": [
@@ -0,0 +1,93 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+const TAG = 'SettingOptions'
17+ 
18+/**
19+ * 设置选项组件
20+ *
21+ * @since 2026-04-10
22+ */
23+@Component
24+export struct SettingOptions {
25+ private title?: string | Resource;
26+ private description?: string | Resource;
27+ @Prop private isEnabled: boolean = true;
28+ private toggleChange?: (isOn: boolean) => void;
29+ @Prop private isOn: boolean = false;
30+ private themeColor?: ResourceColor;
31+ 
32+ @Builder
33+ private optionToggle(isEnabled: boolean) {
34+ Toggle({ type: ToggleType.Switch, isOn: this.isOn })
35+ .width('36vp')
36+ .height('20vp')
37+ .flexShrink(0)
38+ .enabled(isEnabled)
39+ .onChange((isOn) => {
40+ this.toggleChange?.(isOn)
41+ })
42+ .hoverEffect(HoverEffect.None)
43+ .selectedColor(this.themeColor)
44+ .id(`${TAG}_Toggle_01`)
45+ }
46+ 
47+ build() {
48+ Column() {
49+ Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
50+ Text(this.title)
51+ .fontSize($r('sys.float.ohos_id_text_size_body1'))
52+ .width('100%')
53+ .fontColor($r('sys.color.ohos_id_color_text_primary'))
54+ .fontWeight(FontWeight.Medium)
55+ .draggable(false)
56+ .id(`${TAG}_Text_02`)
57+ 
58+ if ((this.isEnabled)) {
59+ this.optionToggle(true);
60+ } else {
61+ this.optionToggle(false);
62+ }
63+ }
64+ .backgroundColor($r('sys.color.comp_background_list_card'))
65+ .padding({
66+ top: $r('app.float.setting_options_padding_vertical'),
67+ right: $r('app.float.setting_options_padding_horizontal'),
68+ bottom: $r('app.float.setting_options_padding_vertical'),
69+ left: $r('app.float.setting_options_padding_horizontal')
70+ })
71+ .id(`${TAG}_Flex_01`)
72+ .constraintSize({ minHeight: $r('app.float.setting_options_height') })
73+ .borderRadius($r('app.float.setting_border_radius'))
74+ 
75+ Text(this.description)
76+ .fontSize($r('app.float.text_size_body'))
77+ .width('100%')
78+ .fontColor($r('sys.color.ohos_id_color_text_secondary'))
79+ .padding({
80+ top: $r('app.float.setting_options_margin_top'),
81+ right: $r('app.float.setting_options_padding_horizontal'),
82+ left: $r('app.float.setting_options_padding_horizontal')
83+ })
84+ .draggable(false)
85+ .id(`${TAG}_Text_01`)
86+ }
87+ .margin({
88+ right: $r('app.float.page_left_or_right_margin'),
89+ left: $r('app.float.page_left_or_right_margin')
90+ })
91+ .id(`${TAG}_Column_01`)
92+ }
93+}
@@ -26,15 +26,21 @@ export struct TitleBar {
26 private static readonly TAG = 'TitleBar';26 private static readonly TAG = 'TitleBar';
27 private title: string | Resource = '';27 private title: string | Resource = '';
28 private onBack?: () => boolean;28 private onBack?: () => boolean;
29+ @Prop private showMenu: boolean = false;
29 30 
30 build() {31 build() {
31 Flex({ justifyContent: FlexAlign.SpaceBetween }) {32 Flex({ justifyContent: FlexAlign.SpaceBetween }) {
32 Row() {33 Row() {
33 Row() {34 Row() {
34- Image($r('app.media.back'))35+ Button({ type: ButtonType.Circle, stateEffect: true }) {
35- .width($r('app.float.title_bar_icon_width'))36+ SymbolGlyph($r('sys.symbol.chevron_backward'))
36- .height($r('app.float.title_bar_icon_height'))37+ .fontSize($r('app.float.title_bar_symbol_font_size'))
37- .objectFit(ImageFit.Contain)38+ .fontColor([$r('sys.color.ohos_id_color_text_primary')])
39+ }
40+ .width($r('app.float.title_bar_icon_width'))
41+ .height($r('app.float.title_bar_icon_height'))
42+ .backgroundColor($r('sys.color.ohos_id_color_button_normal'))
43+ .borderRadius($r('sys.float.ohos_id_corner_radius_icon'))
38 }.height($r('app.float.title_bar_height'))44 }.height($r('app.float.title_bar_height'))
39 .margin({ left: $r('app.float.title_bar_icon_margin_left') })45 .margin({ left: $r('app.float.title_bar_icon_margin_left') })
40 .onClick(() => {46 .onClick(() => {
@@ -54,6 +60,25 @@ export struct TitleBar {
54 .fontWeight(FontWeight.Bold)60 .fontWeight(FontWeight.Bold)
55 }.flexGrow(1)61 }.flexGrow(1)
56 .height($r('app.float.title_bar_height'))62 .height($r('app.float.title_bar_height'))
63+ 
64+ if (this.showMenu) {
65+ this.showMenuBuilder();
66+ }
57 }67 }
58 }68 }
69+ 
70+ @Builder
71+ showMenuBuilder() {
72+ Button({ type: ButtonType.Circle, stateEffect: true }) {
73+ SymbolGlyph($r('sys.symbol.dot_grid_2x2'))
74+ }
75+ .width('28vp')
76+ .backgroundColor('#0C000000')
77+ .margin({ right: $r('app.float.title_bar_text_margin_left'), top: $r('app.float.title_bar_text_margin_left') })
78+ .height('28vp')
79+ .onClick(() => {
80+ LogUtils.info(TitleBar.TAG, 'show menu click.');
81+ router.push({ url: 'pages/settingsView' })
82+ })
83+ }
59}84}
@@ -376,7 +376,12 @@ export enum Action {
376 /**376 /**
377 * 动作--升级失败跳转主页面搜包377 * 动作--升级失败跳转主页面搜包
378 */378 */
379- NOTIFICATION_HOT_UPGRADE_FAILED = 'com.ohos.updateapp.hot_upgrade_failed'379+ NOTIFICATION_HOT_UPGRADE_FAILED = 'com.ohos.updateapp.hot_upgrade_failed',
380+ 
381+ /**
382+ * 动作--跳转夜间升级设置页
383+ */
384+ NOTIFICATION_SETTINGS = 'com.ohos.updateapp.settings'
380}385}
381 386 
382/**387/**
@@ -490,4 +495,14 @@ export interface CustomEventInfo {
490 * Task body info495 * Task body info
491 */496 */
492 taskBody: update.TaskBody;497 taskBody: update.TaskBody;
493-}498+}
499+ 
500+/**
501+ * 夜间升级开始时间
502+ */
503+export const NIGHT_UPGRADE_START = 2;
504+ 
505+/**
506+ * 夜间升级结束时间
507+ */
508+export const NIGHT_UPGRADE_END = 4;
@@ -415,6 +415,33 @@ export class UpdateManager implements IUpdate {
415 });415 });
416 }416 }
417 417 
418+ /**
419+ * 获取升级策略
420+ *
421+ * @returns resolve 升级策略
422+ */
423+ async getUpdatePolicy(): Promise<update.UpgradePolicy> {
424+ return this.otaUpdater.getUpgradePolicy();
425+ }
426+ 
427+ /**
428+ * 设置升级策略
429+ *
430+ * @param policy 策略
431+ * @returns resolve 设置结果/reject 错误信息
432+ */
433+ async setUpdatePolicy(policy: update.UpgradePolicy): Promise<void> {
434+ this.log(`setUpdatePolicy download:${policy.downloadStrategy}, autoUpgrade:${policy.autoUpgradeStrategy}`);
435+ return new Promise((resolve, reject) => {
436+ this.otaUpdater?.setUpgradePolicy(policy).then(() => {
437+ resolve();
438+ }).catch((err: BusinessError) => {
439+ this.logError('setUpdatePolicy err: ' + JSON.stringify(err));
440+ reject(err);
441+ });
442+ });
443+ }
444+ 
418 private log(message: string): void {445 private log(message: string): void {
419 LogUtils.log('UpdateManager', message);446 LogUtils.log('UpdateManager', message);
420 }447 }
@@ -161,6 +161,14 @@ export interface IPage {
161 * @since 2022-12-01161 * @since 2022-12-01
162 */162 */
163export interface INotify {163export interface INotify {
164+ /**
165+ * 下载提醒通知
166+ *
167+ * @param versionName 版本号
168+ * @param context 上下文
169+ */
170+ showDownloadReminder(versionName: string, context: common.Context): Promise<void>;
171+ 
164 /**172 /**
165 * 下载进度通知173 * 下载进度通知
166 *174 *
@@ -170,6 +178,22 @@ export interface INotify {
170 */178 */
171 showDownloading(version: string, progress: number, context: common.Context): Promise<void>;179 showDownloading(version: string, progress: number, context: common.Context): Promise<void>;
172 180 
181+ /**
182+ * 下载暂停通知
183+ *
184+ * @param version 版本号
185+ * @param progress 进度
186+ * @param context 上下文
187+ */
188+ showDownloadPaused(version: string, progress: number, context: common.Context): Promise<void>;
189+ 
190+ /**
191+ * 升级包校验失败通知
192+ *
193+ * @param context 上下文
194+ */
195+ showVerifyFailed(context: common.Context): Promise<void>;
196+ 
173 /**197 /**
174 * 升级失败通知198 * 升级失败通知
175 *199 *
@@ -186,6 +210,13 @@ export interface INotify {
186 */210 */
187 showUpgradeSuccess(versionName: string, context: common.Context): Promise<void>;211 showUpgradeSuccess(versionName: string, context: common.Context): Promise<void>;
188 212 
213+ /**
214+ * 夜间升级设置通知
215+ *
216+ * @param context 上下文
217+ */
218+ showNightUpgradeNotice(context: common.Context): Promise<void>;
219+ 
189 /**220 /**
190 * 安装中通知221 * 安装中通知
191 *222 *
@@ -61,7 +61,7 @@ export namespace DeviceUtils {
61 * @return dialog位置61 * @return dialog位置
62 */62 */
63 export function getDialogLocation(): DialogAlignment{63 export function getDialogLocation(): DialogAlignment{
64- return DialogAlignment.Bottom;64+ return DialogAlignment.Center;
65 }65 }
66 66 
67 /**67 /**
@@ -70,6 +70,6 @@ export namespace DeviceUtils {
70 * @return dialog偏移70 * @return dialog偏移
71 */71 */
72 export function getDialogOffsetY(): string | Resource {72 export function getDialogOffsetY(): string | Resource {
73- return $r('app.float.dialog_location_offset_y');73+ return '0vp';
74 }74 }
75}75}
@@ -16,6 +16,7 @@
16import type common from '@ohos.app.ability.common';16import type common from '@ohos.app.ability.common';
17import { DeviceUtils } from '../util/DeviceUtils';17import { DeviceUtils } from '../util/DeviceUtils';
18import { LogUtils } from './LogUtils';18import { LogUtils } from './LogUtils';
19+import i18n from '@ohos.i18n';
19 20 
20const DECIMAL_POINT = 2;21const DECIMAL_POINT = 2;
21 22 
@@ -102,4 +103,21 @@ export class FormatUtils {
102 let numfmt: Intl.NumberFormat = new Intl.NumberFormat(language, {style:'percent', notation:'standard'});103 let numfmt: Intl.NumberFormat = new Intl.NumberFormat(language, {style:'percent', notation:'standard'});
103 return numfmt.format(num);104 return numfmt.format(num);
104 }105 }
106+ 
107+ /**
108+ * 获取小时范围格式化
109+ *
110+ * @param start 开始时间
111+ * @param end 结束时间
112+ * @returns string 格式化之后的小时范围
113+ */
114+ static getDateRangeFormat(start: number, end: number): string {
115+ let current = new Date();
116+ let startDate = new Date(current.getFullYear(), current.getMonth(), current.getDate(), start, 0, 0);
117+ let endDate = new Date(current.getFullYear(), current.getMonth(), current.getDate(), end, 0, 0);
118+ let language: string = DeviceUtils.getSystemLanguage();
119+ let datefmt: Intl.DateTimeFormat =
120+ new Intl.DateTimeFormat(language, { hour12: !i18n.System.is24HourClock(), hour: 'numeric', minute: 'numeric' });
121+ return datefmt.formatRange(startDate, endDate) as string;
122+ }
105}123}
@@ -68,14 +68,26 @@
68 "name": "title_bar_icon_height",68 "name": "title_bar_icon_height",
69 "value": "24vp"69 "value": "24vp"
70 },70 },
71+ {
72+ "name": "title_bar_icon_background_height",
73+ "value": "40vp"
74+ },
75+ {
76+ "name": "title_bar_icon_background_width",
77+ "value": "40vp"
78+ },
71 {79 {
72 "name": "title_bar_icon_margin_left",80 "name": "title_bar_icon_margin_left",
73- "value": "24vp"81+ "value": "16vp"
74 },82 },
75 {83 {
76 "name": "title_bar_icon_margin_right",84 "name": "title_bar_icon_margin_right",
77 "value": "24vp"85 "value": "24vp"
78 },86 },
87+ {
88+ "name": "title_bar_symbol_font_size",
89+ "value": "24vp"
90+ },
79 {91 {
80 "name": "title_bar_text_margin_left",92 "name": "title_bar_text_margin_left",
81 "value": "16vp"93 "value": "16vp"
@@ -96,6 +108,10 @@
96 "name": "index_dot_width",108 "name": "index_dot_width",
97 "value": "18vp"109 "value": "18vp"
98 },110 },
111+ {
112+ "name": "page_left_or_right_margin",
113+ "value": "16vp"
114+ },
99 {115 {
100 "name": "progress_stroke_width",116 "name": "progress_stroke_width",
101 "value": "18vp"117 "value": "18vp"
@@ -124,7 +140,14 @@
124 "name": "progress_logo_other_text_margin_top",140 "name": "progress_logo_other_text_margin_top",
125 "value": "16vp"141 "value": "16vp"
126 },142 },
127- 143+ {
144+ "name": "setting_border_radius",
145+ "value": "16vp"
146+ },
147+ {
148+ "name": "setting_options_height",
149+ "value": "56vp"
150+ },
128 {151 {
129 "name": "setting_options_margin_top",152 "name": "setting_options_margin_top",
130 "value": "8vp"153 "value": "8vp"
@@ -80,6 +80,19 @@ class RetryUpgradeAction implements DialogOperator {
80 80 
81const retryUpgradeAction: DialogOperator = new RetryUpgradeAction();81const retryUpgradeAction: DialogOperator = new RetryUpgradeAction();
82 82 
83+/**
84+ * 更新包变更后重新搜包动作
85+ */
86+class VersionInfoChangedAction implements DialogOperator {
87+ onConfirm(): void {
88+ RouterUtils.singletonHomePage();
89+ }
90+ onCancel(): void {
91+ }
92+}
93+ 
94+const versionInfoChangedAction: DialogOperator = new VersionInfoChangedAction();
95+ 
83/**96/**
84 * 弹框辅助者97 * 弹框辅助者
85 *98 *
@@ -143,7 +156,18 @@ export namespace DialogHelper {
143 * @param operator 回调156 * @param operator 回调
144 */157 */
145 export function displayDownloadFailDialog(): void {158 export function displayDownloadFailDialog(): void {
146- defaultNoTitleDialog($r('app.string.download_fail'), retryCheckAction);159+ showDialog($r('app.string.download_status_download_failed'), $r('app.string.download_fail'),
160+ $r('app.string.ok'), retryCheckAction);
161+ }
162+ 
163+ /**
164+ * 更新包变更弹框
165+ *
166+ * @param operator 回调
167+ */
168+ export function displayVersionInfoChangedDialog(): void {
169+ showDialog($r('app.string.package_changed_title'), $r('app.string.package_changed_content'),
170+ $r('app.string.ok'), versionInfoChangedAction);
147 }171 }
148 172 
149 /**173 /**
@@ -151,6 +175,39 @@ export namespace DialogHelper {
151 *175 *
152 * @param operator 回调176 * @param operator 回调
153 */177 */
178+ export function displayCancelDownloadDialog(): void {
179+ AlertDialog.show(
180+ {
181+ title: $r('app.string.cancel_download_confirm_title'),
182+ message: $r('app.string.cancel_download_confirm_message'),
183+ primaryButton: {
184+ value: $r('app.string.continue_download'),
185+ action: () => {
186+ logInfo('Continue download button clicked');
187+ },
188+ backgroundColor: $r('sys.float.ohos_id_corner_radius_button')
189+ },
190+ secondaryButton: {
191+ value: $r('app.string.cancel_download'),
192+ action: () => {
193+ logInfo('Cancel download button clicked');
194+ OtaUpdateManager.getInstance().cancel();
195+ },
196+ backgroundColor: $r('sys.float.ohos_id_corner_radius_button')
197+ },
198+ cancel: () => {
199+ logInfo('Closed callbacks');
200+ },
201+ alignment: DeviceUtils.getDialogLocation(),
202+ offset: ({
203+ dx: '0vp',
204+ dy: DeviceUtils.getDialogOffsetY()
205+ }),
206+ autoCancel: false
207+ }
208+ )
209+ }
210+ 
154 export function displayNoNetworkDialog(): void {211 export function displayNoNetworkDialog(): void {
155 defaultKnowDialog($r('app.string.net_error_title'), $r('app.string.net_error_content'));212 defaultKnowDialog($r('app.string.net_error_title'), $r('app.string.net_error_content'));
156 }213 }
@@ -201,7 +258,7 @@ export namespace DialogHelper {
201 * @param message 内容258 * @param message 内容
202 * @param operator 回调259 * @param operator 回调
203 */260 */
204- function defaultKnowDialog(title: string | Resource, message: string | Resource, operator ?: DialogOperator): void {261+ function defaultKnowDialog(title: ResourceStr, message: ResourceStr, operator ?: DialogOperator): void {
205 showDialog(title, message, $r('app.string.button_know'), operator);262 showDialog(title, message, $r('app.string.button_know'), operator);
206 }263 }
207 264 
@@ -211,8 +268,8 @@ export namespace DialogHelper {
211 * @param message 内容268 * @param message 内容
212 * @param operator 回调269 * @param operator 回调
213 */270 */
214- function defaultNoTitleDialog(message: string | Resource, operator ?: DialogOperator): void {271+ function defaultNoTitleDialog(message: ResourceStr, operator ?: DialogOperator): void {
215- showDialog(null, message, $r('app.string.button_know'), operator);272+ showDialog(undefined, message, $r('app.string.button_know'), operator);
216 }273 }
217 274 
218 /**275 /**
@@ -223,7 +280,7 @@ export namespace DialogHelper {
223 * @param confirmText 确认按钮显示内容280 * @param confirmText 确认按钮显示内容
224 * @param operator 回调281 * @param operator 回调
225 */282 */
226- function showDialog(title: string | Resource | null, message: string | Resource, confirmText?: string | Resource,283+ function showDialog(title: ResourceStr | undefined, message: ResourceStr, confirmText: ResourceStr,
227 operator ?: DialogOperator): void {284 operator ?: DialogOperator): void {
228 AlertDialog.show(285 AlertDialog.show(
229 {286 {
@@ -263,4 +320,4 @@ export namespace DialogHelper {
263 function logInfo(message: string): void {320 function logInfo(message: string): void {
264 LogUtils.info('DialogHelper', message);321 LogUtils.info('DialogHelper', message);
265 }322 }
266-}323+}
@@ -81,6 +81,9 @@ class RetryCheckAction implements DialogOperator {
81};81};
82 82 
83 export const retryCheckAction: DialogOperator = new RetryCheckAction();83 export const retryCheckAction: DialogOperator = new RetryCheckAction();
84+const DOWNLOAD_NO_NETWORK_DIALOG_DELAY_MS = 10 * 1000;
85+let downloadNoNetworkDialogTimer: number | undefined = undefined;
86+const DOWNLOAD_NO_NETWORK_DIALOG_VISIBLE_KEY = 'downloadNoNetworkDialogVisible';
84 87 
85/**88/**
86 * 弹框工具类89 * 弹框工具类
@@ -105,11 +108,42 @@ export class DialogUtils {
105 *108 *
106 * @param context 上下文109 * @param context 上下文
107 */110 */
108- @foregroundCheck()111+ // 新增延迟10s后弹出功能可能不在前台,所以注释装饰器
109 static showDownloadNoNetworkDialog(context: common.Context, otaStatus: OtaStatus,112 static showDownloadNoNetworkDialog(context: common.Context, otaStatus: OtaStatus,
110 eventId?: update.EventId): void {113 eventId?: update.EventId): void {
111 LogUtils.log('DialogUtils', 'showDownloadNoNetworkDialog');114 LogUtils.log('DialogUtils', 'showDownloadNoNetworkDialog');
112- DialogHelper.displayNoNetworkDialog();115+ if (globalThis.newVersionThis?.showNoNetworkDialog) {
116+ globalThis.newVersionThis.showNoNetworkDialog();
117+ }
118+ AppStorage.SetOrCreate(DOWNLOAD_NO_NETWORK_DIALOG_VISIBLE_KEY, true);
119+ }
120+ /**
121+ * 延时显示下载断网弹框
122+ *
123+ * @param context 上下文
124+ */
125+ static delayShowDownloadNoNetworkDialog(context: common.Context, otaStatus: OtaStatus,
126+ eventId?: update.EventId): void {
127+ DialogUtils.clearDownloadNoNetworkDialog();
128+ downloadNoNetworkDialogTimer = setTimeout(() => {
129+ downloadNoNetworkDialogTimer = undefined;
130+ DialogUtils.showDownloadNoNetworkDialog(context, otaStatus, eventId);
131+ }, DOWNLOAD_NO_NETWORK_DIALOG_DELAY_MS);
132+ }
133+ 
134+ /**
135+ * 清除下载断网弹框
136+ *
137+ */
138+ static clearDownloadNoNetworkDialog(): void {
139+ if (downloadNoNetworkDialogTimer !== undefined) {
140+ clearTimeout(downloadNoNetworkDialogTimer);
141+ downloadNoNetworkDialogTimer = undefined;
142+ }
143+ if (globalThis.newVersionThis?.hideNoNetworkDialog) {
144+ globalThis.newVersionThis.hideNoNetworkDialog();
145+ }
146+ AppStorage.SetOrCreate(DOWNLOAD_NO_NETWORK_DIALOG_VISIBLE_KEY, false);
113 }147 }
114 148 
115 /**149 /**
@@ -134,6 +168,16 @@ export class DialogUtils {
134 DialogHelper.displayDownloadFailDialog();168 DialogHelper.displayDownloadFailDialog();
135 }169 }
136 170 
171+ /**
172+ * 更新包变更弹框
173+ *
174+ * @param context 上下文
175+ */
176+ static showVersionInfoChangedDialog(context: common.Context, otaStatus?: OtaStatus, eventId?: update.EventId): void {
177+ LogUtils.log('DialogUtils', 'showVersionInfoChangedDialog');
178+ DialogHelper.displayVersionInfoChangedDialog();
179+ }
180+ 
137 /**181 /**
138 * 安装空间不足弹框182 * 安装空间不足弹框
139 *183 *
@@ -35,6 +35,7 @@ import VersionUtils from '../util/VersionUtils';
35import { CommonUtils } from '@ohos/common/src/main/ets/util/CommonUtils';35import { CommonUtils } from '@ohos/common/src/main/ets/util/CommonUtils';
36import { UpgradeAdapter } from '../UpgradeAdapter';36import { UpgradeAdapter } from '../UpgradeAdapter';
37import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';37import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';
38+import { SettingsSyncUtils } from '../util/SettingsSyncUtils';
38 39 
39/**40/**
40 * 升级接口管理类41 * 升级接口管理类
@@ -145,12 +146,15 @@ export class OtaUpdateManager {
145 async checkNewVersion(): Promise<UpgradeData<update.CheckResult>> {146 async checkNewVersion(): Promise<UpgradeData<update.CheckResult>> {
146 return new Promise((resolve, reject) => {147 return new Promise((resolve, reject) => {
147 this.updateManager.checkNewVersion().then((result: UpgradeData<update.CheckResult>) => {148 this.updateManager.checkNewVersion().then((result: UpgradeData<update.CheckResult>) => {
148- if (result?.callResult === UpgradeCallResult.OK) {149+ if (result?.data) {
149- globalThis.cachedNewVersionInfo = result?.data?.newVersionInfo;150+ const hasNewVersion: boolean = Boolean(result.data.isExistNewVersion);
150- resolve(result);151+ globalThis.cachedNewVersionInfo = hasNewVersion ? result.data.newVersionInfo : undefined;
151- } else {152+ SettingsSyncUtils.syncCheckResult(hasNewVersion, globalThis.abilityContext);
152- resolve(result);153+ } else if (result?.callResult === UpgradeCallResult.OK) {
154+ globalThis.cachedNewVersionInfo = undefined;
155+ SettingsSyncUtils.syncReset(globalThis.abilityContext);
153 }156 }
157+ resolve(result);
154 });158 });
155 });159 });
156 }160 }
@@ -222,6 +226,7 @@ export class OtaUpdateManager {
222 async cancel(): Promise<void> {226 async cancel(): Promise<void> {
223 this.setUpdateState(UpdateState.CHECK_SUCCESS);227 this.setUpdateState(UpdateState.CHECK_SUCCESS);
224 this.setDownloadProgress(0);228 this.setDownloadProgress(0);
229+ SettingsSyncUtils.syncCancel(globalThis.abilityContext);
225 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();230 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
226 this.updateManager.cancel();231 this.updateManager.cancel();
227 }232 }
@@ -235,6 +240,25 @@ export class OtaUpdateManager {
235 return this.updateManager.getCurrentVersionInfo();240 return this.updateManager.getCurrentVersionInfo();
236 }241 }
237 242 
243+ /**
244+ * 获取升级策略
245+ *
246+ * @returns resolve 升级策略/reject 错误信息
247+ */
248+ async getUpdatePolicy(): Promise<update.UpgradePolicy> {
249+ return this.updateManager.getUpdatePolicy();
250+ }
251+ 
252+ /**
253+ * 设置升级策略
254+ *
255+ * @param policy 升级策略
256+ * @returns resolve 设置结果/reject 错误信息
257+ */
258+ async setUpdatePolicy(policy: update.UpgradePolicy): Promise<void> {
259+ await this.updateManager.setUpdatePolicy(policy);
260+ }
261+ 
238 /**262 /**
239 * 取升级状态缓存数据263 * 取升级状态缓存数据
240 *264 *
@@ -295,6 +319,12 @@ export class OtaUpdateManager {
295 this.stateObj = StateManager.createInstance(otaStatus);319 this.stateObj = StateManager.createInstance(otaStatus);
296 }320 }
297 this.stateObj.refresh(otaStatus);321 this.stateObj.refresh(otaStatus);
322+ if (otaStatus.status === UpdateState.DOWNLOADING || otaStatus.status === UpdateState.DOWNLOAD_PAUSE) {
323+ let currentProgress: number = this.getDownloadProgress() ?? 0;
324+ if (currentProgress > this.stateObj.percent) {
325+ this.stateObj.percent = currentProgress;
326+ }
327+ }
298 this.lastStatus = otaStatus.status;328 this.lastStatus = otaStatus.status;
299 this.setUpdateState(this.stateObj.state);329 this.setUpdateState(this.stateObj.state);
300 this.setDownloadProgress(this.stateObj.percent);330 this.setDownloadProgress(this.stateObj.percent);
@@ -318,6 +348,12 @@ export class OtaUpdateManager {
318 private async handleMessage(message: Message): Promise<void> {348 private async handleMessage(message: Message): Promise<void> {
319 let eventInfo: CustomEventInfo = message.eventInfo as CustomEventInfo;349 let eventInfo: CustomEventInfo = message.eventInfo as CustomEventInfo;
320 let otaStatus: OtaStatus = this.getFormattedOtaStatus(eventInfo);350 let otaStatus: OtaStatus = this.getFormattedOtaStatus(eventInfo);
351+ const previousEndReason = this.stateObj?.otaStatus?.endReason;
352+ if (eventInfo.eventId === update.EventId.EVENT_DOWNLOAD_FAIL && !otaStatus.endReason &&
353+ Number(previousEndReason) === ErrorCode.VERIFY_PACKAGE_FAIL) {
354+ this.log('Ignore empty duplicate download failure after verify failure.');
355+ return;
356+ }
321 if (this.isTerminalState(otaStatus)) {357 if (this.isTerminalState(otaStatus)) {
322 globalThis.lastVersionName = await VersionUtils.obtainNewVersionName(eventInfo?.taskBody);358 globalThis.lastVersionName = await VersionUtils.obtainNewVersionName(eventInfo?.taskBody);
323 }359 }
@@ -334,6 +370,7 @@ export class OtaUpdateManager {
334 LogUtils.warn('UpdateManager', 'notifyUpdateStatus is repeating, abandon.');370 LogUtils.warn('UpdateManager', 'notifyUpdateStatus is repeating, abandon.');
335 return;371 return;
336 }372 }
373+ SettingsSyncUtils.syncFromOtaStatus(otaStatus, context);
337 if (!globalThis.cachedNewVersionInfo && !this.isTerminalState(otaStatus)) {374 if (!globalThis.cachedNewVersionInfo && !this.isTerminalState(otaStatus)) {
338 await this.getNewVersion();375 await this.getNewVersion();
339 }376 }
@@ -454,8 +491,12 @@ export class OtaUpdateManager {
454 status = UpdateState.INIT;491 status = UpdateState.INIT;
455 break;492 break;
456 case update.EventId.EVENT_DOWNLOAD_START:493 case update.EventId.EVENT_DOWNLOAD_START:
494+ case update.EventId.EVENT_DOWNLOAD_RESUME:
457 status = UpdateState.DOWNLOADING;495 status = UpdateState.DOWNLOADING;
458 break;496 break;
497+ case update.EventId.EVENT_DOWNLOAD_PAUSE:
498+ status = UpdateState.DOWNLOAD_PAUSE;
499+ break;
459 case update.EventId.EVENT_DOWNLOAD_SUCCESS:500 case update.EventId.EVENT_DOWNLOAD_SUCCESS:
460 status = UpdateState.DOWNLOAD_SUCCESS;501 status = UpdateState.DOWNLOAD_SUCCESS;
461 break;502 break;
@@ -473,4 +514,4 @@ export class OtaUpdateManager {
473 }514 }
474 return status;515 return status;
475 }516 }
476-}517+}
@@ -14,6 +14,7 @@
14 */14 */
15 15 
16import type common from '@ohos.app.ability.common';16import type common from '@ohos.app.ability.common';
17+import Settings from '@ohos.settings';
17import update from '@ohos.update';18import update from '@ohos.update';
18import { ErrorCode, OtaStatus, UpdateState } from '@ohos/common/src/main/ets/const/update_const';19import { ErrorCode, OtaStatus, UpdateState } from '@ohos/common/src/main/ets/const/update_const';
19import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';20import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
@@ -22,7 +23,28 @@ import { OtaUpdateManager } from '../manager/OtaUpdateManager';
22import VersionUtils from '../util/VersionUtils';23import VersionUtils from '../util/VersionUtils';
23import ToastUtils from '../util/ToastUtils';24import ToastUtils from '../util/ToastUtils';
24import { UpgradeAdapter } from '../UpgradeAdapter';25import { UpgradeAdapter } from '../UpgradeAdapter';
25-import { NotificationManager } from '../notify/NotificationManager';26+import { SceneBoardDialogManager, SceneBoardDialogType } from '../notify/SceneBoardDialogManager';
27+ 
28+const SETTINGS_DEVICE_PROVISIONED = 'device_provisioned';
29+const SETTINGS_USER_SETUP_COMPLETE = 'user_setup_complete';
30+const SETTINGS_IS_OTA_FINISHED = 'is_ota_finished';
31+ 
32+function isInOobe(context: common.Context): boolean {
33+ try {
34+ const deviceProvisioned = Settings.getValueSync(context, SETTINGS_DEVICE_PROVISIONED, '0',
35+ Settings.domainName.DEVICE_SHARED);
36+ const userSetupComplete = Settings.getValueSync(context, SETTINGS_USER_SETUP_COMPLETE, '0',
37+ Settings.domainName.USER_SECURITY);
38+ const isOtaFinished = Settings.getValueSync(context, SETTINGS_IS_OTA_FINISHED, '1',
39+ Settings.domainName.USER_SECURITY);
40+ const result = !(deviceProvisioned === '1' && userSetupComplete === '1' && isOtaFinished === '1');
41+ LogUtils.info('StateManager', `isInOobe=${result}`);
42+ return result;
43+ } catch (err) {
44+ LogUtils.error('StateManager', `isInOobe query failed: ${JSON.stringify(err)}`);
45+ return true;
46+ }
47+}
26 48 
27/**49/**
28 * 状态工厂50 * 状态工厂
@@ -263,8 +285,12 @@ export class Init extends BaseState {
263 this.actionSet.push(UpdateAction.CHECK_NEW_VERSION);285 this.actionSet.push(UpdateAction.CHECK_NEW_VERSION);
264 }286 }
265 287 
266- async notify(): Promise<void> {288+ async notify(context?: common.Context, eventId?: update.EventId): Promise<void> {
267 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();289 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
290+ // 升级包失效时清空通知栏
291+ if (context && eventId == update.EventId.EVENT_TASK_CANCEL) {
292+ DialogUtils.showVersionInfoChangedDialog(context, this.otaStatus, eventId);
293+ }
268 }294 }
269}295}
270 296 
@@ -298,6 +324,25 @@ export class CheckSuccess extends BaseState {
298 DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId);324 DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId);
299 break;325 break;
300 }326 }
327+ return;
328+ }
329+ const isBackgroundReminder = globalThis.AbilityStatus !== 'ON_FOREGROUND' &&
330+ (eventId === update.EventId.EVENT_DOWNLOAD_WAIT || eventId === update.EventId.EVENT_TASK_RECEIVE);
331+ if (isBackgroundReminder && isInOobe(context)) {
332+ LogUtils.info('StateManager', 'OOBE is not complete, skip download reminder.');
333+ return;
334+ }
335+ if (eventId === update.EventId.EVENT_DOWNLOAD_WAIT && globalThis.AbilityStatus !== 'ON_FOREGROUND') {
336+ LogUtils.info('StateManager', 'Check success in background, show download reminder dialog.');
337+ await SceneBoardDialogManager.showDialog(context, {
338+ dialogType: SceneBoardDialogType.DOWNLOAD_REMINDER
339+ });
340+ return;
341+ }
342+ if (eventId === update.EventId.EVENT_TASK_RECEIVE && globalThis.AbilityStatus !== 'ON_FOREGROUND') {
343+ LogUtils.info('StateManager', 'Download reminder reached notification threshold, show notification.');
344+ const versionName = await VersionUtils.obtainNewVersionName(globalThis.cachedNewVersionInfo);
345+ await UpgradeAdapter.getInstance().getNotifyInstance()?.showDownloadReminder(versionName, context);
301 }346 }
302 }347 }
303}348}
@@ -318,7 +363,10 @@ export class Downloading extends BaseState {
318 this.buttonClickAction = UpdateAction.CANCEL;363 this.buttonClickAction = UpdateAction.CANCEL;
319 }364 }
320 365 
321- async notify(context: common.Context): Promise<void> {366+ async notify(context: common.Context, eventId?: update.EventId): Promise<void> {
367+ if (eventId == update.EventId.EVENT_DOWNLOAD_RESUME) {
368+ DialogUtils.clearDownloadNoNetworkDialog();
369+ }
322 if (this.percent == 100) {370 if (this.percent == 100) {
323 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();371 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
324 return;372 return;
@@ -376,30 +424,51 @@ export class DownloadPause extends BaseState {
376 }424 }
377 425 
378 async notify(context: common.Context, eventId?: update.EventId): Promise<void> {426 async notify(context: common.Context, eventId?: update.EventId): Promise<void> {
379- if (!VersionUtils.isInNewVersionPage()) {427+ const otaStatus = this.otaStatus;
428+ if (!otaStatus) {
380 return;429 return;
381 }430 }
382- if (this.otaStatus?.endReason) {431+ const isDownloadPauseEvent = eventId == update.EventId.EVENT_DOWNLOAD_PAUSE;
383- await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();432+ if (isDownloadPauseEvent) {
384- switch (Number(this.otaStatus?.endReason)) {433+ if (VersionUtils.isInNewVersionPage()) {
385- case ErrorCode.NETWORK_ERROR:434+ await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
386- if (eventId == update.EventId.EVENT_DOWNLOAD_PAUSE) {435+ } else {
387- DialogUtils.showDownloadNoNetworkDialog(context, this.otaStatus, eventId);436+ let versionName = await VersionUtils.obtainNewVersionName(globalThis.cachedNewVersionInfo);
388- } else {437+ await UpgradeAdapter.getInstance().getNotifyInstance()?.showDownloadPaused(versionName, this.percent, context);
389- let message = await context.resourceManager.getString($r('app.string.network_err_toast').id);
390- ToastUtils.showToast(message);
391- }
392- break;
393- case ErrorCode.NETWORK_NOT_ALLOW:
394- if (eventId == update.EventId.EVENT_DOWNLOAD_PAUSE) {
395- DialogUtils.showDownloadNoNetworkDialog(context, this.otaStatus, eventId);
396- }
397- break;
398- default:
399- DialogUtils.showDownloadFailDialog(context, this.otaStatus, eventId);
400- break;
401 }438 }
402 }439 }
440+ if (!VersionUtils.isInNewVersionPage()) {
441+ if (globalThis.AbilityStatus !== 'ON_FOREGROUND' && isDownloadPauseEvent) {
442+ DialogUtils.delayShowDownloadNoNetworkDialog(context, otaStatus, eventId);
443+ }
444+ return;
445+ }
446+ if (!otaStatus.endReason) {
447+ if (isDownloadPauseEvent) {
448+ DialogUtils.delayShowDownloadNoNetworkDialog(context, otaStatus, eventId);
449+ }
450+ return;
451+ }
452+ switch (Number(otaStatus.endReason)) {
453+ case ErrorCode.NETWORK_ERROR:
454+ if (isDownloadPauseEvent) {
455+ DialogUtils.delayShowDownloadNoNetworkDialog(context, otaStatus, eventId);
456+ } else {
457+ await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
458+ let message = await context.resourceManager.getString($r('app.string.network_err_toast').id);
459+ ToastUtils.showToast(message);
460+ }
461+ break;
462+ case ErrorCode.NETWORK_NOT_ALLOW:
463+ if (isDownloadPauseEvent) {
464+ DialogUtils.delayShowDownloadNoNetworkDialog(context, otaStatus, eventId);
465+ }
466+ break;
467+ default:
468+ await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
469+ DialogUtils.showDownloadFailDialog(context, otaStatus, eventId);
470+ break;
471+ }
403 }472 }
404}473}
405 474 
@@ -424,6 +493,10 @@ export class DownloadFailed extends BaseState {
424 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();493 await UpgradeAdapter.getInstance().getNotifyInstance()?.cancelAll();
425 switch (Number(this.otaStatus?.endReason)) {494 switch (Number(this.otaStatus?.endReason)) {
426 case ErrorCode.VERIFY_PACKAGE_FAIL:495 case ErrorCode.VERIFY_PACKAGE_FAIL:
496+ await UpgradeAdapter.getInstance().getNotifyInstance()?.showVerifyFailed(context);
497+ if (globalThis.AbilityStatus !== 'ON_FOREGROUND') {
498+ break;
499+ }
427 DialogUtils.showVerifyFailDialog(context, this.otaStatus, eventId);500 DialogUtils.showVerifyFailDialog(context, this.otaStatus, eventId);
428 break;501 break;
429 default:502 default:
@@ -454,17 +527,20 @@ export class DownloadSuccess extends BaseState {
454 async notify(context: common.Context, eventId?: update.EventId): Promise<void> {527 async notify(context: common.Context, eventId?: update.EventId): Promise<void> {
455 let isABInstall = await VersionUtils.isABInstall();528 let isABInstall = await VersionUtils.isABInstall();
456 LogUtils.info('StateManager:', 'notify ab flag ' + isABInstall + ',eventId:' + eventId);529 LogUtils.info('StateManager:', 'notify ab flag ' + isABInstall + ',eventId:' + eventId);
457- if (eventId == update.EventId.EVENT_DOWNLOAD_SUCCESS && isABInstall) {530+ if (eventId == update.EventId.EVENT_DOWNLOAD_SUCCESS) {
458- OtaUpdateManager.getInstance().upgrade(update.Order.INSTALL);531+ if (isABInstall) {
532+ OtaUpdateManager.getInstance().upgrade(update.Order.INSTALL);
533+ return;
534+ }
535+ if (VersionUtils.isInNewVersionPage()) {
536+ AppStorage.Set('isClickInstall', 1);
537+ }
459 return;538 return;
460 }539 }
461 540 
462 if (eventId == update.EventId.EVENT_UPGRADE_WAIT && !isABInstall) {541 if (eventId == update.EventId.EVENT_UPGRADE_WAIT && !isABInstall) {
463- LogUtils.info('StateManager', 'manual download complete to count down');542+ LogUtils.info('StateManager', 'download complete, show night upgrade notice');
464- if (!VersionUtils.isInNewVersionPage()) {543+ await UpgradeAdapter.getInstance().getNotifyInstance()?.showNightUpgradeNotice(context);
465- NotificationManager.startToNewVersion(context);
466- }
467- AppStorage.Set('isClickInstall', 1);
468 return;544 return;
469 }545 }
470 546 
@@ -541,13 +617,12 @@ export class InstallSuccess extends BaseState {
541 this.buttonClickAction = UpdateAction.REBOOT;617 this.buttonClickAction = UpdateAction.REBOOT;
542 }618 }
543 619 
544- async notify(context: common.Context, eventId?: update.EventId): Promise<void> {620+ async notify(_context: common.Context, eventId?: update.EventId): Promise<void> {
545 if (eventId == update.EventId.EVENT_APPLY_WAIT) {621 if (eventId == update.EventId.EVENT_APPLY_WAIT) {
546- LogUtils.info('StateManager', 'ab install complete to count down');622+ LogUtils.info('StateManager', 'AB install complete, handle countdown only in new version page.');
547- if (!VersionUtils.isInNewVersionPage()) {623+ if (VersionUtils.isInNewVersionPage()) {
548- NotificationManager.startToNewVersion(context);624+ AppStorage.Set('isClickInstall', 1);
549 }625 }
550- AppStorage.Set('isClickInstall', 1);
551 }626 }
552 }627 }
553}628}
@@ -652,4 +727,4 @@ export class UpgradeFailed extends InstallFailed {
652 super();727 super();
653 this.state = UpdateState.UPGRADE_FAILED;728 this.state = UpdateState.UPGRADE_FAILED;
654 }729 }
655-}730+}
@@ -16,7 +16,14 @@
16import notification from '@ohos.notificationManager';16import notification from '@ohos.notificationManager';
17import wantAgent from '@ohos.app.ability.wantAgent';17import wantAgent from '@ohos.app.ability.wantAgent';
18import type common from '@ohos.app.ability.common';18import type common from '@ohos.app.ability.common';
19-import { BusinessError, Action, PACKAGE_NAME } from '@ohos/common/src/main/ets/const/update_const';19+import {
20+ BusinessError,
21+ Action,
22+ PACKAGE_NAME,
23+ NIGHT_UPGRADE_START,
24+ NIGHT_UPGRADE_END
25+} from '@ohos/common/src/main/ets/const/update_const';
26+import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';
20import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';27import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
21import { INotify } from '@ohos/common/src/main/ets/manager/UpgradeInterface';28import { INotify } from '@ohos/common/src/main/ets/manager/UpgradeInterface';
22 29 
@@ -26,6 +33,35 @@ import { INotify } from '@ohos/common/src/main/ets/manager/UpgradeInterface';
26 * @since 2022-06-0533 * @since 2022-06-05
27 */34 */
28export class NotificationHelper implements INotify {35export class NotificationHelper implements INotify {
36+ /**
37+ * 下载提醒通知
38+ *
39+ * @param versionName 版本号
40+ * @param context 上下文
41+ */
42+ async showDownloadReminder(versionName: string, context: common.Context): Promise<void> {
43+ let request: notification.NotificationRequest = {
44+ content: {
45+ notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
46+ normal: {
47+ title: await context.resourceManager.getString($r('app.string.software_update').id),
48+ text: await context.resourceManager.getString($r('app.string.desktop_download_dialog_content').id),
49+ additionalText: versionName
50+ }
51+ },
52+ wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),
53+ id: 5,
54+ isAlertOnce: true,
55+ notificationSlotType: notification.SlotType.SERVICE_INFORMATION,
56+ deliveryTime: new Date().getTime()
57+ };
58+ await notification.publish(request).then(() => {
59+ this.logInfo('showDownloadReminder publish promise success.');
60+ }).catch((err: BusinessError) => {
61+ this.logError('showDownloadReminder publish promise failed because ' + JSON.stringify(err));
62+ });
63+ }
64+ 
29 /**65 /**
30 * 跳转信息--跳转到搜包页面66 * 跳转信息--跳转到搜包页面
31 */67 */
@@ -54,6 +90,34 @@ export class NotificationHelper implements INotify {
54 wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],90 wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],
55 };91 };
56 92 
93+ /**
94+ * 跳转信息--夜间升级直接安装
95+ */
96+ private installWantAgentInfo: wantAgent.WantAgentInfo = {
97+ wants: [{
98+ bundleName: PACKAGE_NAME,
99+ abilityName: 'ServiceExtAbility',
100+ action: Action.NOTIFICATION_INSTALL
101+ }],
102+ actionType: wantAgent.OperationType.START_ABILITY,
103+ requestCode: 0,
104+ wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],
105+ };
106+ 
107+ /**
108+ * 跳转信息--夜间升级设置页
109+ */
110+ private settingsWantAgentInfo: wantAgent.WantAgentInfo = {
111+ wants: [{
112+ bundleName: PACKAGE_NAME,
113+ abilityName: 'ServiceExtAbility',
114+ action: Action.NOTIFICATION_SETTINGS
115+ }],
116+ actionType: wantAgent.OperationType.START_ABILITY,
117+ requestCode: 0,
118+ wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],
119+ };
120+ 
57 /**121 /**
58 * 下载进度通知122 * 下载进度通知
59 *123 *
@@ -63,6 +127,9 @@ export class NotificationHelper implements INotify {
63 */127 */
64 async showDownloading(version: string, progress: number, context: common.Context): Promise<void> {128 async showDownloading(version: string, progress: number, context: common.Context): Promise<void> {
65 let templateName: string = 'downloadTemplate';129 let templateName: string = 'downloadTemplate';
130+ let downloadText: string = await context.resourceManager.getString($r('app.string.software_download_progress').id);
131+ let downloadTitle: string = downloadText + ':' + progress + '%';
132+ this.logInfo('showDownloading start, version=' + version + ', progress=' + progress);
66 if (!globalThis.isSupportTemplate) {133 if (!globalThis.isSupportTemplate) {
67 globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch((err: BusinessError) => {134 globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch((err: BusinessError) => {
68 this.logError('showDownloading isSupportTemplate failed because ' + JSON.stringify(err));135 this.logError('showDownloading isSupportTemplate failed because ' + JSON.stringify(err));
@@ -77,14 +144,14 @@ export class NotificationHelper implements INotify {
77 content: {144 content: {
78 notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,145 notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
79 normal: {146 normal: {
80- title: await context.resourceManager.getString($r('app.string.software_update').id),147+ title: downloadTitle,
81- text: await context.resourceManager.getString($r('app.string.software_download_progress').id)148+ text: version
82 },149 },
83 },150 },
84 template: {151 template: {
85 name: 'downloadTemplate',152 name: 'downloadTemplate',
86 data: {153 data: {
87- title: await context.resourceManager.getString($r('app.string.software_download_progress').id),154+ title: downloadTitle,
88 fileName: version,155 fileName: version,
89 progressValue: progress156 progressValue: progress
90 }157 }
@@ -92,6 +159,7 @@ export class NotificationHelper implements INotify {
92 wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),159 wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),
93 id: 5,160 id: 5,
94 label: '111',161 label: '111',
162+ isAlertOnce: true,
95 notificationSlotType: notification.SlotType.SERVICE_INFORMATION,163 notificationSlotType: notification.SlotType.SERVICE_INFORMATION,
96 deliveryTime: new Date().getTime()164 deliveryTime: new Date().getTime()
97 }165 }
@@ -102,6 +170,65 @@ export class NotificationHelper implements INotify {
102 });170 });
103 }171 }
104 172 
173+ /**
174+ * 下载暂停通知
175+ *
176+ * @param version 版本号
177+ * @param progress 进度
178+ * @param context 上下文
179+ */
180+ async showDownloadPaused(version: string, progress: number, context: common.Context): Promise<void> {
181+ let pausedText: string =
182+ await context.resourceManager.getString($r('app.string.download_status_download_pause').id);
183+ let request: notification.NotificationRequest = {
184+ content: {
185+ notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
186+ normal: {
187+ title: pausedText,
188+ text: version
189+ }
190+ },
191+ wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),
192+ id: 5,
193+ label: '111',
194+ isAlertOnce: true,
195+ notificationSlotType: notification.SlotType.SERVICE_INFORMATION,
196+ deliveryTime: new Date().getTime()
197+ };
198+ await notification.publish(request).then(() => {
199+ this.logInfo('showDownloadPaused publish promise success.');
200+ }).catch((err: BusinessError) => {
201+ this.logError('showDownloadPaused publish promise failed because ' + JSON.stringify(err));
202+ });
203+ }
204+ 
205+ /**
206+ * 升级包校验失败通知
207+ *
208+ * @param context 上下文
209+ */
210+ async showVerifyFailed(context: common.Context): Promise<void> {
211+ let request: notification.NotificationRequest = {
212+ content: {
213+ notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
214+ normal: {
215+ title: await context.resourceManager.getString($r('app.string.download_verify_fail_notification_title').id),
216+ text: await context.resourceManager.getString($r('app.string.download_verify_fail_notification_content').id)
217+ }
218+ },
219+ wantAgent: await wantAgent.getWantAgent(this.checkWantAgentInfo),
220+ id: 5,
221+ isAlertOnce: true,
222+ notificationSlotType: notification.SlotType.SERVICE_INFORMATION,
223+ deliveryTime: new Date().getTime()
224+ };
225+ await notification.publish(request).then(() => {
226+ this.logInfo('showVerifyFailed publish promise success.');
227+ }).catch((err: BusinessError) => {
228+ this.logError('showVerifyFailed publish promise failed because ' + JSON.stringify(err));
229+ });
230+ }
231+ 
105 /**232 /**
106 * 下载进度通知233 * 下载进度通知
107 *234 *
@@ -111,6 +238,8 @@ export class NotificationHelper implements INotify {
111 */238 */
112 async showInstalling(version: string, progress: number, context: common.Context): Promise<void> {239 async showInstalling(version: string, progress: number, context: common.Context): Promise<void> {
113 let templateName: string = 'installTemplate';240 let templateName: string = 'installTemplate';
241+ let installText: string = await context.resourceManager.getString($r('app.string.software_install_progress').id);
242+ let installProgressText: string = installText + ' ' + progress + '%';
114 if (!globalThis.isSupportTemplate) {243 if (!globalThis.isSupportTemplate) {
115 globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch((err: BusinessError) => {244 globalThis.isSupportTemplate = await notification.isSupportTemplate(templateName).catch((err: BusinessError) => {
116 this.logError('showInstalling isSupportTemplate failed because ' + JSON.stringify(err));245 this.logError('showInstalling isSupportTemplate failed because ' + JSON.stringify(err));
@@ -126,20 +255,21 @@ export class NotificationHelper implements INotify {
126 notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,255 notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
127 normal: {256 normal: {
128 title: await context.resourceManager.getString($r('app.string.software_update').id),257 title: await context.resourceManager.getString($r('app.string.software_update').id),
129- text: await context.resourceManager.getString($r('app.string.software_install_progress').id)258+ text: installProgressText
130 },259 },
131 },260 },
132 template: {261 template: {
133 name: 'installTemplate',262 name: 'installTemplate',
134 data: {263 data: {
135- title: await context.resourceManager.getString($r('app.string.software_install_progress').id),264+ title: installText,
136- fileName: version,265+ fileName: installProgressText,
137 progressValue: progress266 progressValue: progress
138 }267 }
139 },268 },
140 wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),269 wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),
141 id: 5,270 id: 5,
142 label: '111',271 label: '111',
272+ isAlertOnce: true,
143 notificationSlotType: notification.SlotType.SERVICE_INFORMATION,273 notificationSlotType: notification.SlotType.SERVICE_INFORMATION,
144 deliveryTime: new Date().getTime()274 deliveryTime: new Date().getTime()
145 }275 }
@@ -198,6 +328,46 @@ export class NotificationHelper implements INotify {
198 });328 });
199 }329 }
200 330 
331+ /**
332+ * 夜间升级设置通知
333+ *
334+ * @param context 实际上下文
335+ */
336+ async showNightUpgradeNotice(context: common.Context): Promise<void> {
337+ let description: string =
338+ await context.resourceManager.getString($r('app.string.setting_auto_update_description').id);
339+ let detailText: string = await context.resourceManager.getString($r('app.string.detail_info').id);
340+ let installText: string = await context.resourceManager.getString($r('app.string.btn_install_now').id);
341+ let request: notification.NotificationRequest = {
342+ content: {
343+ notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
344+ normal: {
345+ title: await context.resourceManager.getString($r('app.string.setting_auto_update_title').id),
346+ text: FormatUtils.formatStr(description,
347+ FormatUtils.getDateRangeFormat(NIGHT_UPGRADE_START, NIGHT_UPGRADE_END))
348+ }
349+ },
350+ wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo),
351+ actionButtons: [
352+ {
353+ title: detailText,
354+ wantAgent: await wantAgent.getWantAgent(this.downloadingWantAgentInfo)
355+ },
356+ {
357+ title: installText,
358+ wantAgent: await wantAgent.getWantAgent(this.installWantAgentInfo)
359+ }
360+ ],
361+ id: 6,
362+ notificationSlotType: notification.SlotType.SERVICE_INFORMATION
363+ };
364+ await notification.publish(request).then(() => {
365+ this.logInfo('showNightUpgradeNotice publish promise success.');
366+ }).catch((err: BusinessError) => {
367+ this.logError('showNightUpgradeNotice publish promise failed because ' + JSON.stringify(err));
368+ });
369+ }
370+ 
201 /**371 /**
202 * 取消所有通知372 * 取消所有通知
203 */373 */
@@ -251,4 +421,4 @@ export class NotificationHelper implements INotify {
251 logError(message: string): void {421 logError(message: string): void {
252 LogUtils.error('NotificationHelper', message);422 LogUtils.error('NotificationHelper', message);
253 }423 }
254-}424+}
@@ -14,7 +14,7 @@
14 */14 */
15 15 
16import type common from '@ohos.app.ability.common';16import type common from '@ohos.app.ability.common';
17-import { StartOptions, Want } from '@kit.AbilityKit';17+import { Want } from '@kit.AbilityKit';
18import {18import {
19 Action,19 Action,
20 PACKAGE_NAME,20 PACKAGE_NAME,
@@ -48,6 +48,12 @@ export class NotificationManager {
48 case Action.NOTIFICATION_DETAIL:48 case Action.NOTIFICATION_DETAIL:
49 await NotificationManager.handleDetailAction(context);49 await NotificationManager.handleDetailAction(context);
50 return true;50 return true;
51+ case Action.NOTIFICATION_INSTALL:
52+ NotificationManager.startAbility('pages/newVersion', context, true);
53+ return true;
54+ case Action.NOTIFICATION_SETTINGS:
55+ NotificationManager.handleSettingsAction(context);
56+ return true;
51 default:57 default:
52 return false;58 return false;
53 }59 }
@@ -60,11 +66,12 @@ export class NotificationManager {
60 66 
61 private static async handleDetailAction(context: common.Context): Promise<void> {67 private static async handleDetailAction(context: common.Context): Promise<void> {
62 LogUtils.log(TAG, 'handleDetailAction');68 LogUtils.log(TAG, 'handleDetailAction');
63- if (await RouterUtils.isCanToNewVersion()) {69+ NotificationManager.startAbility('pages/newVersion', context);
64- NotificationManager.startAbility('pages/newVersion', context);70+ }
65- } else {71+ 
66- NotificationManager.startAbility('pages/index', context);72+ private static handleSettingsAction(context: common.Context): void {
67- }73+ LogUtils.log(TAG, 'handleSettingsAction');
74+ NotificationManager.startAbility('pages/settingsView', context);
68 }75 }
69 76 
70 public static async startToNewVersion(context: common.Context): Promise<void> {77 public static async startToNewVersion(context: common.Context): Promise<void> {
@@ -73,16 +80,16 @@ export class NotificationManager {
73 }80 }
74 }81 }
75 82 
76- private static startAbility(uri: string, context: common.Context): void {83+ private static startAbility(uri: string, context: common.Context, autoInstall?: boolean): void {
77 let want: Want = {84 let want: Want = {
78 bundleName: PACKAGE_NAME,85 bundleName: PACKAGE_NAME,
79 abilityName: MAIN_ABILITY_NAME,86 abilityName: MAIN_ABILITY_NAME,
80 uri: uri87 uri: uri
81 };88 };
82- let options: StartOptions = {89+ // 新增直接直接安装逻辑
83- windowMode: 0,90+ if (autoInstall !== undefined) {
84- displayId: 291+ want.parameters = { autoInstall: autoInstall };
85- };92+ }
86- UpdateUtils.startAbility(context, want, options);93+ UpdateUtils.startAbility(context, want);
87 }94 }
88}95}
@@ -0,0 +1,140 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import type common from '@ohos.app.ability.common';
17+import type Want from '@ohos.app.ability.Want';
18+import rpc from '@ohos.rpc';
19+import { PACKAGE_NAME } from '@ohos/common/src/main/ets/const/update_const';
20+import type { BusinessError } from '@ohos/common/src/main/ets/const/update_const';
21+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
22+ 
23+const TAG = 'SceneBoardDialogMgr';
24+const SYSTEM_DIALOG_BUNDLE = 'com.ohos.sceneboard';
25+const SYSTEM_DIALOG_ABILITY = 'com.ohos.sceneboard.systemdialog';
26+const UPDATE_DIALOG_ABILITY = 'UpdateSystemDialogAbility';
27+const START_DIALOG_CMD = 1;
28+const REQUEST_PARAM_NUM = 3;
29+const SUCCESS_CODE = 0;
30+ 
31+type DialogConnectContext = common.UIAbilityContext | common.ServiceExtensionContext;
32+ 
33+export enum SceneBoardDialogType {
34+ DOWNLOAD_REMINDER = 'downloadReminder'
35+}
36+ 
37+export interface SceneBoardDialogOptions {
38+ dialogType: SceneBoardDialogType;
39+}
40+ 
41+/**
42+ * 使用SceneBoard的系统弹窗服务来显示 updateapp UI扩展弹窗
43+ */
44+export class SceneBoardDialogManager {
45+ private static requestingType?: SceneBoardDialogType;
46+ 
47+ static async showDialog(context: common.Context, options: SceneBoardDialogOptions): Promise<boolean> {
48+ const dialogContext = context as DialogConnectContext;
49+ if (!dialogContext?.connectServiceExtensionAbility) {
50+ LogUtils.warn(TAG, 'Context does not support connectServiceExtensionAbility.');
51+ return false;
52+ }
53+ if (SceneBoardDialogManager.requestingType !== undefined) {
54+ LogUtils.info(TAG, `Dialog request is in progress: ${SceneBoardDialogManager.requestingType}`);
55+ return false;
56+ }
57+ 
58+ SceneBoardDialogManager.requestingType = options.dialogType;
59+ const want: Want = {
60+ bundleName: SYSTEM_DIALOG_BUNDLE,
61+ abilityName: SYSTEM_DIALOG_ABILITY
62+ };
63+ 
64+ return new Promise<boolean>((resolve) => {
65+ let connectionId: number = -1;
66+ let settled: boolean = false;
67+ const finish = (success: boolean): void => {
68+ if (settled) {
69+ return;
70+ }
71+ settled = true;
72+ SceneBoardDialogManager.requestingType = undefined;
73+ if (connectionId >= 0) {
74+ try {
75+ dialogContext.disconnectServiceExtensionAbility(connectionId).catch((error: BusinessError) => {
76+ LogUtils.error(TAG, `Disconnect system dialog failed: ${JSON.stringify(error)}`);
77+ });
78+ } catch (error) {
79+ LogUtils.error(TAG, `Disconnect system dialog exception: ${JSON.stringify(error)}`);
80+ }
81+ }
82+ resolve(success);
83+ };
84+ 
85+ const connectOption: common.ConnectOptions = {
86+ onConnect(elementName, remote) {
87+ const data = rpc.MessageSequence.create();
88+ const reply = rpc.MessageSequence.create();
89+ const option = new rpc.MessageOption();
90+ const parameters: Record<string, Object> = {
91+ 'ability.want.params.uiExtensionType': 'sysDialog/common',
92+ 'sysDialogZOrder': 1,
93+ 'dialogType': options.dialogType
94+ };
95+ try {
96+ data.writeInt(REQUEST_PARAM_NUM);
97+ data.writeString('bundleName');
98+ data.writeString(PACKAGE_NAME);
99+ data.writeString('abilityName');
100+ data.writeString(UPDATE_DIALOG_ABILITY);
101+ data.writeString('parameters');
102+ data.writeString(JSON.stringify(parameters));
103+ remote.sendMessageRequest(START_DIALOG_CMD, data, reply, option).then(() => {
104+ const result = reply.readInt();
105+ LogUtils.info(TAG, `Show ${options.dialogType} reply=${result}`);
106+ finish(result === SUCCESS_CODE);
107+ }).catch((error: BusinessError) => {
108+ LogUtils.error(TAG, `Show ${options.dialogType} failed: ${JSON.stringify(error)}`);
109+ finish(false);
110+ }).finally(() => {
111+ data.reclaim();
112+ reply.reclaim();
113+ });
114+ } catch (error) {
115+ data.reclaim();
116+ reply.reclaim();
117+ LogUtils.error(TAG, `Build ${options.dialogType} request failed: ${JSON.stringify(error)}`);
118+ finish(false);
119+ }
120+ LogUtils.info(TAG, `Connected to system dialog: ${elementName?.bundleName}`);
121+ },
122+ onDisconnect(elementName) {
123+ LogUtils.info(TAG, `Disconnected from system dialog: ${elementName?.bundleName}`);
124+ },
125+ onFailed(code) {
126+ LogUtils.error(TAG, `Connect system dialog failed: ${code}`);
127+ finish(false);
128+ }
129+ };
130+ 
131+ try {
132+ connectionId = dialogContext.connectServiceExtensionAbility(want, connectOption) ?? -1;
133+ LogUtils.info(TAG, `Connect system dialog start, connectionId=${connectionId}`);
134+ } catch (error) {
135+ LogUtils.error(TAG, `Connect system dialog exception: ${JSON.stringify(error)}`);
136+ finish(false);
137+ }
138+ });
139+ }
140+}
@@ -0,0 +1,127 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import Settings from '@ohos.settings';
17+import type common from '@ohos.app.ability.common';
18+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
19+import type { OtaStatus } from '@ohos/common/src/main/ets/const/update_const';
20+import { UpdateState } from '@ohos/common/src/main/ets/const/update_const';
21+ 
22+const TAG = 'SettingsSyncUtils';
23+const HW_NEW_SYSTEM_UPDATE = 'hw_new_system_update';
24+const HW_NEW_SYSTEM_UPDATE_STATUS = 'hw_new_system_update_status';
25+ 
26+enum NewVersionViewStatus {
27+ VIEW_INIT = 0,
28+ VIEW_CHECK_SUCCESS,
29+ VIEW_DOWNLOADING,
30+ VIEW_DOWNLOAD_PAUSE,
31+ VIEW_DOWNLOAD_VERIFYING,
32+ VIEW_DOWNLOAD_SUCCESS,
33+ VIEW_UPGRADE_VERIFYING,
34+ VIEW_UPGRADE_REBOOTING,
35+ VIEW_UPGRADE_HOT_INSTALLING,
36+ VIEW_PRE_INSTALLING,
37+ VIEW_INSTALLING,
38+ VIEW_INSTALL_SUCCESS,
39+ VIEW_INSTALL_WAIT,
40+}
41+ 
42+interface SettingsUpdateView {
43+ badge: number;
44+ status: NewVersionViewStatus;
45+}
46+ 
47+export class SettingsSyncUtils {
48+ static syncCheckResult(hasNewVersion: boolean, context?: common.Context): void {
49+ if (hasNewVersion) {
50+ SettingsSyncUtils.writeView({ badge: 1, status: NewVersionViewStatus.VIEW_CHECK_SUCCESS }, context);
51+ return;
52+ }
53+ SettingsSyncUtils.syncReset(context);
54+ }
55+ 
56+ static syncCancel(context?: common.Context): void {
57+ SettingsSyncUtils.writeView({ badge: 1, status: NewVersionViewStatus.VIEW_CHECK_SUCCESS }, context);
58+ }
59+ 
60+ static syncReset(context?: common.Context): void {
61+ SettingsSyncUtils.writeView({ badge: 0, status: NewVersionViewStatus.VIEW_INIT }, context);
62+ }
63+ 
64+ static syncFromOtaStatus(otaStatus: OtaStatus, context?: common.Context): void {
65+ const view = SettingsSyncUtils.mapStatus(otaStatus?.status);
66+ if (!view) {
67+ return;
68+ }
69+ SettingsSyncUtils.writeView(view, context);
70+ }
71+ 
72+ private static mapStatus(status?: number): SettingsUpdateView | undefined {
73+ switch (status) {
74+ case UpdateState.INIT:
75+ return { badge: 0, status: NewVersionViewStatus.VIEW_INIT };
76+ case UpdateState.CHECK_SUCCESS:
77+ return { badge: 1, status: NewVersionViewStatus.VIEW_CHECK_SUCCESS };
78+ case UpdateState.DOWNLOADING:
79+ return { badge: 1, status: NewVersionViewStatus.VIEW_DOWNLOADING };
80+ case UpdateState.DOWNLOAD_PAUSE:
81+ return { badge: 1, status: NewVersionViewStatus.VIEW_DOWNLOAD_PAUSE };
82+ case UpdateState.DOWNLOAD_SUCCESS:
83+ return { badge: 1, status: NewVersionViewStatus.VIEW_DOWNLOAD_SUCCESS };
84+ case UpdateState.INSTALLING:
85+ case UpdateState.UPGRADING:
86+ return { badge: 1, status: NewVersionViewStatus.VIEW_INSTALLING };
87+ case UpdateState.INSTALL_SUCCESS:
88+ return { badge: 1, status: NewVersionViewStatus.VIEW_INSTALL_SUCCESS };
89+ case UpdateState.DOWNLOAD_CANCEL:
90+ case UpdateState.DOWNLOAD_FAILED:
91+ case UpdateState.INSTALL_FAILED:
92+ case UpdateState.UPGRADE_FAILED:
93+ return { badge: 1, status: NewVersionViewStatus.VIEW_CHECK_SUCCESS };
94+ case UpdateState.UPGRADE_SUCCESS:
95+ return { badge: 0, status: NewVersionViewStatus.VIEW_INIT };
96+ default:
97+ return undefined;
98+ }
99+ }
100+ 
101+ private static writeView(view: SettingsUpdateView, context?: common.Context): void {
102+ const resolvedContext = SettingsSyncUtils.getContext(context);
103+ if (!resolvedContext) {
104+ LogUtils.warn(TAG, 'writeView skip because context is empty');
105+ return;
106+ }
107+ 
108+ SettingsSyncUtils.setSettingsValue(resolvedContext, HW_NEW_SYSTEM_UPDATE, String(view.badge));
109+ SettingsSyncUtils.setSettingsValue(resolvedContext, HW_NEW_SYSTEM_UPDATE_STATUS, String(view.status));
110+ }
111+ 
112+ private static getContext(context?: common.Context): common.Context | undefined {
113+ return globalThis.abilityContext || context || globalThis.stageContext || globalThis.extensionContext;
114+ }
115+ 
116+ private static setSettingsValue(context: common.Context, key: string, value: string): void {
117+ try {
118+ const result = Settings.setValueSync(context, key, value);
119+ LogUtils.info(TAG, `setSettingsValue key=${key}, value=${value}, result=${JSON.stringify(result)}`);
120+ if (!result) {
121+ LogUtils.warn(TAG, `setSettingsValue returned false, key=${key}`);
122+ }
123+ } catch (err) {
124+ LogUtils.error(TAG, `setSettingsValue failed, key=${key}, err=${JSON.stringify(err)}`);
125+ }
126+ }
127+}
@@ -4,6 +4,10 @@
4 "name":"btn_check_new_version",4 "name":"btn_check_new_version",
5 "value":"Check for updates"5 "value":"Check for updates"
6 },6 },
7+ {
8+ "name":"btn_checking_new_version",
9+ "value":"Checking for updates"
10+ },
7 {11 {
8 "name":"btn_download",12 "name":"btn_download",
9 "value":"Download & install"13 "value":"Download & install"
@@ -68,6 +72,22 @@
68 "name":"package_verify_fail",72 "name":"package_verify_fail",
69 "value":"Update verification failed."73 "value":"Update verification failed."
70 },74 },
75+ {
76+ "name":"download_verify_fail_notification_title",
77+ "value":"New version download failed"
78+ },
79+ {
80+ "name":"download_verify_fail_notification_content",
81+ "value":"Update package verification failed"
82+ },
83+ {
84+ "name":"package_changed_title",
85+ "value":"Update package changed"
86+ },
87+ {
88+ "name":"package_changed_content",
89+ "value":"The update package has changed. Please wait for a later push."
90+ },
71 {91 {
72 "name":"later",92 "name":"later",
73 "value":"Later"93 "value":"Later"
@@ -76,6 +96,10 @@
76 "name":"install_now",96 "name":"install_now",
77 "value":"Install now (%d)"97 "value":"Install now (%d)"
78 },98 },
99+ {
100+ "name":"btn_install_now",
101+ "value":"Install now"
102+ },
79 {103 {
80 "name":"software_update",104 "name":"software_update",
81 "value":"Software update"105 "value":"Software update"
@@ -98,7 +122,7 @@
98 },122 },
99 {123 {
100 "name":"download_fail",124 "name":"download_fail",
101- "value":"Download failed. Please check for updates again."125+ "value":"Tap \"OK\" to check for updates again."
102 },126 },
103 {127 {
104 "name":"update_fail",128 "name":"update_fail",
@@ -182,6 +206,50 @@
182 {206 {
183 "name":"reboot_now",207 "name":"reboot_now",
184 "value":"Reboot Now (%d)"208 "value":"Reboot Now (%d)"
209+ },
210+ {
211+ "name": "setting_auto_download_title",
212+ "value": "Automatically download updates"
213+ },
214+ {
215+ "name": "setting_auto_download_description",
216+ "value": "Updates for this device will be downloaded over WLAN."
217+ },
218+ {
219+ "name": "setting_auto_update_title",
220+ "value": "Update overnight"
221+ },
222+ {
223+ "name": "setting_auto_update_description",
224+ "value": "When enabled, your device will automatically install updates and restart between %s if not in use. Some battery power will be used."
225+ },
226+ {
227+ "name": "local_settings",
228+ "value": "Update options"
229+ },
230+ {
231+ "name":"detail_info",
232+ "value":"Details"
233+ },
234+ {
235+ "name":"cancel_download",
236+ "value":"Cancel download"
237+ },
238+ {
239+ "name":"continue_download",
240+ "value":"Continue download"
241+ },
242+ {
243+ "name":"cancel_download_confirm_title",
244+ "value":"Cancel download?"
245+ },
246+ {
247+ "name":"cancel_download_confirm_message",
248+ "value":"Tap \"Cancel download\" to cancel this update package download."
249+ },
250+ {
251+ "name":"desktop_download_dialog_content",
252+ "value":"A new software update is available. Download and install it now?"
185 }253 }
186 ]254 ]
187-}255+}
@@ -4,6 +4,10 @@
4 "name":"btn_check_new_version",4 "name":"btn_check_new_version",
5 "value":"检查更新"5 "value":"检查更新"
6 },6 },
7+ {
8+ "name":"btn_checking_new_version",
9+ "value":"检查更新中"
10+ },
7 {11 {
8 "name":"btn_download",12 "name":"btn_download",
9 "value":"下载并安装"13 "value":"下载并安装"
@@ -38,7 +42,7 @@
38 },42 },
39 {43 {
40 "name":"ok",44 "name":"ok",
41- "value":"确"45+ "value":"确"
42 },46 },
43 {47 {
44 "name":"cancel",48 "name":"cancel",
@@ -68,6 +72,22 @@
68 "name":"package_verify_fail",72 "name":"package_verify_fail",
69 "value":"升级包校验失败。"73 "value":"升级包校验失败。"
70 },74 },
75+ {
76+ "name":"download_verify_fail_notification_title",
77+ "value":"新版本下载失败"
78+ },
79+ {
80+ "name":"download_verify_fail_notification_content",
81+ "value":"升级包校验失败"
82+ },
83+ {
84+ "name":"package_changed_title",
85+ "value":"更新包有变更"
86+ },
87+ {
88+ "name":"package_changed_content",
89+ "value":"更新包有变更,请等待后续推送。"
90+ },
71 {91 {
72 "name":"later",92 "name":"later",
73 "value":"稍后"93 "value":"稍后"
@@ -76,6 +96,10 @@
76 "name":"install_now",96 "name":"install_now",
77 "value":"现在安装 (%d)"97 "value":"现在安装 (%d)"
78 },98 },
99+ {
100+ "name":"btn_install_now",
101+ "value":"现在安装"
102+ },
79 {103 {
80 "name":"software_update",104 "name":"software_update",
81 "value":"软件更新"105 "value":"软件更新"
@@ -98,7 +122,7 @@
98 },122 },
99 {123 {
100 "name":"download_fail",124 "name":"download_fail",
101- "value":"下载失败,请重新搜索。"125+ "value":"点击“确定”将重新搜索新版本。"
102 },126 },
103 {127 {
104 "name":"update_fail",128 "name":"update_fail",
@@ -182,6 +206,50 @@
182 {206 {
183 "name":"reboot_now",207 "name":"reboot_now",
184 "value":"现在重启 (%d)"208 "value":"现在重启 (%d)"
209+ },
210+ {
211+ "name": "setting_auto_download_title",
212+ "value": "WLAN 下自动下载"
213+ },
214+ {
215+ "name": "setting_auto_download_description",
216+ "value": "本机仅 WLAN 环境自动下载升级包或者更新应用。"
217+ },
218+ {
219+ "name": "setting_auto_update_title",
220+ "value": "夜间安装"
221+ },
222+ {
223+ "name": "setting_auto_update_description",
224+ "value": "开启后,本机将在 %s 且处于空闲状态时自动安装并重启。安装过程中将少量耗电。"
225+ },
226+ {
227+ "name": "local_settings",
228+ "value": "更新选项"
229+ },
230+ {
231+ "name":"detail_info",
232+ "value":"详细信息"
233+ },
234+ {
235+ "name":"cancel_download",
236+ "value":"取消下载"
237+ },
238+ {
239+ "name":"continue_download",
240+ "value":"继续下载"
241+ },
242+ {
243+ "name":"cancel_download_confirm_title",
244+ "value":"确认取消下载?"
245+ },
246+ {
247+ "name":"cancel_download_confirm_message",
248+ "value":"点击“取消下载”将取消本次更新包的下载任务"
249+ },
250+ {
251+ "name":"desktop_download_dialog_content",
252+ "value":"发现新的软件版本,是否立即下载并安装?"
185 }253 }
186 ]254 ]
187-}255+}
@@ -1,5 +1,5 @@
1{1{
2- "modelVersion": "5.0.2",2+ "modelVersion": "6.0.0",
3 "dependencies": {},3 "dependencies": {},
4 "execution": {4 "execution": {
5 // "analyze": "default", /* Define the build analyze mode. Value: [ "default" | "verbose" | false ]. Default: "default" */5 // "analyze": "default", /* Define the build analyze mode. Value: [ "default" | "verbose" | false ]. Default: "default" */
@@ -1,5 +1,5 @@
1{1{
2- "modelVersion": "5.0.2",2+ "modelVersion": "6.0.0",
3 "license": "ISC",3 "license": "ISC",
4 "devDependencies": {},4 "devDependencies": {},
5 "author": "",5 "author": "",
@@ -32,5 +32,6 @@ export default class MyAbilityStage extends AbilityStage {
32 AppStorage.SetOrCreate('isClickInstall', 0);32 AppStorage.SetOrCreate('isClickInstall', 0);
33 AppStorage.SetOrCreate('configLanguage', '');33 AppStorage.SetOrCreate('configLanguage', '');
34 AppStorage.SetOrCreate('installStatusRefresh', '');34 AppStorage.SetOrCreate('installStatusRefresh', '');
35+ AppStorage.SetOrCreate('downloadNoNetworkDialogVisible', false);
35 }36 }
36}37}
@@ -16,6 +16,7 @@
16import Ability from '@ohos.app.ability.UIAbility';16import Ability from '@ohos.app.ability.UIAbility';
17import type Want from '@ohos.app.ability.Want';17import type Want from '@ohos.app.ability.Want';
18import type AbilityConstant from '@ohos.app.ability.AbilityConstant';18import type AbilityConstant from '@ohos.app.ability.AbilityConstant';
19+import notification from '@ohos.notificationManager';
19import router from '@ohos.router';20import router from '@ohos.router';
20import type update from '@ohos.update';21import type update from '@ohos.update';
21import type window from '@ohos.window';22import type window from '@ohos.window';
@@ -54,8 +55,8 @@ export default class MainAbility extends Ability {
54 globalThis.AbilityStatus = null;55 globalThis.AbilityStatus = null;
55 if (globalThis.abilityWant?.uri === 'pages/newVersion') {56 if (globalThis.abilityWant?.uri === 'pages/newVersion') {
56 windowStage.loadContent('pages/newVersion', null);57 windowStage.loadContent('pages/newVersion', null);
57- } else if (globalThis.abilityWant?.uri === 'pages/setting') {58+ } else if (globalThis.abilityWant?.uri === 'pages/settingsView') {
58- windowStage.loadContent('pages/setting', null);59+ windowStage.loadContent('pages/settingsView', null);
59 } else {60 } else {
60 windowStage.loadContent('pages/index', null);61 windowStage.loadContent('pages/index', null);
61 }62 }
@@ -63,6 +64,7 @@ export default class MainAbility extends Ability {
63 64 
64 onNewWant(want: Want): void {65 onNewWant(want: Want): void {
65 this.log('BaseAbility onNewWant:' + JSON.stringify(want));66 this.log('BaseAbility onNewWant:' + JSON.stringify(want));
67+ globalThis.abilityWant = want;
66 globalThis.newPage = want.uri;68 globalThis.newPage = want.uri;
67 if (globalThis.AbilityStatus === 'ON_FOREGROUND') {69 if (globalThis.AbilityStatus === 'ON_FOREGROUND') {
68 this.routePage();70 this.routePage();
@@ -85,6 +87,7 @@ export default class MainAbility extends Ability {
85 87 
86 onForeground(): void {88 onForeground(): void {
87 this.log('BaseAbility onForeground');89 this.log('BaseAbility onForeground');
90+ this.requestEnableNotificationIfNeeded();
88 new NotificationHelper().cancelAll();91 new NotificationHelper().cancelAll();
89 globalThis.AbilityStatus = 'ON_FOREGROUND';92 globalThis.AbilityStatus = 'ON_FOREGROUND';
90 setTimeout(() => {93 setTimeout(() => {
@@ -122,6 +125,18 @@ export default class MainAbility extends Ability {
122 }125 }
123 }126 }
124 127 
128+ private async requestEnableNotificationIfNeeded(): Promise<void> {
129+ try {
130+ if (await notification.isNotificationEnabled()) {
131+ return;
132+ }
133+ await notification.requestEnableNotification(this.context);
134+ this.log('requestEnableNotification success');
135+ } catch (err) {
136+ this.log('requestEnableNotificationIfNeeded failed: ' + JSON.stringify(err));
137+ }
138+ }
139+ 
125 protected log(message: string): void {140 protected log(message: string): void {
126 LogUtils.log('BaseAbility', message);141 LogUtils.log('BaseAbility', message);
127 }142 }
@@ -0,0 +1,46 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import UIExtensionAbility from '@ohos.app.ability.UIExtensionAbility';
17+import type Want from '@ohos.app.ability.Want';
18+import type UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession';
19+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
20+ 
21+const TAG = 'UpdateSystemDialogAbility';
22+ 
23+export default class UpdateSystemDialogAbility extends UIExtensionAbility {
24+ onSessionCreate(want: Want, session: UIExtensionContentSession): void {
25+ LogUtils.info(TAG, `onSessionCreate: ${JSON.stringify(want?.parameters)}`);
26+ const storage = new LocalStorage();
27+ storage.setOrCreate('session', session);
28+ storage.setOrCreate('dialogType', (want?.parameters?.dialogType as string) ?? '');
29+ try {
30+ session.loadContent('UpdateSystemDialog/pages/UpdateSystemDialogPage', storage);
31+ } catch (error) {
32+ LogUtils.error(TAG, `Load system dialog content failed: ${JSON.stringify(error)}`);
33+ try {
34+ session.terminateSelf();
35+ } catch (terminateError) {
36+ LogUtils.error(TAG, `Terminate invalid session failed: ${JSON.stringify(terminateError)}`);
37+ }
38+ return;
39+ }
40+ try {
41+ session.setWindowBackgroundColor('#00000000');
42+ } catch (error) {
43+ LogUtils.warn(TAG, `Set transparent background failed: ${JSON.stringify(error)}`);
44+ }
45+ }
46+}
@@ -0,0 +1,104 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import type common from '@ohos.app.ability.common';
17+import type UIExtensionContentSession from '@ohos.app.ability.UIExtensionContentSession';
18+import { AlertDialog } from '@kit.ArkUI';
19+import { MAIN_ABILITY_NAME, PACKAGE_NAME } from '@ohos/common/src/main/ets/const/update_const';
20+import { DeviceUtils } from '@ohos/common/src/main/ets/util/DeviceUtils';
21+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
22+import { UpdateUtils } from '@ohos/common/src/main/ets/util/UpdateUtils';
23+import { SceneBoardDialogType } from 'ota/src/main/ets/notify/SceneBoardDialogManager';
24+ 
25+const TAG = 'UpdateSystemDialogPage';
26+const storage = LocalStorage.getShared();
27+ 
28+@Entry(storage)
29+@Component
30+struct UpdateSystemDialogPage {
31+ private session?: UIExtensionContentSession = storage?.get('session') as UIExtensionContentSession;
32+ private context = getContext(this) as common.UIExtensionContext;
33+ private dialogType: string = (storage?.get('dialogType') as string) ?? '';
34+ private actionHandled: boolean = false;
35+ 
36+ private downloadDialogController = new CustomDialogController({
37+ builder: AlertDialog({
38+ primaryTitle: $r('app.string.software_update'),
39+ content: $r('app.string.desktop_download_dialog_content'),
40+ primaryButton: {
41+ value: $r('app.string.later'),
42+ action: () => {
43+ this.closeSession();
44+ }
45+ },
46+ secondaryButton: {
47+ value: $r('app.string.btn_download'),
48+ action: () => {
49+ this.startDownload();
50+ }
51+ }
52+ }),
53+ alignment: DeviceUtils.getDialogLocation(),
54+ offset: {
55+ dx: '0vp',
56+ dy: DeviceUtils.getDialogOffsetY()
57+ },
58+ autoCancel: false,
59+ cancel: () => {
60+ this.closeSession();
61+ }
62+ });
63+ 
64+ aboutToAppear(): void {
65+ LogUtils.info(TAG, `aboutToAppear, dialogType=${this.dialogType}`);
66+ if (this.dialogType === SceneBoardDialogType.DOWNLOAD_REMINDER) {
67+ this.downloadDialogController.open();
68+ return;
69+ }
70+ LogUtils.error(TAG, `Unsupported dialog type: ${this.dialogType}`);
71+ this.closeSession();
72+ }
73+ 
74+ private startDownload(): void {
75+ if (this.actionHandled) {
76+ return;
77+ }
78+ this.actionHandled = true;
79+ UpdateUtils.startAbility(this.context, {
80+ bundleName: PACKAGE_NAME,
81+ abilityName: MAIN_ABILITY_NAME,
82+ uri: 'pages/newVersion',
83+ parameters: {
84+ autoDownload: true
85+ }
86+ });
87+ this.closeSession();
88+ }
89+ 
90+ private closeSession(): void {
91+ try {
92+ this.session?.terminateSelf();
93+ } catch (error) {
94+ LogUtils.error(TAG, `Terminate system dialog session failed: ${JSON.stringify(error)}`);
95+ }
96+ }
97+ 
98+ build() {
99+ Column()
100+ .width('100%')
101+ .height('100%')
102+ .backgroundColor('#00000000')
103+ }
104+}
@@ -79,7 +79,7 @@ struct CurrentVersion {
79 build() {79 build() {
80 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {80 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {
81 Column() {81 Column() {
82- TitleBar({ title: $r('app.string.title_current_version'), onBack: this.onBackPress.bind(this) })82+ TitleBar({ title: $r('app.string.title_current_version'), onBack: this.onBackPress.bind(this), showMenu: true})
83 }.flexShrink(0)83 }.flexShrink(0)
84 84 
85 Scroll() {85 Scroll() {
@@ -14,6 +14,7 @@
14 */14 */
15 15 
16import update from '@ohos.update';16import update from '@ohos.update';
17+import window from '@ohos.window';
17import {18import {
18 ErrorCode,19 ErrorCode,
19 OtaStatus,20 OtaStatus,
@@ -24,7 +25,6 @@ import {
24} from '@ohos/common/src/main/ets/const/update_const';25} from '@ohos/common/src/main/ets/const/update_const';
25import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar';26import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar';
26import { HomeCardView } from '@ohos/common/src/main/ets/component/HomeCardView';27import { HomeCardView } from '@ohos/common/src/main/ets/component/HomeCardView';
27-import { CheckingDots } from '@ohos/common/src/main/ets/component/CheckingDots';
28import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';28import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
29import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';29import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';
30import RouterUtils from 'ota/src/main/ets/util/RouterUtils';30import RouterUtils from 'ota/src/main/ets/util/RouterUtils';
@@ -44,6 +44,9 @@ enum NewVersionStatus {
44 HAS_NEW_VERSION = 'HAS_NEW_VERSION',44 HAS_NEW_VERSION = 'HAS_NEW_VERSION',
45}45}
46 46 
47+const CHECK_ANIMATION_DURATION: number = 450;
48+const CHECK_ANIMATION_SCALE: number = 1.2;
49+ 
47/**50/**
48 * 主页index51 * 主页index
49 *52 *
@@ -53,7 +56,6 @@ enum NewVersionStatus {
53@Component56@Component
54struct Index {57struct Index {
55 @State newVersionActionText: Resource = $r('app.string.check_version_status_no_new');58 @State newVersionActionText: Resource = $r('app.string.check_version_status_no_new');
56- @State dotTextPlay: boolean = false;
57 @State newVersionStatus: string = NewVersionStatus.NO_NEW_VERSION;59 @State newVersionStatus: string = NewVersionStatus.NO_NEW_VERSION;
58 @StorageProp('updateStatus')60 @StorageProp('updateStatus')
59 private updateStatus: number = AppStorage.Get('updateStatus') as number;61 private updateStatus: number = AppStorage.Get('updateStatus') as number;
@@ -65,6 +67,7 @@ struct Index {
65 private checkIntervalId: number | null = null;67 private checkIntervalId: number | null = null;
66 private checkLoopTimes: number = 0;68 private checkLoopTimes: number = 0;
67 @State private buttonText: string = '';69 @State private buttonText: string = '';
70+ @State private statusBarHeight: number = 0;
68 71 
69 /**72 /**
70 * 是否已经有搜包结果73 * 是否已经有搜包结果
@@ -92,7 +95,7 @@ struct Index {
92 this.log('afterNewVersionFound');95 this.log('afterNewVersionFound');
93 setTimeout(() => {96 setTimeout(() => {
94 RouterUtils.openNewVersionPage();97 RouterUtils.openNewVersionPage();
95- }, 50);98+ }, CHECK_ANIMATION_DURATION);
96 } else if (this.checkEndStatus == NewVersionStatus.NO_NEW_VERSION) {99 } else if (this.checkEndStatus == NewVersionStatus.NO_NEW_VERSION) {
97 this.log('afterNewVersionFound no new version');100 this.log('afterNewVersionFound no new version');
98 this.newVersionStatus = NewVersionStatus.NO_NEW_VERSION;101 this.newVersionStatus = NewVersionStatus.NO_NEW_VERSION;
@@ -107,7 +110,6 @@ struct Index {
107 this.checkEndStatus = newVersionStatus; // 延时更新110 this.checkEndStatus = newVersionStatus; // 延时更新
108 if (newVersionStatus == NewVersionStatus.CHECKING) {111 if (newVersionStatus == NewVersionStatus.CHECKING) {
109 this.newVersionStatus = newVersionStatus;112 this.newVersionStatus = newVersionStatus;
110- this.newVersionActionText = $r('app.string.check_version_status_checking');
111 }113 }
112 }114 }
113 115 
@@ -186,7 +188,6 @@ struct Index {
186 private uiCheckLoop(running: boolean): void {188 private uiCheckLoop(running: boolean): void {
187 this.log(`uiCheckLoop, running ${running}`)189 this.log(`uiCheckLoop, running ${running}`)
188 this.displayVideo = running;190 this.displayVideo = running;
189- this.dotTextPlay = running;
190 if (this.checkIntervalId != null) {191 if (this.checkIntervalId != null) {
191 clearInterval(this.checkIntervalId);192 clearInterval(this.checkIntervalId);
192 this.checkIntervalId = null;193 this.checkIntervalId = null;
@@ -260,53 +261,60 @@ struct Index {
260 261 
261 build() {262 build() {
262 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {263 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {
263- TitleBar({264+ Column() {
264- title: $r('app.string.software_update'),265+ TitleBar({
265- onBack: this.onBackPress.bind(this)266+ title: $r('app.string.software_update'),
266- })267+ onBack: this.onBackPress.bind(this),
268+ showMenu: true
269+ })
270+ }
271+ .width('100%')
272+ .padding({ top: this.statusBarHeight })
273+ 
267 Column() {274 Column() {
268 Scroll() {275 Scroll() {
269- Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {276+ Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
270- HomeCardView({ playVideo: this.displayVideo, videoController: this.videoController, onVideoFinished:277+ Column() {
271- (): boolean => {278+ Image($r('app.media.center'))
272- this.checkTimes++;279+ .width($r('app.float.index_center_image_width'))
273- return this.onCheckAction.bind(this)();280+ .objectFit(ImageFit.Contain)
274- } })281+ .margin({bottom: '10vp'})
275- Text(this.displayVersion)282+ Row() {
276- .fontSize($r('app.float.home_text_size_version_name'))283+ Text(this.newVersionActionText)
277- .fontWeight(FontWeight.Medium)284+ .fontSize($r('app.float.text_size_body'))
278- .margin({285+ .fontColor(this.newVersionStatus !== NewVersionStatus.CHECKING ?
279- top: $r('app.float.index_version_name_margin_top'),286+ $r('app.color.deep_blue') : Color.Black)
280- bottom: $r('app.float.index_version_name_margin_bottom')287+ .opacity(this.newVersionStatus !== NewVersionStatus.CHECKING ? 1 : 0.6)
281- })288+ .fontWeight(FontWeight.Regular)
282- Row() {289+ .align(Alignment.Start)
283- Text(this.newVersionActionText)290+ .onClick(() => this.handleStateClicked())
284- .fontSize($r('app.float.text_size_body'))291+ if (this.newVersionStatus == NewVersionStatus.HAS_NEW_VERSION) {
285- .fontColor(this.newVersionStatus !== NewVersionStatus.CHECKING ?292+ Circle({ width: '8vp', height: '8vp' })
286- $r('app.color.blue') : Color.Black)293+ .margin({ left: $r('app.float.index_has_new_version_margin_left') })
287- .opacity(this.newVersionStatus !== NewVersionStatus.CHECKING ? 1 : 0.6)294+ .fill($r('app.color.has_new_version_circle_fill'))
288- .fontWeight(FontWeight.Regular)295+ }
289- .align(Alignment.Start)
290- .onClick(() => this.handleStateClicked())
291- CheckingDots({ dotTextPlay: this.dotTextPlay })
292- if (this.newVersionStatus == NewVersionStatus.HAS_NEW_VERSION) {
293- Circle({ width: '8vp', height: '8vp' })
294- .margin({ left: $r('app.float.index_has_new_version_margin_left') })
295- .fill($r('app.color.has_new_version_circle_fill'))
296 }296 }
297 }297 }
298+ .alignItems(HorizontalAlign.Center)
299+ .scale({
300+ x: this.newVersionStatus === NewVersionStatus.CHECKING ? CHECK_ANIMATION_SCALE : 1,
301+ y: this.newVersionStatus === NewVersionStatus.CHECKING ? CHECK_ANIMATION_SCALE : 1
302+ })
303+ .opacity(this.newVersionStatus === NewVersionStatus.CHECKING ? 0 : 1)
304+ .animation({ duration: CHECK_ANIMATION_DURATION, curve: Curve.EaseInOut })
298 }.padding({305 }.padding({
299 left: $r('app.float.index_content_padding_horizontal'),306 left: $r('app.float.index_content_padding_horizontal'),
300 right: $r('app.float.index_content_padding_horizontal'),307 right: $r('app.float.index_content_padding_horizontal'),
301 bottom: $r('app.float.index_content_padding_bottom')308 bottom: $r('app.float.index_content_padding_bottom')
302 }).width('100%')309 }).width('100%')
303- }.width('100%')310+ }.width('100%').height('100%')
304 .scrollable(ScrollDirection.Vertical)311 .scrollable(ScrollDirection.Vertical)
305 .scrollBar(BarState.On)312 .scrollBar(BarState.On)
306 }.flexGrow(1)313 }.flexGrow(1)
307 Column() {314 Column() {
308 Button() {315 Button() {
309- Text(this.buttonText)316+ Text(this.newVersionStatus === NewVersionStatus.CHECKING ?
317+ $r('app.string.btn_checking_new_version') : this.buttonText)
310 .fontSize($r('app.float.text_size_btn')).fontColor(Color.White).fontWeight(FontWeight.Medium)318 .fontSize($r('app.float.text_size_btn')).fontColor(Color.White).fontWeight(FontWeight.Medium)
311 .margin({ left: $r('app.float.custom_button_text_margin_left'),319 .margin({ left: $r('app.float.custom_button_text_margin_left'),
312 right: $r('app.float.custom_button_text_margin_right') })320 right: $r('app.float.custom_button_text_margin_right') })
@@ -328,6 +336,9 @@ struct Index {
328 bottom: $r('app.float.index_content_padding_bottom')336 bottom: $r('app.float.index_content_padding_bottom')
329 })337 })
330 }338 }
339+ .backgroundImage($r('app.media.background'))
340+ .backgroundImageSize(ImageSize.Cover)
341+ .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
331 .backgroundColor($r('app.color.page_background'))342 .backgroundColor($r('app.color.page_background'))
332 .width('100%')343 .width('100%')
333 .height('100%')344 .height('100%')
@@ -39,6 +39,7 @@ import { StateManager, UpdateAction } from 'ota/src/main/ets/manager/StateManage
39import { UpgradeAdapter } from 'ota/src/main/ets/UpgradeAdapter';39import { UpgradeAdapter } from 'ota/src/main/ets/UpgradeAdapter';
40import { VersionUtils } from 'ota/src/main/ets/util/VersionUtils';40import { VersionUtils } from 'ota/src/main/ets/util/VersionUtils';
41import { NotificationHelper } from 'ota/src/main/ets/notify/NotificationHelper';41import { NotificationHelper } from 'ota/src/main/ets/notify/NotificationHelper';
42+import { AlertDialog } from '@kit.ArkUI';
42import prompt from '@ohos.prompt';43import prompt from '@ohos.prompt';
43import RouterUtils from 'ota/src/main/ets/util/RouterUtils';44import RouterUtils from 'ota/src/main/ets/util/RouterUtils';
44 45 
@@ -56,6 +57,8 @@ struct NewVersion {
56 @State private displayNewVersionName: string = '';57 @State private displayNewVersionName: string = '';
57 @State private isButtonEnable: boolean = true;58 @State private isButtonEnable: boolean = true;
58 @State private isButtonVisible: boolean = true;59 @State private isButtonVisible: boolean = true;
60+ @StorageProp('downloadProgress')
61+ private downloadProgress: number = AppStorage.Get('downloadProgress') as number;
59 @StorageProp('updateStatus')62 @StorageProp('updateStatus')
60 @Watch('initDataByStatus') private updateStatus: number = AppStorage.Get('updateStatus') as number;63 @Watch('initDataByStatus') private updateStatus: number = AppStorage.Get('updateStatus') as number;
61 @StorageProp('isClickInstall') @Watch('onInstallClick')64 @StorageProp('isClickInstall') @Watch('onInstallClick')
@@ -68,6 +71,10 @@ struct NewVersion {
68 @State private buttonText: string = '';71 @State private buttonText: string = '';
69 @StorageProp('installStatusRefresh') @Watch('refresh')72 @StorageProp('installStatusRefresh') @Watch('refresh')
70 private installStatusRefresh: string = AppStorage.Get('installStatusRefresh') as string;73 private installStatusRefresh: string = AppStorage.Get('installStatusRefresh') as string;
74+ @StorageProp('downloadNoNetworkDialogVisible') @Watch('syncDownloadNoNetworkDialog')
75+ private downloadNoNetworkDialogVisible: boolean =
76+ AppStorage.Get('downloadNoNetworkDialogVisible') as boolean;
77+ @State private isNoNetworkDialogOpen: boolean = false;
71 78 
72 private countdownDialogController = new CustomDialogController({79 private countdownDialogController = new CustomDialogController({
73 builder: CountDownInstallDialogBuilder({80 builder: CountDownInstallDialogBuilder({
@@ -103,6 +110,26 @@ struct NewVersion {
103 })110 })
104 });111 });
105 112 
113+ private noNetworkDialogController: CustomDialogController = new CustomDialogController({
114+ builder: AlertDialog({
115+ primaryTitle: $r('app.string.net_error_title'),
116+ content: $r('app.string.net_error_content'),
117+ primaryButton: {
118+ value: $r('app.string.button_know'),
119+ action: () => {
120+ this.isNoNetworkDialogOpen = false;
121+ AppStorage.Set('downloadNoNetworkDialogVisible', false);
122+ }
123+ }
124+ }),
125+ autoCancel: false,
126+ alignment: DeviceUtils.getDialogLocation(),
127+ offset: ({
128+ dx: '0vp',
129+ dy: DeviceUtils.getDialogOffsetY()
130+ })
131+ });
132+ 
106 private refreshDescription(): void {133 private refreshDescription(): void {
107 this.log('refreshDescription');134 this.log('refreshDescription');
108 this.description = JSON.stringify(this.changelogArray);135 this.description = JSON.stringify(this.changelogArray);
@@ -119,6 +146,8 @@ struct NewVersion {
119 this.initDataByStatus();146 this.initDataByStatus();
120 new NotificationHelper().cancelAll();147 new NotificationHelper().cancelAll();
121 this.handleAbnormalState();148 this.handleAbnormalState();
149+ this.handleAbilityWantAction();
150+ this.syncDownloadNoNetworkDialog();
122 }151 }
123 152 
124 private async handleAbnormalState(): Promise<void> {153 private async handleAbnormalState(): Promise<void> {
@@ -135,6 +164,10 @@ struct NewVersion {
135 164 
136 onPageHide() {165 onPageHide() {
137 this.log('onPageHide NewVersionPage');166 this.log('onPageHide NewVersionPage');
167+ if (this.isNoNetworkDialogOpen) {
168+ this.noNetworkDialogController.close();
169+ this.isNoNetworkDialogOpen = false;
170+ }
138 }171 }
139 172 
140 onBackPress(): boolean {173 onBackPress(): boolean {
@@ -165,6 +198,28 @@ struct NewVersion {
165 }198 }
166 }199 }
167 200 
201+ private syncDownloadNoNetworkDialog(): void {
202+ if (this.downloadNoNetworkDialogVisible) {
203+ this.showNoNetworkDialog();
204+ return;
205+ }
206+ this.hideNoNetworkDialog();
207+ }
208+ 
209+ showNoNetworkDialog(): void {
210+ if (!this.isNoNetworkDialogOpen) {
211+ this.isNoNetworkDialogOpen = true;
212+ this.noNetworkDialogController.open();
213+ }
214+ }
215+ 
216+ hideNoNetworkDialog(): void {
217+ if (this.isNoNetworkDialogOpen) {
218+ this.noNetworkDialogController.close();
219+ this.isNoNetworkDialogOpen = false;
220+ }
221+ }
222+ 
168 public onLanguageChange(): void {223 public onLanguageChange(): void {
169 this.initDataByStatus();224 this.initDataByStatus();
170 }225 }
@@ -241,19 +296,48 @@ struct NewVersion {
241 }296 }
242 }297 }
243 298 
299+ private async handleAbilityWantAction(): Promise<void> {
300+ const autoDownload = Boolean(globalThis.abilityWant?.parameters?.autoDownload);
301+ const autoInstall = Boolean(globalThis.abilityWant?.parameters?.autoInstall);
302+ if (!autoDownload && !autoInstall) {
303+ return;
304+ }
305+ globalThis.abilityWant = null;
306+ const statusResult = await OtaUpdateManager.getInstance().getOtaStatus();
307+ if (statusResult?.callResult !== UpgradeCallResult.OK || statusResult?.data?.status === undefined) {
308+ this.log('Ignore ability want action because the current update status is unavailable.');
309+ return;
310+ }
311+ const currentStatus = statusResult.data.status;
312+ await this.initDataByStatus();
313+ if (autoDownload) {
314+ if (StateManager.getButtonClickAction(currentStatus) === UpdateAction.DOWNLOAD) {
315+ await this.startDownload();
316+ }
317+ return;
318+ }
319+ if (StateManager.getButtonClickAction(currentStatus) === UpdateAction.INSTALL) {
320+ await this.upgrade();
321+ }
322+ }
323+ 
324+ private async startDownload(): Promise<void> {
325+ if (await NetUtils.isCellularNetwork()) {
326+ DialogHelper.displayNetworkDialog();
327+ return;
328+ }
329+ if (this.isABStreamInstall()) {
330+ await this.upgrade();
331+ return;
332+ }
333+ await OtaUpdateManager.getInstance().download();
334+ }
335+ 
244 private async handleButtonClick(): Promise<void> {336 private async handleButtonClick(): Promise<void> {
245 this.log('handleButtonClick');337 this.log('handleButtonClick');
246 switch (StateManager.getButtonClickAction(this.updateStatus)) {338 switch (StateManager.getButtonClickAction(this.updateStatus)) {
247 case UpdateAction.DOWNLOAD:339 case UpdateAction.DOWNLOAD:
248- if (await NetUtils.isCellularNetwork()) {340+ await this.startDownload();
249- DialogHelper.displayNetworkDialog();
250- } else {
251- if (this.isABStreamInstall()) {
252- this.upgrade();
253- } else {
254- OtaUpdateManager.getInstance().download();
255- }
256- }
257 break;341 break;
258 case UpdateAction.INSTALL:342 case UpdateAction.INSTALL:
259 this.upgrade();343 this.upgrade();
@@ -262,7 +346,11 @@ struct NewVersion {
262 this.reboot();346 this.reboot();
263 break;347 break;
264 case UpdateAction.CANCEL:348 case UpdateAction.CANCEL:
265- OtaUpdateManager.getInstance().cancel();349+ if (this.updateStatus === UpdateState.DOWNLOADING) {
350+ DialogHelper.displayCancelDownloadDialog();
351+ } else {
352+ OtaUpdateManager.getInstance().cancel();
353+ }
266 break;354 break;
267 case UpdateAction.RESUME:355 case UpdateAction.RESUME:
268 if (this.isABStreamInstall()) {356 if (this.isABStreamInstall()) {
@@ -279,28 +367,21 @@ struct NewVersion {
279 build() {367 build() {
280 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {368 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {
281 Column() {369 Column() {
282- TitleBar({ title: $r('app.string.title_new_version'), onBack: this.onBackPress.bind(this)});370+ TitleBar({ title: $r('app.string.title_new_version'), onBack: this.onBackPress.bind(this), showMenu: true});
283 }.flexShrink(0);371 }.flexShrink(0);
284 Column() {372 Column() {
285 Scroll() {373 Scroll() {
286 Column() {374 Column() {
287- ProgressContent();375+ Row() {
376+ Image($r('app.media.home_bg'))
377+ .width('100%')
378+ .constraintSize({ maxWidth: 600 })
379+ .objectFit(ImageFit.Contain)
380+ }
381+ .width('100%')
382+ .padding({ left: 20, right: 20, bottom: 20 })
383+ .justifyContent(FlexAlign.Center)
288 if (this.isInitComplete) {384 if (this.isInitComplete) {
289- Text(this.displayNewVersionName)
290- .fontSize($r('app.float.text_size_version_name'))
291- .maxLines(5)
292- .textOverflow({ overflow: TextOverflow.Ellipsis })
293- .padding({
294- right: $r('app.float.changelog_detail_content_padding_horizontal'),
295- left: $r('app.float.changelog_detail_content_padding_horizontal'),
296- })
297- .fontWeight(FontWeight.Medium);
298- Text(this.displayFileSize)
299- .fontSize($r('app.float.text_size_version_size')).fontWeight(FontWeight.Regular).opacity(0.6)
300- .margin({
301- top: $r('app.float.new_version_size_margin_top'),
302- bottom: $r('app.float.new_version_size_margin_bottom')
303- })
304 ChangelogContent({385 ChangelogContent({
305 isCurrentPage: false,386 isCurrentPage: false,
306 isNeedFold: this.changelogArray?.length > 1,387 isNeedFold: this.changelogArray?.length > 1,
@@ -315,23 +396,49 @@ struct NewVersion {
315 }.flexGrow(1)396 }.flexGrow(1)
316 397 
317 Column() {398 Column() {
318- Button() {399+ if (this.updateStatus === UpdateState.DOWNLOADING) {
319- Text(this.buttonText)400+ Stack({ alignContent: Alignment.Center }) {
320- .fontSize($r('app.float.text_size_btn')).fontColor(Color.White).fontWeight(FontWeight.Medium)401+ Progress({
321- .margin({ left: $r('app.float.custom_button_text_margin_left'),402+ value: this.downloadProgress,
322- right: $r('app.float.custom_button_text_margin_right') })403+ total: 100,
323- }404+ type: ProgressType.Capsule
324- .type(ButtonType.Capsule)405+ })
325- .constraintSize({ minWidth: $r('app.float.custom_button_width') })406+ .width($r('app.float.custom_button_width'))
326- .height($r('app.float.custom_button_height'))407+ .height($r('app.float.custom_button_height'))
327- .backgroundColor($r('app.color.blue'))408+ .color($r('app.color.blue'))
328- .opacity(this.isButtonEnable ? 1 : 0.4)409+ .backgroundColor($r('app.color.white'))
329- .visibility(this.isButtonVisible ? Visibility.Visible : Visibility.None)410+ Text(`下载中 ${this.getDisplayProgress(this.downloadProgress)} %`)
330- .onClick(() => {411+ .fontSize($r('app.float.text_size_btn'))
331- if(this.isButtonEnable) {412+ .fontColor($r('app.color.black'))
332- this.handleButtonClick();413+ .fontWeight(FontWeight.Medium)
333 }414 }
334- })415+ .width($r('app.float.custom_button_width'))
416+ .height($r('app.float.custom_button_height'))
417+ .visibility(this.isButtonVisible ? Visibility.Visible : Visibility.None)
418+ .onClick(() => {
419+ if (this.isButtonEnable) {
420+ this.handleButtonClick();
421+ }
422+ })
423+ } else {
424+ Button() {
425+ Text(this.buttonText)
426+ .fontSize($r('app.float.text_size_btn')).fontColor(Color.White).fontWeight(FontWeight.Medium)
427+ .margin({ left: $r('app.float.custom_button_text_margin_left'),
428+ right: $r('app.float.custom_button_text_margin_right') })
429+ }
430+ .type(ButtonType.Capsule)
431+ .constraintSize({ minWidth: $r('app.float.custom_button_width') })
432+ .height($r('app.float.custom_button_height'))
433+ .backgroundColor($r('app.color.blue'))
434+ .opacity(this.isButtonEnable ? 1 : 0.4)
435+ .visibility(this.isButtonVisible ? Visibility.Visible : Visibility.None)
436+ .onClick(() => {
437+ if(this.isButtonEnable) {
438+ this.handleButtonClick();
439+ }
440+ })
441+ }
335 }442 }
336 .flexShrink(0)443 .flexShrink(0)
337 .padding({444 .padding({
@@ -341,9 +448,17 @@ struct NewVersion {
341 }448 }
342 .width('100%')449 .width('100%')
343 .height('100%')450 .height('100%')
451+ .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
344 .backgroundColor($r('app.color.page_background'))452 .backgroundColor($r('app.color.page_background'))
345 }453 }
346 454 
455+ private getDisplayProgress(progress: number): number {
456+ if (isNaN(progress)) {
457+ return 0;
458+ }
459+ return Number((progress).toFixed(0));
460+ }
461+ 
347 private log(message: string): void {462 private log(message: string): void {
348 LogUtils.log('NewVersion', message);463 LogUtils.log('NewVersion', message);
349 }464 }
@@ -373,4 +488,4 @@ struct NewVersion {
373 this.restartDialogController.open();488 this.restartDialogController.open();
374 }489 }
375 }490 }
376-}491+}
@@ -0,0 +1,100 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import { SettingOptions } from '@ohos/common/src/main/ets/component/SettingOptions'
17+import { TitleBar } from '@ohos/common/src/main/ets/component/TitleBar';
18+import { NIGHT_UPGRADE_START, NIGHT_UPGRADE_END } from '@ohos/common/src/main/ets/const/update_const';
19+import { FormatUtils } from '@ohos/common/src/main/ets/util/FormatUtils';
20+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
21+import { SettingsViewModel } from '../viewmodel/settingsViewModel';
22+ 
23+const TAG = 'SettingsView'
24+ 
25+/**
26+ * 更新选项
27+ *
28+ * @since 2026-04-13
29+ */
30+@Entry
31+@Component
32+struct SettingsView {
33+ @State private viewModel: SettingsViewModel = new SettingsViewModel();
34+ 
35+ aboutToAppear(): void {
36+ LogUtils.info(TAG, 'aboutToAppear SettingsView');
37+ globalThis.newVersionThis = this;
38+ }
39+ 
40+ onPageShow(): void {
41+ LogUtils.info(TAG, 'onPageShow SettingsView');
42+ globalThis.currentPage = 'pages/settingsView';
43+ this.viewModel.initUpdatePolicy();
44+ }
45+ 
46+ onPageHide(): void {
47+ LogUtils.info(TAG, 'onPageHide SettingsView');
48+ }
49+ 
50+ onBackPress(): boolean | void {
51+ LogUtils.info(TAG, 'onBackPress SettingsView');
52+ }
53+ 
54+ build() {
55+ NavDestination() {
56+ this.pageContent();
57+ }
58+ .hideTitleBar(true)
59+ .onShown((): void => this.onPageShow())
60+ .id(`${TAG}_NavDestination`)
61+ }
62+ 
63+ @Builder
64+ pageContent() {
65+ Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {
66+ TitleBar({
67+ title: $r('app.string.local_settings')
68+ })
69+ 
70+ Scroll() {
71+ Column({ space: '24vp' }) {
72+ SettingOptions({
73+ title: $r('app.string.setting_auto_download_title'),
74+ description: $r('app.string.setting_auto_download_description'),
75+ toggleChange: (isOn: boolean) => {
76+ this.viewModel.autoDownloadToggleChange(isOn)
77+ },
78+ isOn: this.viewModel.isAutoDownloadOpen()
79+ })
80+ 
81+ SettingOptions({
82+ title: $r('app.string.setting_auto_update_title'),
83+ description: $r('app.string.setting_auto_update_description',
84+ FormatUtils.getDateRangeFormat(NIGHT_UPGRADE_START, NIGHT_UPGRADE_END)),
85+ toggleChange: (isOn: boolean) => {
86+ this.viewModel.nightUpgradeToggleChange(isOn);
87+ },
88+ isOn: this.viewModel.isAutoUpgradeOpen()
89+ })
90+ }
91+ .padding({
92+ top: '16vp'
93+ })
94+ .id(`${TAG}_pageContent_Column`)
95+ }
96+ }
97+ .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
98+ .backgroundColor($r('app.color.page_background'))
99+ }
100+}
@@ -0,0 +1,94 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import { LogUtils } from '@ohos/common/src/main/ets/util/LogUtils';
17+import { OtaUpdateManager } from 'ota/src/main/ets/manager/OtaUpdateManager';
18+import update from '@ohos.update';
19+import { JSON } from '@kit.ArkTS';
20+ 
21+const TAG = 'SettingsViewModel';
22+ 
23+/**
24+ * 设置页面视图控制
25+ *
26+ * @since 2026-04-13
27+ */
28+export class SettingsViewModel {
29+ private isAutoDownload: boolean = false;
30+ private isAutoUpgrade: boolean = false;
31+ 
32+ constructor() {
33+ }
34+ 
35+ isAutoDownloadOpen(): boolean {
36+ LogUtils.info(TAG, `isAutoDownloadOpen: ${this.isAutoDownload}`);
37+ return this.isAutoDownload;
38+ }
39+ 
40+ isAutoUpgradeOpen(): boolean {
41+ LogUtils.info(TAG, `isAutoUpgradeOpen: ${this.isAutoUpgrade}`);
42+ return this.isAutoUpgrade;
43+ }
44+ 
45+ async initUpdatePolicy(): Promise<void> {
46+ OtaUpdateManager.getInstance().getUpdatePolicy().then((upgradePolicy: update.UpgradePolicy) => {
47+ LogUtils.info(TAG, `initUpdatePolicy: ${JSON.stringify(upgradePolicy)}`);
48+ if (!upgradePolicy.downloadStrategy && upgradePolicy.autoUpgradeStrategy) {
49+ // 不能出现夜间安装打开,自动下载关闭的情况
50+ upgradePolicy.downloadStrategy = false;
51+ upgradePolicy.autoUpgradeStrategy = false;
52+ OtaUpdateManager.getInstance().setUpdatePolicy(upgradePolicy);
53+ }
54+ this.refreshSwitches(upgradePolicy);
55+ });
56+ }
57+ 
58+ async autoDownloadToggleChange(isOn: boolean): Promise<void> {
59+ LogUtils.info(TAG, `autoDownloadToggleChange isOn: ${isOn}`);
60+ let upgradePolicy = await OtaUpdateManager.getInstance().getUpdatePolicy();
61+ if (isOn) {
62+ this.isAutoDownload = true;
63+ upgradePolicy.downloadStrategy = true;
64+ } else {
65+ this.isAutoDownload = false;
66+ this.isAutoUpgrade = false;
67+ upgradePolicy.downloadStrategy = false;
68+ // 关闭自动下载,夜间安装也要随之关闭
69+ upgradePolicy.autoUpgradeStrategy = false;
70+ }
71+ OtaUpdateManager.getInstance().setUpdatePolicy(upgradePolicy);
72+ }
73+ 
74+ async nightUpgradeToggleChange(isOn: boolean): Promise<void> {
75+ LogUtils.info(TAG, `nightUpgradeToggleChange isOn: ${isOn}`);
76+ let upgradePolicy = await OtaUpdateManager.getInstance().getUpdatePolicy();
77+ if (isOn) {
78+ this.isAutoUpgrade = true;
79+ this.isAutoDownload = true;
80+ upgradePolicy.autoUpgradeStrategy = true;
81+ // 打开夜间升级,自动下载也要随之打开
82+ upgradePolicy.downloadStrategy = true;
83+ } else {
84+ this.isAutoUpgrade = false;
85+ upgradePolicy.autoUpgradeStrategy = false;
86+ }
87+ OtaUpdateManager.getInstance().setUpdatePolicy(upgradePolicy);
88+ }
89+ 
90+ refreshSwitches(upgradePolicy: update.UpgradePolicy): void {
91+ this.isAutoDownload = upgradePolicy.downloadStrategy;
92+ this.isAutoUpgrade = upgradePolicy.autoUpgradeStrategy;
93+ }
94+}
@@ -68,6 +68,13 @@
68 "type": "service",68 "type": "service",
69 "visible": true,69 "visible": true,
70 "srcEntrance": "./ets/ServiceExtAbility/service.ets"70 "srcEntrance": "./ets/ServiceExtAbility/service.ets"
71+ },
72+ {
73+ "name": "UpdateSystemDialogAbility",
74+ "srcEntry": "./ets/UpdateSystemDialog/UpdateSystemDialogAbility.ets",
75+ "description": "Software update system dialog",
76+ "type": "sysDialog/common",
77+ "exported": true
71 }78 }
72 ],79 ],
73 "requestPermissions": [80 "requestPermissions": [
@@ -79,7 +86,13 @@
79 },86 },
80 {87 {
81 "name": "ohos.permission.START_ABILITIES_FROM_BACKGROUND"88 "name": "ohos.permission.START_ABILITIES_FROM_BACKGROUND"
89+ },
90+ {
91+ "name": "ohos.permission.MANAGE_SECURE_SETTINGS"
92+ },
93+ {
94+ "name": "ohos.permission.START_SYSTEM_DIALOG"
82 }95 }
83 ]96 ]
84 }97 }
85-}98+}
@@ -7,6 +7,14 @@
7 {7 {
8 "name": "white",8 "name": "white",
9 "value": "#FFFFFF"9 "value": "#FFFFFF"
10+ },
11+ {
12+ "name": "black",
13+ "value": "#000000"
14+ },
15+ {
16+ "name": "deep_blue",
17+ "value": "#0A59F7"
10 }18 }
11 ]19 ]
12}20}
@@ -3,6 +3,10 @@
3 {3 {
4 "name": "card_border_radius",4 "name": "card_border_radius",
5 "value": "24vp"5 "value": "24vp"
6+ },
7+ {
8+ "name": "index_center_image_width",
9+ "value": "150vp"
6 }10 }
7 ]11 ]
8}12}
@@ -2,6 +2,8 @@
2 "src": [2 "src": [
3 "pages/index",3 "pages/index",
4 "pages/newVersion",4 "pages/newVersion",
5- "pages/currentVersion"5+ "pages/currentVersion",
6+ "pages/settingsView",
7+ "UpdateSystemDialog/pages/UpdateSystemDialogPage"
6 ]8 ]
7}9}
@@ -0,0 +1,29 @@
1+-----BEGIN CERTIFICATE-----
2+MIICNDCCAbegAwIBAgIEPkDWrTAMBggqhkjOPQQDAwUAMGMxCzAJBgNVBAYTAkNO
3+MRQwEgYDVQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVh
4+bTEjMCEGA1UEAxMaT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gQ0EwHhcNMjIxMDA5
5+MDEzMTA0WhcNMzIxMDA2MDEzMTA0WjBoMQswCQYDVQQGEwJDTjEUMBIGA1UEChML
6+T3Blbkhhcm1vbnkxGTAXBgNVBAsTEE9wZW5IYXJtb255IFRlYW0xKDAmBgNVBAMT
7+H09wZW5IYXJtb255IEFwcGxpY2F0aW9uIFJlbGVhc2UwWTATBgcqhkjOPQIBBggq
8+hkjOPQMBBwNCAATbYOCQQpW5fdkYHN45v0X3AHax12jPBdEDosFRIZ1eXmxOYzSG
9+JwMfsHhUU90E8lI0TXYZnNmgM1sovubeQqATo1IwUDAdBgNVHQ4EFgQU+3Gu6GVx
10+EFCwl3Bxmou72g1RWBowDgYDVR0PAQH/BAQDAgeAMB8GA1UdIwQYMBaAFNuGtyIW
11+1QuhS7fdJXu58QV9oi1HMAwGCCqGSM49BAMDBQADaQAwZgIxAI6oQLYENgt6sA9G
12+IVf4eRg3feITJxZX2QvVv2YONy99gX+WW2ZDLtNzgx3ks5HrwQIxANewK7wVFlJx
13+5Xdn8Ao0pOkdyBRpRnPL6FUSANqY+pbdUvEo88R0rXc/cR84WkQYBw==
14+-----END CERTIFICATE-----
15+-----BEGIN CERTIFICATE-----
16+MIICYTCCAeWgAwIBAgIEHmXAPTAMBggqhkjOPQQDAwUAMGgxCzAJBgNVBAYTAkNO
17+MRQwEgYDVQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVh
18+bTEoMCYGA1UEAxMfT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gUm9vdCBDQTAeFw0y
19+MTAyMDIxMjE1MzJaFw00OTEyMzExMjE1MzJaMGMxCzAJBgNVBAYTAkNOMRQwEgYD
20+VQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVhbTEjMCEG
21+A1UEAxMaT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gQ0EwdjAQBgcqhkjOPQIBBgUr
22+gQQAIgNiAAQhnu7Hna8XNa2KyqRf5+lBJScE4xqf89N0g0OuqAb2re8nGsvWkw26
23+uDekfnBYicd+G3Cydqa2zFIwV7Talyg2ULW3r8KbGpyl84mJEPPRmCGJ+H9gtCsf
24++OrJ4Y76LVWjYzBhMB8GA1UdIwQYMBaAFBc6EKGrGXzlAE+s0Zgnsphadw7NMA8G
25+A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTbhrciFtUL
26+oUu33SV7ufEFfaItRzAMBggqhkjOPQQDAwUAA2gAMGUCMG3cXjiDmXTvf7D4Omhf
27+qcc2nuO+EMfWE+N9ZhBP5UhV34mAGWi3SfLU6rcV0urWEQIxAMYIb3epOnKhUrcm
28+Lfu1WKzFlpYQwmw73RaCHP2I3k6NcuWOYeNwWXSNZ8o0nzvaLg==
29+-----END CERTIFICATE-----
@@ -0,0 +1,10 @@
1+-----BEGIN NEW CERTIFICATE REQUEST-----
2+MIIBVTCB+gIBADBoMQswCQYDVQQGEwJDTjEUMBIGA1UEChMLT3Blbkhhcm1vbnkx
3+GTAXBgNVBAsTEE9wZW5IYXJtb255IFRlYW0xKDAmBgNVBAMTH09wZW5IYXJtb255
4+IEFwcGxpY2F0aW9uIFJlbGVhc2UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATb
5+YOCQQpW5fdkYHN45v0X3AHax12jPBdEDosFRIZ1eXmxOYzSGJwMfsHhUU90E8lI0
6+TXYZnNmgM1sovubeQqAToDAwLgYJKoZIhvcNAQkOMSEwHzAdBgNVHQ4EFgQU+3Gu
7+6GVxEFCwl3Bxmou72g1RWBowDAYIKoZIzj0EAwIFAANIADBFAiEApsgx1bKHVCIU
8+c9aKaImMUWk7oYSDbBDSYTlWbIZrOvMCIFtvD32iKSc5/abTA3u8PdWpL3k7hQab
9+ChPtge/7IHDb
10+-----END NEW CERTIFICATE REQUEST-----
Binary files do not support preview
Binary files do not support preview