import 'dart:convert'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/network/dio_provider.dart'; /// 金值 `jinzhi.v1.UpgradeReply` 的 HTTP protobuf 契约。 class UpgradeInfo { const UpgradeInfo({ required this.show, required this.force, required this.message, required this.url, required this.isLatestVersion, required this.latestVersion, required this.versionCode, required this.hash, }); final int show; final int force; final String message; final String url; final int isLatestVersion; final String latestVersion; final String versionCode; final String hash; bool get hasPublishedUpdate => isLatestVersion != 1 && url.trim().isNotEmpty && (latestVersion.trim().isNotEmpty || versionCode.trim().isNotEmpty); factory UpgradeInfo.fromProtobuf(List bytes) { final fields = _ProtoReader(bytes).readFields(); return UpgradeInfo( show: fields.integer(1), force: fields.integer(2), message: fields.string(3), url: fields.string(4), isLatestVersion: fields.integer(5), latestVersion: fields.string(6), versionCode: fields.string(7), hash: fields.string(8), ); } } class UpdateApiClient { UpdateApiClient( this._dio, { required String apiPathPrefix, required String updatePackageName, }) : _apiPathPrefix = _normalizePrefix(apiPathPrefix), _updatePackageName = updatePackageName.trim(); final Dio _dio; final String _apiPathPrefix; final String _updatePackageName; Options get _options { final options = protobufOptions(); if (_updatePackageName.isEmpty) return options; return options.copyWith( headers: {...?options.headers, 'pkg': _updatePackageName}, ); } Future checkUpgrade() async { final response = await _dio.post>( _path('/sys/upgrade'), // Uint8List ensures Dio sends the empty protobuf as 0 bytes instead of // JSON-encoding an empty Dart List as `[]` (Content-Length: 2). data: Uint8List(0), // jinzhi.v1.GetUpgradeRequest options: _options, ); return UpgradeInfo.fromProtobuf(response.data ?? const []); } Future reportClick({ required String buildId, required String source, }) async { await _dio.post>( _path('/active/upgrade/click'), data: _ProtoWriter() .string(1, buildId) .string(2, source) .toBytes(), // jinzhi.v1.UpgradeClickRequest options: _options, ); } String _path(String endpoint) => '$_apiPathPrefix$endpoint'; static String _normalizePrefix(String value) { final trimmed = value.trim(); if (trimmed.isEmpty || trimmed == '/') return ''; final withLeadingSlash = trimmed.startsWith('/') ? trimmed : '/$trimmed'; return withLeadingSlash.endsWith('/') ? withLeadingSlash.substring(0, withLeadingSlash.length - 1) : withLeadingSlash; } } final updateApiClientProvider = Provider( (ref) => UpdateApiClient( ref.watch(dioProvider), apiPathPrefix: ref.watch(appConfigProvider).apiPathPrefix, updatePackageName: ref.watch(appConfigProvider).updatePackageName, ), ); class _ProtoFields { final _values = {}; int integer(int tag) => _values[tag] as int? ?? 0; String string(int tag) => _values[tag] as String? ?? ''; } class _ProtoReader { _ProtoReader(this._bytes); final List _bytes; var _index = 0; _ProtoFields readFields() { final result = _ProtoFields(); while (_index < _bytes.length) { final key = _varint(); final tag = key >> 3; switch (key & 7) { case 0: result._values[tag] = _varint(); case 2: final length = _varint(); final end = _index + length; if (end > _bytes.length) { throw const FormatException('Invalid protobuf'); } result._values[tag] = utf8.decode(_bytes.sublist(_index, end)); _index = end; default: throw const FormatException('Unsupported protobuf wire type'); } } return result; } int _varint() { var value = 0; var shift = 0; while (_index < _bytes.length && shift < 64) { final byte = _bytes[_index++]; value |= (byte & 0x7f) << shift; if (byte < 0x80) return value; shift += 7; } throw const FormatException('Invalid protobuf varint'); } } class _ProtoWriter { final _bytes = BytesBuilder(copy: false); _ProtoWriter string(int tag, String value) { final bytes = utf8.encode(value); _varint((tag << 3) | 2); _varint(bytes.length); _bytes.add(bytes); return this; } Uint8List toBytes() => _bytes.toBytes(); void _varint(int value) { while (value > 0x7f) { _bytes.addByte((value & 0x7f) | 0x80); value >>= 7; } _bytes.addByte(value); } }