Skip to content

Commit abb20a0

Browse files
author
Vadim Belov
committed
Improve device detection and model code recognition
Enhance UserAgentHelpers to better identify devices by: - Accepting nullable user agent strings for improved null safety. - Adding TryParseKnownDeviceCode to detect known device models (Samsung, Apple, OnePlus, Xiaomi) via regex and lookup. - Expanding Samsung model regex and refining device type resolution (e.g., SM-L* as watches). - Improving Android model extraction logic to skip noise tokens. - Updating tests for new device recognition, including specific Samsung watch models. - Removing "okhttp" from script detection. These changes increase accuracy for device identification and improve test coverage.
1 parent 1e84280 commit abb20a0

2 files changed

Lines changed: 57 additions & 16 deletions

File tree

Sources/EasyExtensions.Tests/UserAgentHelpersTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class UserAgentHelpersTests
4343
[TestCase(" ", "Unknown")]
4444
public void GetDevice_Returns_Unknown_For_NullOrWhitespace(string? ua, string expected)
4545
{
46-
Assert.That(UserAgentHelpers.GetDevice(ua!), Is.EqualTo(expected));
46+
Assert.That(UserAgentHelpers.GetDevice(ua), Is.EqualTo(expected));
4747
}
4848

4949
[TestCase("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Bot")]
@@ -83,7 +83,7 @@ public void GetDevice_Identifies_iOS(string ua, string expected)
8383
[TestCase("Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36", "Samsung Galaxy S20")]
8484
[TestCase("Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36", "Samsung Galaxy S23 Ultra")]
8585
[TestCase("Mozilla/5.0 (Linux; Android 13; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36", "Samsung SM-A556B")]
86-
[TestCase("Mozilla/5.0 (Linux; Android 13; SM-R960) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36", "Samsung SM-R960")]
86+
[TestCase("Mozilla/5.0 (Linux; Android 13; SM-R960) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36", "Samsung Galaxy Watch6 Classic 47mm")]
8787
[TestCase("Mozilla/5.0 (Linux; Android 11; SM-T970 Build/RP1A.200720.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Safari/537.36", "Android Tablet (SM-T970)")]
8888
[TestCase("Mozilla/5.0 (Linux; Android 13; Tablet; rv:102.0) Gecko/102.0 Firefox/102.0", "Android Tablet")]
8989
public void GetDevice_Identifies_Android(string ua, string expected)

Sources/EasyExtensions/Helpers/UserAgentHelpers.cs

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class UserAgentHelpers
2222
TryParseScript,
2323
TryParseTv,
2424
TryParseConsole,
25+
TryParseKnownDeviceCode,
2526
TryParseIos,
2627
TryParseAndroid,
2728
TryParseChromeOs,
@@ -30,7 +31,11 @@ public static class UserAgentHelpers
3031
TryParseServerFallback,
3132
};
3233

