openUrlPreference.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /** 本地存储:为 true 时用系统浏览器打开链接,否则用内置 WebView */
  2. export const OPEN_URL_IN_SYSTEM_BROWSER_KEY = 'open_url_in_system_browser'
  3. /** 是否已做过「首次启动弱机默认」检测(全应用仅一次) */
  4. export const OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY = 'open_url_device_default_applied'
  5. /** 用户在「应用设置」里手动改过开关后为 true,不再自动覆盖 */
  6. export const OPEN_URL_USER_EXPLICIT_SET_KEY = 'open_url_user_explicit_set'
  7. /** 首次/刷新时写入的检测结果(JSON) */
  8. export const OPEN_URL_DEVICE_SNAPSHOT_KEY = 'open_url_device_snapshot'
  9. /** Android 12 对应 API 31 */
  10. const ANDROID_12_API_LEVEL = 31
  11. /** 物理内存低于此值(GB)视为弱机,默认用系统浏览器打开链接 */
  12. const WEAK_DEVICE_RAM_GB = 8
  13. /**
  14. * Java Long / 任意桥接值为字节数
  15. * @param {*} v
  16. * @returns {number}
  17. */
  18. function javaLongishToNumber(v) {
  19. if (v == null || v === undefined) return 0
  20. if (typeof v === 'number' && Number.isFinite(v)) return v
  21. if (typeof v === 'string') {
  22. const n = parseFloat(v)
  23. return Number.isFinite(n) ? n : 0
  24. }
  25. try {
  26. if (typeof plus !== 'undefined' && plus.android && plus.android.invoke) {
  27. const lv = plus.android.invoke(v, 'longValue')
  28. if (typeof lv === 'number' && Number.isFinite(lv)) return lv
  29. }
  30. } catch (e) {}
  31. try {
  32. const n = Number(v)
  33. return Number.isFinite(n) ? n : 0
  34. } catch (e2) {}
  35. return 0
  36. }
  37. /**
  38. * 读取 /proc/meminfo 中 MemTotal(kB)换算为字节(多数机型可用,作 ActivityManager 失败时的回退)
  39. * @returns {number}
  40. */
  41. function readMemTotalBytesFromProc() {
  42. // #ifdef APP-PLUS
  43. let br = null
  44. try {
  45. const FileReader = plus.android.importClass('java.io.FileReader')
  46. const BufferedReader = plus.android.importClass('java.io.BufferedReader')
  47. const fr = new FileReader('/proc/meminfo')
  48. br = new BufferedReader(fr)
  49. let line = br.readLine()
  50. let guard = 0
  51. while (line != null && guard < 400) {
  52. guard++
  53. const s = String(line)
  54. if (s.indexOf('MemTotal:') === 0) {
  55. const parts = s.trim().split(/\s+/)
  56. const kb = parseInt(parts[1], 10)
  57. if (Number.isFinite(kb) && kb > 0) return kb * 1024
  58. return 0
  59. }
  60. line = br.readLine()
  61. }
  62. } catch (e) {
  63. } finally {
  64. try {
  65. if (br) br.close()
  66. } catch (e2) {}
  67. }
  68. // #endif
  69. return 0
  70. }
  71. /**
  72. * 物理内存总字节数(Android App)
  73. * @returns {{ bytes: number, source: string }}
  74. */
  75. function readAndroidTotalRamBytes() {
  76. const empty = { bytes: 0, source: '' }
  77. // #ifdef APP-PLUS
  78. try {
  79. const main = plus.android.runtimeMainActivity()
  80. const ActivityManager = plus.android.importClass('android.app.ActivityManager')
  81. const Context = plus.android.importClass('android.content.Context')
  82. const MemoryInfo = plus.android.importClass('android.app.ActivityManager$MemoryInfo')
  83. const am = main.getSystemService(Context.ACTIVITY_SERVICE)
  84. const memInfo = new MemoryInfo()
  85. try {
  86. plus.android.invoke(am, 'getMemoryInfo', memInfo)
  87. } catch (e) {
  88. try {
  89. am.getMemoryInfo(memInfo)
  90. } catch (e2) {}
  91. }
  92. let raw = null
  93. try {
  94. raw = plus.android.invoke(memInfo, 'totalMem')
  95. } catch (e) {}
  96. if (raw == null || raw === undefined) {
  97. try {
  98. raw = memInfo.totalMem
  99. } catch (e2) {}
  100. }
  101. const fromAm = javaLongishToNumber(raw)
  102. if (fromAm > 0) return { bytes: fromAm, source: 'ActivityManager' }
  103. const fromProc = readMemTotalBytesFromProc()
  104. if (fromProc > 0) return { bytes: fromProc, source: '/proc/meminfo' }
  105. return empty
  106. } catch (e) {
  107. const fromProc = readMemTotalBytesFromProc()
  108. if (fromProc > 0) return { bytes: fromProc, source: '/proc/meminfo' }
  109. return empty
  110. }
  111. // #endif
  112. return empty
  113. }
  114. /**
  115. * 读取 Android 设备信息(App 端)。用于快照与弱机判断。
  116. * @returns {object|null}
  117. */
  118. function collectAndroidSnapshot() {
  119. // #ifdef APP-PLUS
  120. try {
  121. if (uni.getSystemInfoSync().platform !== 'android') return null
  122. const out = {
  123. platform: 'android',
  124. at: Date.now(),
  125. androidSdkInt: null,
  126. androidRelease: '',
  127. totalRamBytes: null,
  128. totalRamGb: null,
  129. ramSource: '',
  130. weakOs: false,
  131. weakRam: false,
  132. weakDevice: false
  133. }
  134. const VERSION = plus.android.importClass('android.os.Build$VERSION')
  135. out.androidSdkInt = Number(VERSION.SDK_INT)
  136. out.androidRelease = String(VERSION.RELEASE || '')
  137. out.weakOs = out.androidSdkInt < ANDROID_12_API_LEVEL
  138. const ram = readAndroidTotalRamBytes()
  139. if (ram.bytes > 0) {
  140. out.totalRamBytes = ram.bytes
  141. out.totalRamGb = Math.round((ram.bytes / (1024 * 1024 * 1024)) * 100) / 100
  142. out.weakRam = out.totalRamGb < WEAK_DEVICE_RAM_GB
  143. out.ramSource = ram.source || ''
  144. }
  145. out.weakDevice = !!(out.weakOs || out.weakRam)
  146. return out
  147. } catch (e) {
  148. return {
  149. platform: 'android',
  150. at: Date.now(),
  151. error: String((e && e.message) || e)
  152. }
  153. }
  154. // #endif
  155. return null
  156. }
  157. function persistSnapshot(snap) {
  158. if (!snap) return
  159. try {
  160. uni.setStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY, JSON.stringify(snap))
  161. } catch (e) {}
  162. }
  163. /**
  164. * 供应用设置页展示多行说明
  165. * @param {object|null} s getOpenUrlDeviceSnapshot() 返回值
  166. * @returns {string[]}
  167. */
  168. export function formatOpenUrlDeviceSnapshotLines(s) {
  169. if (!s || typeof s !== 'object') return []
  170. if (s.platform === 'android') {
  171. const lines = ['本机检测(已保存;进入本页会刷新内存与系统信息)']
  172. if (s.androidSdkInt != null) {
  173. lines.push(`Android API:${s.androidSdkInt}(低于 31 视为系统版本偏低)`)
  174. }
  175. lines.push(`Android 版本:${s.androidRelease || '—'}`)
  176. if (s.totalRamGb != null) {
  177. const src = s.ramSource ? `,来源:${s.ramSource}` : ''
  178. lines.push(`物理内存:约 ${s.totalRamGb} GB(低于 8 GB 视为内存偏低)${src}`)
  179. } else {
  180. lines.push('物理内存:未能读取(ActivityManager 与 /proc/meminfo 均失败)')
  181. }
  182. lines.push(`弱机判定:${s.weakDevice ? '是' : '否'}(系统偏低或内存偏低)`)
  183. if (s.error) lines.push(`读取异常:${s.error}`)
  184. return lines
  185. }
  186. if (s.note) return [String(s.note)]
  187. return []
  188. }
  189. /**
  190. * 进入应用设置等页面时刷新并保存检测快照(不改变首次默认逻辑)。
  191. */
  192. export function refreshAndroidOpenUrlDeviceSnapshot() {
  193. // #ifdef APP-PLUS
  194. const snap = collectAndroidSnapshot()
  195. persistSnapshot(snap)
  196. // #endif
  197. }
  198. /**
  199. * @returns {object|null}
  200. */
  201. export function getOpenUrlDeviceSnapshot() {
  202. try {
  203. const raw = uni.getStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY)
  204. if (raw == null || raw === '') return null
  205. if (typeof raw === 'object') return raw
  206. return JSON.parse(String(raw))
  207. } catch (e) {
  208. return null
  209. }
  210. }
  211. function userHasExplicitOpenUrlPreference() {
  212. try {
  213. return !!uni.getStorageSync(OPEN_URL_USER_EXPLICIT_SET_KEY)
  214. } catch (e) {
  215. return false
  216. }
  217. }
  218. /**
  219. * 应用首次启动执行一次(见 App.vue onLaunch):
  220. * 仅 Android:系统版本低于 12 或物理内存低于 8GB 时,若用户未在设置里手动改过,则默认开启「用手机浏览器打开」。
  221. */
  222. export function maybeApplyAndroidWeakDeviceOpenUrlDefault() {
  223. // #ifdef APP-PLUS
  224. try {
  225. if (uni.getStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY)) return
  226. const platform = uni.getSystemInfoSync().platform
  227. if (platform !== 'android') {
  228. try {
  229. uni.setStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY, JSON.stringify({
  230. platform,
  231. at: Date.now(),
  232. note: '弱机默认仅 Android 检测'
  233. }))
  234. } catch (e) {}
  235. uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
  236. return
  237. }
  238. const snap = collectAndroidSnapshot()
  239. persistSnapshot(snap)
  240. const weak =
  241. snap &&
  242. !snap.error &&
  243. (snap.weakOs || snap.weakRam)
  244. if (weak && !userHasExplicitOpenUrlPreference()) {
  245. setOpenUrlInSystemBrowser(true)
  246. }
  247. uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
  248. } catch (e) {
  249. try {
  250. uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
  251. } catch (e2) {}
  252. }
  253. // #endif
  254. }
  255. export function getOpenUrlInSystemBrowser() {
  256. try {
  257. const v = uni.getStorageSync(OPEN_URL_IN_SYSTEM_BROWSER_KEY)
  258. return v === true || v === 'true' || v === 1
  259. } catch (e) {
  260. return false
  261. }
  262. }
  263. /**
  264. * @param {boolean} value
  265. * @param {{ fromUser?: boolean }} [options] fromUser 为 true 时表示用户在应用设置中操作,之后不再被弱机默认覆盖
  266. */
  267. export function setOpenUrlInSystemBrowser(value, options) {
  268. try {
  269. uni.setStorageSync(OPEN_URL_IN_SYSTEM_BROWSER_KEY, !!value)
  270. if (options && options.fromUser) {
  271. uni.setStorageSync(OPEN_URL_USER_EXPLICIT_SET_KEY, true)
  272. }
  273. } catch (e) {}
  274. }
  275. /**
  276. * 按用户偏好:系统浏览器或内置 WebView。
  277. * @param {string} url
  278. * @param {string} [title] 内置 WebView 标题
  279. */
  280. export function openEmbeddedOrSystemBrowser(url, title) {
  281. const u = String(url || '').trim()
  282. if (!u) return
  283. const encTitle = encodeURIComponent(title || '页面')
  284. const pageUrl =
  285. '/pages/webview/index?url=' + encodeURIComponent(u) + '&title=' + encTitle
  286. if (getOpenUrlInSystemBrowser()) {
  287. // #ifdef APP-PLUS
  288. try {
  289. if (typeof plus !== 'undefined' && plus.runtime && typeof plus.runtime.openURL === 'function') {
  290. plus.runtime.openURL(u)
  291. return
  292. }
  293. } catch (e) {}
  294. // #endif
  295. // #ifdef H5
  296. if (typeof window !== 'undefined' && typeof window.open === 'function') {
  297. window.open(u, '_blank')
  298. return
  299. }
  300. // #endif
  301. }
  302. uni.navigateTo({ url: pageUrl })
  303. }