@@ -7,6 +7,7 @@ import 'package:package_info_plus/package_info_plus.dart' as package_info_plus;
7
7
import 'package:url_launcher/url_launcher.dart' as url_launcher;
8
8
9
9
import '../host/android_notifications.dart' ;
10
+ import '../log.dart' ;
10
11
import '../widgets/store.dart' ;
11
12
import 'store.dart' ;
12
13
@@ -131,6 +132,12 @@ abstract class ZulipBinding {
131
132
132
133
/// Wraps the [AndroidNotificationHostApi] constructor.
133
134
AndroidNotificationHostApi get androidNotificationHost;
135
+
136
+ /// Generates a user agent header for HTTP requests.
137
+ ///
138
+ /// Uses [deviceInfo] to get operating system information
139
+ /// and [packageInfo] to get application version information.
140
+ Map <String , String > userAgentHeader ();
134
141
}
135
142
136
143
/// Like [device_info_plus.BaseDeviceInfo] , but without things we don't use.
@@ -140,13 +147,23 @@ abstract class BaseDeviceInfo {
140
147
141
148
/// Like [device_info_plus.AndroidDeviceInfo] , but without things we don't use.
142
149
class AndroidDeviceInfo extends BaseDeviceInfo {
150
+ /// The user-visible version string.
151
+ ///
152
+ /// E.g., "1.0" or "3.4b5" or "bananas". This field is an opaque string.
153
+ /// Do not assume that its value has any particular structure or that
154
+ /// values of RELEASE from different releases can be somehow ordered.
155
+ final String release;
156
+
143
157
/// The Android SDK version.
144
158
///
145
159
/// Possible values are defined in:
146
160
/// https://developer.android.com/reference/android/os/Build.VERSION_CODES.html
147
161
final int sdkInt;
148
162
149
- AndroidDeviceInfo ({required this .sdkInt});
163
+ AndroidDeviceInfo ({
164
+ required this .release,
165
+ required this .sdkInt,
166
+ });
150
167
}
151
168
152
169
/// Like [device_info_plus.IosDeviceInfo] , but without things we don't use.
@@ -159,6 +176,59 @@ class IosDeviceInfo extends BaseDeviceInfo {
159
176
IosDeviceInfo ({required this .systemVersion});
160
177
}
161
178
179
+ /// Like [device_info_plus.MacOsDeviceInfo] , but without things we don't use.
180
+ class MacOsDeviceInfo extends BaseDeviceInfo {
181
+ /// The major release number, such as 10 in version 10.9.3.
182
+ final int majorVersion;
183
+
184
+ /// The minor release number, such as 9 in version 10.9.3.
185
+ final int minorVersion;
186
+
187
+ /// The update release number, such as 3 in version 10.9.3.
188
+ final int patchVersion;
189
+
190
+ MacOsDeviceInfo ({
191
+ required this .majorVersion,
192
+ required this .minorVersion,
193
+ required this .patchVersion,
194
+ });
195
+ }
196
+
197
+ /// Like [device_info_plus.WindowsDeviceInfo] , currently only used to
198
+ /// determine if we're on Windows.
199
+ class WindowsDeviceInfo implements BaseDeviceInfo {}
200
+
201
+ /// Like [device_info_plus.LinuxDeviceInfo] , but without things we don't use.
202
+ ///
203
+ /// See:
204
+ /// https://www.freedesktop.org/software/systemd/man/os-release.html
205
+ class LinuxDeviceInfo implements BaseDeviceInfo {
206
+ /// A string identifying the operating system, without a version component,
207
+ /// and suitable for presentation to the user.
208
+ ///
209
+ /// Examples: 'Fedora', 'Debian GNU/Linux'.
210
+ ///
211
+ /// If not set, defaults to 'Linux'.
212
+ final String name;
213
+
214
+ /// A lower-case string identifying the operating system version, excluding
215
+ /// any OS name information or release code name, and suitable for processing
216
+ /// by scripts or usage in generated filenames.
217
+ ///
218
+ /// The version is mostly numeric, and contains no spaces or other characters
219
+ /// outside of 0–9, a–z, '.', '_' and '-'.
220
+ ///
221
+ /// Examples: '17', '11.04'.
222
+ ///
223
+ /// This field is optional and may be null on some systems.
224
+ final String ? versionId;
225
+
226
+ LinuxDeviceInfo ({
227
+ required this .name,
228
+ required this .versionId,
229
+ });
230
+ }
231
+
162
232
/// Like [package_info_plus.PackageInfo] , but without things we don't use.
163
233
class PackageInfo {
164
234
final String version;
@@ -189,6 +259,9 @@ class LiveZulipBinding extends ZulipBinding {
189
259
return ZulipBinding .instance as LiveZulipBinding ;
190
260
}
191
261
262
+ // Stored user agent header, since it remains constant.
263
+ Map <String , String >? _userAgentHeader;
264
+
192
265
@override
193
266
BaseDeviceInfo ? get deviceInfo => _deviceInfo;
194
267
BaseDeviceInfo ? _deviceInfo;
@@ -200,9 +273,16 @@ class LiveZulipBinding extends ZulipBinding {
200
273
Future <void > _prefetchDeviceInfo () async {
201
274
final info = await device_info_plus.DeviceInfoPlugin ().deviceInfo;
202
275
_deviceInfo = switch (info) {
203
- device_info_plus.AndroidDeviceInfo (: var version) => AndroidDeviceInfo (sdkInt: version.sdkInt),
204
- device_info_plus.IosDeviceInfo (: var systemVersion) => IosDeviceInfo (systemVersion: systemVersion),
205
- _ => throw UnimplementedError (),
276
+ device_info_plus.AndroidDeviceInfo () => AndroidDeviceInfo (release: info.version.release,
277
+ sdkInt: info.version.sdkInt),
278
+ device_info_plus.IosDeviceInfo () => IosDeviceInfo (systemVersion: info.systemVersion),
279
+ device_info_plus.MacOsDeviceInfo () => MacOsDeviceInfo (majorVersion: info.majorVersion,
280
+ minorVersion: info.minorVersion,
281
+ patchVersion: info.patchVersion),
282
+ device_info_plus.WindowsDeviceInfo () => WindowsDeviceInfo (),
283
+ device_info_plus.LinuxDeviceInfo () => LinuxDeviceInfo (name: info.name,
284
+ versionId: info.versionId),
285
+ _ => throw UnimplementedError (),
206
286
};
207
287
}
208
288
@@ -266,4 +346,43 @@ class LiveZulipBinding extends ZulipBinding {
266
346
267
347
@override
268
348
AndroidNotificationHostApi get androidNotificationHost => AndroidNotificationHostApi ();
349
+
350
+ @override
351
+ Map <String , String > userAgentHeader () {
352
+ if (deviceInfo == null || packageInfo == null ) {
353
+ debugLog ('userAgentHeader: Dependencies not initialized, falling back to \' ZulipFlutter\' .' );
354
+ return {'User-Agent' : 'ZulipFlutter' }; // TODO(log)
355
+ }
356
+ return _userAgentHeader ?? = buildUserAgentHeader (deviceInfo! , packageInfo! );
357
+ }
358
+ }
359
+
360
+ @visibleForTesting
361
+ Map <String , String > buildUserAgentHeader (BaseDeviceInfo deviceInfo, PackageInfo packageInfo) {
362
+ final osInfo = switch (deviceInfo) {
363
+ AndroidDeviceInfo (
364
+ : var release) => 'Android $release ' , // "Android 14"
365
+ IosDeviceInfo (
366
+ : var systemVersion) => 'iOS $systemVersion ' , // "iOS 17.4"
367
+ MacOsDeviceInfo (
368
+ : var majorVersion,
369
+ : var minorVersion,
370
+ : var patchVersion) => 'macOS $majorVersion .$minorVersion .$patchVersion ' , // "macOS 14.5.0"
371
+ WindowsDeviceInfo () => 'Windows' , // "Windows"
372
+ LinuxDeviceInfo (
373
+ : var name,
374
+ : var versionId) => 'Linux; $name ${versionId != null ? ' $versionId ' : '' }' , // "Linux; Fedora Linux 40" or "Linux; Fedora Linux"
375
+ _ => throw UnimplementedError (),
376
+ };
377
+ final PackageInfo (: version, : buildNumber) = packageInfo;
378
+
379
+ // Possible examples:
380
+ // 'ZulipFlutter/0.0.15+15 (Android 14)'
381
+ // 'ZulipFlutter/0.0.15+15 (iOS 17.4)'
382
+ // 'ZulipFlutter/0.0.15+15 (macOS 14.5.0)'
383
+ // 'ZulipFlutter/0.0.15+15 (Windows)'
384
+ // 'ZulipFlutter/0.0.15+15 (Linux; Fedora Linux 40)'
385
+ return {
386
+ 'User-Agent' : 'ZulipFlutter/$version +$buildNumber ($osInfo )' ,
387
+ };
269
388
}
0 commit comments