33-
private static readonly Regex _samsungModelRegex = new Regex(@"\bSM-[A-Z]\d{3}[A-Z0-9]?\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
34+
private static readonly Regex _samsungModelRegex = new Regex(@"\bSM-[A-Z]\d{3}[A-Z0-9]{0,4}\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
35+
36+
private static readonly Regex _appleMachineRegex = new Regex(@"\b(iPhone|iPad)\d{1,2},\d{1,2}\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
37+
private static readonly Regex _onePlusRegex = new Regex(@"\bCPH\d{4}\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
38+
private static readonly Regex _xiaomiSkuRegex = new Regex(@"\b\d{5}[A-Z]{2}\d{2}[A-Z0-9]{1,3}\b", RegexOptions.Compiled | RegexOptions.CultureInvariant);
3439

3540
/// <summary>
3641
/// Determines the type of device based on the provided user agent string.
@@ -42,7 +47,7 @@ public static class UserAgentHelpers
4247
/// <param name="userAgent">The user agent string to analyze for device identification. Cannot be null or whitespace.</param>
4348
/// <returns>A string representing the detected device type, such as "iPhone", "Android Phone", "Windows PC", or "Bot".
4449
/// Returns "Unknown" if the user agent is null or whitespace.</returns>
45-
public static string GetDevice(string userAgent)
50+
public static string GetDevice(string? userAgent)
4651
{
4752
return GetDeviceInfo(userAgent).FriendlyName ?? "Unknown";
4853
}
@@ -54,7 +59,7 @@ public static string GetDevice(string userAgent)
5459
/// <returns>A <see cref="UserAgentDeviceInfo"/> object containing the detected device information. If the device type
5560
/// cannot be determined, the returned object will have <see cref="UserAgentDeviceType.Unknown"/> as the device
5661
/// type.</returns>
57-
public static UserAgentDeviceInfo GetDeviceInfo(string userAgent)
62+
public static UserAgentDeviceInfo GetDeviceInfo(string? userAgent)
5863
{
5964
if (string.IsNullOrWhiteSpace(userAgent))
6065
{
@@ -73,6 +78,11 @@ public static UserAgentDeviceInfo GetDeviceInfo(string userAgent)
7378
return new UserAgentDeviceInfo(UserAgentDeviceType.Unknown, null, "Unknown");
7479
}
7580

81+
private static UserAgentDeviceInfo? TryParseKnownDeviceCode(string ua)
82+
{
83+
return TryGetFirstKnownDeviceCode(ua);
84+
}
85+
7686
// small helpers
7787
private static bool ContainsAny(string haystack, params string[] needles)
7888
{
@@ -96,7 +106,7 @@ private static bool ContainsAny(string haystack, params string[] needles)
96106
private static UserAgentDeviceInfo? TryParseScript(string ua)
97107
{
98108
var ual = ua.ToLowerInvariant();
99-
if (ContainsAny(ual, "curl/", "wget/", "httpclient", "libwww", "okhttp", "java/"))
109+
if (ContainsAny(ual, "curl/", "wget/", "httpclient", "libwww", "java/"))
100110
{
101111
return new UserAgentDeviceInfo(UserAgentDeviceType.Script, null, "Script");
102112
}
@@ -174,12 +184,35 @@ private static bool ContainsAny(string haystack, params string[] needles)
174184

175185
private static UserAgentDeviceInfo? TryGetFirstKnownDeviceCode(string value)
176186
{
177-
// Currently we support Samsung-style model codes, but this can be expanded.
187+
// Lookup-only: we only return a match if the extracted code exists in KnownDeviceCodes.Map.
188+
189+
// 1) Samsung
178190
var samsungCode = GetFirstSamsungModelCode(value);
179191
if (samsungCode != null && KnownDeviceCodes.Map.TryGetValue(samsungCode, out var knownSamsung))
180192
{
181193
return knownSamsung;
182194
}
195+
196+
// 2) Apple machine ids (useful in SDK/logs, typically not present in Safari UA)
197+
var am = _appleMachineRegex.Match(value);
198+
if (am.Success && KnownDeviceCodes.Map.TryGetValue(am.Value, out var knownApple))
199+
{
200+
return knownApple;
201+
}
202+
203+
// 3) OnePlus CPH
204+
var op = _onePlusRegex.Match(value);
205+
if (op.Success && KnownDeviceCodes.Map.TryGetValue(op.Value, out var knownOp))
206+
{
207+
return knownOp;
208+
}
209+
210+
// 4) Xiaomi-ish SKU (rough; match then lookup)
211+
var xi = _xiaomiSkuRegex.Match(value);
212+
if (xi.Success && KnownDeviceCodes.Map.TryGetValue(xi.Value, out var knownXi))
213+
{
214+
return knownXi;
215+
}
183216
return null;
184217
}
185218

@@ -192,7 +225,9 @@ private static UserAgentDeviceType ResolveSamsungType(string samsungModelCode)
192225
return UserAgentDeviceType.SamsungTablet;
193226
}
194227

195-
if (samsungModelCode.StartsWith("SM-R", StringComparison.OrdinalIgnoreCase) || samsungModelCode.StartsWith("SM-W", StringComparison.OrdinalIgnoreCase))
228+
if (samsungModelCode.StartsWith("SM-R", StringComparison.OrdinalIgnoreCase)
229+
|| samsungModelCode.StartsWith("SM-W", StringComparison.OrdinalIgnoreCase)
230+
|| samsungModelCode.StartsWith("SM-L", StringComparison.OrdinalIgnoreCase))
196231
{
197232
return UserAgentDeviceType.SamsungWatch;
198233
}
@@ -221,17 +256,23 @@ private static UserAgentDeviceType ResolveSamsungType(string samsungModelCode)
221256
var token = parts[i].Trim();
222257
if (!token.StartsWith("Android", StringComparison.OrdinalIgnoreCase)) continue;
223258

224-
var next = i + 1 < parts.Length ? parts[i + 1].Trim() : null;
225-
if (string.IsNullOrWhiteSpace(next)) return null;
226-
227-
// ignore common noise tokens
228-
if (next.Equals("wv", StringComparison.OrdinalIgnoreCase) || next.Equals("mobile", StringComparison.OrdinalIgnoreCase))
259+
for (var j = i + 1; j < parts.Length; j++)
229260
{
230-
return null;
261+
var cand = parts[j].Trim();
262+
if (string.IsNullOrWhiteSpace(cand)) continue;
263+
264+
// ignore common noise tokens
265+
if (cand.Equals("wv", StringComparison.OrdinalIgnoreCase)
266+
|| cand.Equals("mobile", StringComparison.OrdinalIgnoreCase))
267+
{
268+
continue;
269+
}
270+
271+
var model = SanitizeModel(cand);
272+
return model.Length > 0 ? model : null;
231273
}
232274

233-
var model = SanitizeModel(next);
234-
return model.Length > 0 ? model : null;
275+
return null;
235276
}
236277
return null;
237278
}

0 commit comments

Comments
 (0)