-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.js
More file actions
289 lines (266 loc) · 11.5 KB
/
Copy pathapp.js
File metadata and controls
289 lines (266 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import './GrantPermission/device-polyfill.js'
import { MessageBuilder } from './GrantPermission/message.js'
import { getPackageInfo, requestPermission } from '@zos/app'
import * as ble from '@zos/ble'
import { log } from '@zos/utils'
import WidgetInterceptor from './lib/core/widgetInterceptor.js'
import PageInterceptor from './lib/core/pageInterceptor.js'
import ScreenReader from './lib/core/screenReader.js'
import ShortcutHandler from './lib/interaction/shortcut.js'
import GestureHandler from './lib/interaction/gesture.js'
import VoiceControlService from './lib/core/voiceControlService.js'
import AccessibilityService from './lib/core/accessibility.js'
import { loadSettings } from './lib/core/config.js'
import ErrorMonitor from './lib/utils/errorMonitor.js'
import { ServerConfig, saveSettings } from './lib/core/config.js'
import BrailleService from './lib/core/braille.js'
import SpeechHistory from './lib/utils/speechHistory.js'
// Side-effect import: ContextMenu (and, transitively, BrailleKeyboard) is a
// self-registering singleton (sets globalThis.ContextMenuInstance at
// module scope) - but nothing was ever importing this file, so that
// registration line never ran and globalThis.ContextMenuInstance stayed
// undefined for the whole app lifetime. gesture.js's LONG_PRESS handler
// and the "Open Context Menu" gesture action both silently no-op via
// optional chaining (`globalThis.ContextMenuInstance?.show()`) when this
// isn't loaded, so the entire context menu feature was unreachable dead
// code despite the menu logic itself being correct.
import './lib/components/contextMenu.js'
// Initialize interceptors as early as possible
WidgetInterceptor.init()
PageInterceptor.init()
App({
globalData: {
messageBuilder: null
},
async onCreate() {
log.info('app on create invoke')
// Initialize globalData if it doesn't exist (some versions of Zepp OS)
if (!this.globalData) {
this.globalData = {}
}
// Load configuration
try {
globalThis.ScreenReaderConfig = loadSettings()
} catch (e) {
log.error('Failed to load settings:', String(e))
globalThis.ScreenReaderConfig = {}
}
try {
// 1. Initialize core communications
const { appId } = getPackageInfo()
const messageBuilder = new MessageBuilder(
/** @type {any} */ ({
appId,
ble
})
)
this.globalData.messageBuilder = messageBuilder
messageBuilder.connect()
// 2. Grant permissions
this.grantPermissions()
// 3. Initialize Services
// "Reliability & Diagnostics" settings (settingsKeys
// 'autoRecoveryEnabled' / 'maxRecoveryAttempts' from
// setting/accessibilitySetting.js) are applied here: on init failure,
// retry with backoff instead of leaving the screen reader silently
// dead for the rest of the session.
await this.initScreenReaderWithRecovery()
ShortcutHandler.init()
GestureHandler.init()
VoiceControlService.init()
AccessibilityService.init()
// 4. Handle incoming messages
messageBuilder.on('request', async (ctx) => {
try {
const jsonRpc = messageBuilder.buf2Json(ctx.request.payload)
const { method, params = {} } = jsonRpc || {}
if (method === 'NOTIFICATION_RECEIVE') {
const {
title = '',
content = '',
appName = 'System',
type = 'push',
isPriority
} = params
const config = globalThis.ScreenReaderConfig || {}
// Per-category filters from setting/NotificationSetting.js -
// previously every notification was announced regardless of
// these toggles.
const typeEnabled = {
sms: config.notifySMS !== false,
call: config.notifyCalls !== false,
missed_call: config.notifyMissedCalls !== false,
push: config.notifyPush !== false
}
const shouldAnnounceType = typeEnabled[type] !== false
// "Priority Notifications" only restricts when explicitly
// enabled AND the incoming notification identifies itself as
// non-priority - a notification that doesn't report a
// priority flag at all still gets announced normally, so this
// can't silently swallow every notification just because the
// sender never set isPriority.
const passesPriorityFilter = config.notifyPriority !== true || isPriority !== false
if (shouldAnnounceType && passesPriorityFilter) {
let announcement = ''
if (type === 'sms') {
announcement = `New SMS from ${title}: ${content}`
} else if (type === 'call') {
const callerName = params.callerName || title || ''
const phoneNumber =
params.phoneNumber ||
params.callerNumber ||
(content !== callerName ? content : '')
if (callerName && phoneNumber && callerName !== phoneNumber) {
announcement = `Incoming call from ${callerName}, number ${phoneNumber}`
} else {
announcement = `Incoming call from ${callerName || phoneNumber || 'Unknown'}`
}
} else if (type === 'missed_call') {
announcement = `Missed call from ${title || 'Unknown'}`
} else {
announcement = `Notification from ${appName}: ${title}. ${content}`
}
await ScreenReader.speak(announcement, { priority: 'high', secondary: true })
}
} else if (method === 'SETTING_UPDATE') {
const { key, value } = params
if (key === 'clearHistorySignal') {
// "Clear History" button (setting/SpeechHistorySetting.js)
// previously just stored a timestamp nobody read.
SpeechHistory.clear()
} else if (key === 'brailleTable') {
// Previously the raw config value updated, but
// BrailleService's own internal `currentTable` (what
// translation actually uses) never synced with it.
BrailleService.setTable(value)
if (globalThis.ScreenReaderConfig) globalThis.ScreenReaderConfig[key] = value
} else if (key === 'resetSettingsSignal') {
// "Reset All Settings" button (setting/DeveloperSettings.js)
// previously just stored a timestamp nobody read.
const defaults = JSON.parse(JSON.stringify(ServerConfig))
globalThis.ScreenReaderConfig = defaults
saveSettings(defaults)
await ScreenReader.speak('All settings have been reset to defaults.', {
priority: 'high',
force: true
})
} else if (key === 'exportLogSignal') {
// "Export Debug Log" button - there's no file-sharing API
// exposed to a Mini Program's device-side code to actually
// write a shareable file, so this speaks a summary via TTS
// instead of silently doing nothing.
const stats = ErrorMonitor.getErrorStats()
await ScreenReader.speak(
`Debug log summary: ${stats.total} errors recorded, ${stats.recovered} recovered.`,
{ priority: 'high', force: true }
)
} else if (globalThis.ScreenReaderConfig && key !== undefined) {
globalThis.ScreenReaderConfig[key] = value
}
}
} catch (error) {
// A malformed/corrupted BLE payload (dropped bytes, mid-transfer
// disconnect, etc.) would otherwise throw inside JSON.parse and
// crash this handler as an unhandled rejection. Parsing now lives
// inside the try block so a single bad message can't take down
// message handling for the rest of the session.
log.error('App Message Receive Error:', String(error))
}
})
// Restore state and re-enable if it was on
if (globalThis.ScreenReaderConfig?.autoStart !== false) {
ScreenReader.enabled = true
// Delay greeting to ensure TTS is ready
setTimeout(() => {
ScreenReader.vibration.vibrate('success')
ScreenReader.speak('ZSR Started', { priority: 'high' })
}, 1500)
}
} catch (error) {
log.error('App onCreate fatal error:', String(error))
}
},
// onBoot is a Zepp OS lifecycle hook; the @zeppos/device-types package does
// not declare it in the App Option type. Suppress the type error here since
// removing it would break runtime behavior on devices that call onBoot.
// @ts-ignore - onBoot is a valid Zepp OS lifecycle hook not in the type package
onBoot() {
console.log('app on boot invoke')
},
async initScreenReaderWithRecovery() {
const config = globalThis.ScreenReaderConfig || {}
const autoRecoveryEnabled = config.autoRecoveryEnabled !== false
const maxAttempts = autoRecoveryEnabled ? Math.max(1, config.maxRecoveryAttempts || 3) : 1
/** @type {{ timestamp: number; error: any; stack: any; context: any; recovered: boolean; } | null} */
let lastErrorEntry = null
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await ScreenReader.init()
if (lastErrorEntry) {
ErrorMonitor.markAsRecovered(lastErrorEntry)
}
return
} catch (error) {
lastErrorEntry = ErrorMonitor.trackError(error, `ScreenReader.init (attempt ${attempt})`)
if (attempt >= maxAttempts) {
log.error(
`ScreenReader init failed after ${attempt} attempt(s), giving up:`,
String(error)
)
return
}
log.warn(
`ScreenReader init failed (attempt ${attempt}/${maxAttempts}), retrying...`,
String(error)
)
// Simple linear backoff so a fast-failing sensor/service doesn't
// spin the retry loop.
await new Promise((resolve) => setTimeout(resolve, attempt * 500))
}
}
},
grantPermissions() {
const permissions = [
'data:os.device.info',
'device:os.local_storage',
'device:os.vibration',
'device:os.sound',
'device:os.camera',
'device:os.ble',
'data:user.hd.heart_rate',
'data:user.hd.sleep',
'data:user.hd.spo2',
'data:user.hd.step',
'data:user.hd.stress',
'data:user.hd.calorie',
'data:user.hd.distance'
]
// @zos/app has no `permission` object - the real Zepp OS 2.0/3.0 API is
// the standalone requestPermission() function, and its result comes
// back through a single `callback` (not separate success/fail
// handlers). See
// https://docs.zepp.com/docs/reference/device-app-api/newAPI/app/requestPermission/
try {
requestPermission({
permissions,
callback: (...args) => {
console.log('Permission request result:', args)
}
})
} catch (_e) {
console.log(
'Standard permission request not supported or failed, proceeding with system defaults.'
)
}
},
onDestroy() {
console.log('app on destroy invoke')
// Use getApp() to access globalData — avoids the 'this' type mismatch
// in the @zeppos/device-types App Option definition which does not expose
// globalData on the lifecycle context.
const app = getApp()
if (app.globalData && app.globalData.messageBuilder) {
app.globalData.messageBuilder.disConnect()
}
}
})