Files
jinzhi/lib/features/settings/data/update_api_client.dart
T

154 lines
4.0 KiB
Dart

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<int> 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),
);
}
}
// BLOCKED(proto): this endpoint has no authoritative generated Upgrade proto.
// Keep its existing wire contract until that source becomes available.
class UpdateApiClient {
UpdateApiClient(this._dio);
final Dio _dio;
Future<UpgradeInfo> checkUpgrade() async {
final response = await _dio.post<List<int>>(
'/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: protobufOptions(),
);
return UpgradeInfo.fromProtobuf(response.data ?? const <int>[]);
}
Future<void> reportClick({
required String buildId,
required String source,
}) async {
await _dio.post<List<int>>(
'/active/upgrade/click',
data: _ProtoWriter()
.string(1, buildId)
.string(2, source)
.toBytes(), // jinzhi.v1.UpgradeClickRequest
options: protobufOptions(),
);
}
}
final updateApiClientProvider = Provider<UpdateApiClient>(
(ref) => UpdateApiClient(ref.watch(dioProvider)),
);
class _ProtoFields {
final _values = <int, Object>{};
int integer(int tag) => _values[tag] as int? ?? 0;
String string(int tag) => _values[tag] as String? ?? '';
}
class _ProtoReader {
_ProtoReader(this._bytes);
final List<int> _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);
}
}