-
Notifications
You must be signed in to change notification settings - Fork 504
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extended device info (PSG-772) (#6766)
- Loading branch information
1 parent
46a975b
commit 2f689f4
Showing
17 changed files
with
825 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// | ||
// Copyright 2022 New Vector Ltd | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
|
||
extension Collection { | ||
/// Returns the element at the specified index if it is within bounds, otherwise nil. | ||
subscript(safe index: Index) -> Element? { | ||
indices.contains(index) ? self[index] : nil | ||
} | ||
} |
201 changes: 201 additions & 0 deletions
201
RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// | ||
// Copyright 2022 New Vector Ltd | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
|
||
struct UserAgent { | ||
let deviceType: DeviceType | ||
let deviceModel: String? | ||
let deviceOS: String? | ||
let clientName: String? | ||
let clientVersion: String? | ||
|
||
static let unknown = UserAgent(deviceType: .unknown, | ||
deviceModel: nil, | ||
deviceOS: nil, | ||
clientName: nil, | ||
clientVersion: nil) | ||
} | ||
|
||
extension UserAgent: Equatable { } | ||
|
||
enum UserAgentParser { | ||
private enum Constants { | ||
static let deviceInfoRegexPattern = "\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)" | ||
|
||
static let androidKeyword = "; MatrixAndroidSdk2" | ||
static let iosKeyword = "; iOS " | ||
static let desktopKeyword = " Electron/" | ||
static let webKeyword = "Mozilla/" | ||
} | ||
|
||
static func parse(_ userAgent: String) -> UserAgent { | ||
if userAgent.vc_caseInsensitiveContains(Constants.androidKeyword) { | ||
return parseAndroid(userAgent) | ||
} else if userAgent.vc_caseInsensitiveContains(Constants.iosKeyword) { | ||
return parseIOS(userAgent) | ||
} else if userAgent.vc_caseInsensitiveContains(Constants.desktopKeyword) { | ||
return parseDesktop(userAgent) | ||
} else if userAgent.vc_caseInsensitiveContains(Constants.webKeyword) { | ||
return parseWeb(userAgent) | ||
} | ||
return .unknown | ||
} | ||
|
||
// Legacy: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) | ||
// New: Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) | ||
private static func parseAndroid(_ userAgent: String) -> UserAgent { | ||
var deviceModel: String? | ||
var deviceOS: String? | ||
var clientName: String? | ||
var clientVersion: String? | ||
|
||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/") | ||
clientName = beforeSlash | ||
if let afterSlash = afterSlash { | ||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ") | ||
clientVersion = beforeSpace | ||
if let afterSpace = afterSpace { | ||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) { | ||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") | ||
let isLegacy = deviceInfoComponents[safe: 0] == "Linux" | ||
if isLegacy { | ||
// find the segment starting with "Android" | ||
if let osSegmentIndex = deviceInfoComponents.firstIndex(where: { $0.hasPrefix("Android") }) { | ||
deviceOS = deviceInfoComponents[safe: osSegmentIndex] | ||
deviceModel = deviceInfoComponents[safe: osSegmentIndex + 1] | ||
} | ||
} else { | ||
deviceModel = deviceInfoComponents[safe: 0] | ||
deviceOS = deviceInfoComponents[safe: 1] | ||
} | ||
} | ||
} | ||
} | ||
|
||
return UserAgent(deviceType: .mobile, | ||
deviceModel: deviceModel, | ||
deviceOS: deviceOS, | ||
clientName: clientName, | ||
clientVersion: clientVersion) | ||
} | ||
|
||
// Legacy: Riot/1.8.21 (iPhone; iOS 15.2; Scale/3.00) | ||
// New: Riot/1.8.21 (iPhone X; iOS 15.2; Scale/3.00) | ||
private static func parseIOS(_ userAgent: String) -> UserAgent { | ||
var deviceModel: String? | ||
var deviceOS: String? | ||
var clientName: String? | ||
var clientVersion: String? | ||
|
||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/") | ||
clientName = beforeSlash | ||
if let afterSlash = afterSlash { | ||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ") | ||
clientVersion = beforeSpace | ||
if let afterSpace = afterSpace { | ||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) { | ||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") | ||
deviceModel = deviceInfoComponents[safe: 0] | ||
deviceOS = deviceInfoComponents[safe: 1] | ||
} | ||
} | ||
} | ||
|
||
return UserAgent(deviceType: .mobile, | ||
deviceModel: deviceModel, | ||
deviceOS: deviceOS, | ||
clientName: clientName, | ||
clientVersion: clientVersion) | ||
} | ||
|
||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36 | ||
private static func parseDesktop(_ userAgent: String) -> UserAgent { | ||
var deviceOS: String? | ||
let browserName = browserName(for: userAgent) | ||
|
||
if let deviceInfo = findFirstDeviceInfo(in: userAgent) { | ||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") | ||
deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first | ||
} | ||
|
||
return UserAgent(deviceType: .desktop, | ||
deviceModel: browserName, | ||
deviceOS: deviceOS, | ||
clientName: nil, | ||
clientVersion: nil) | ||
} | ||
|
||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 | ||
private static func parseWeb(_ userAgent: String) -> UserAgent { | ||
let desktopUserAgent = parseDesktop(userAgent) | ||
|
||
return UserAgent(deviceType: .web, | ||
deviceModel: desktopUserAgent.deviceModel, | ||
deviceOS: desktopUserAgent.deviceOS, | ||
clientName: desktopUserAgent.clientName, | ||
clientVersion: desktopUserAgent.clientVersion) | ||
} | ||
|
||
private static func findFirstDeviceInfo(in string: String) -> String? { | ||
guard let regex = try? NSRegularExpression(pattern: Constants.deviceInfoRegexPattern, | ||
options: .caseInsensitive) else { | ||
return nil | ||
} | ||
var range = regex.rangeOfFirstMatch(in: string, range: NSRange(string.startIndex..., in: string)) | ||
if range.location != NSNotFound { | ||
range.location += 1 | ||
range.length -= 2 | ||
return string[range] | ||
} | ||
return nil | ||
} | ||
|
||
private static func browserName(for userAgent: String) -> String? { | ||
let components = userAgent.components(separatedBy: " ") | ||
if components.last?.hasPrefix("Firefox") == true { | ||
return "Firefox" | ||
} else if components.last?.hasPrefix("Safari") == true | ||
&& components[safe:components.count - 2]?.hasPrefix("Mobile") == true { | ||
// mobile browser | ||
let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first | ||
return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName | ||
} else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true { | ||
return "Safari" | ||
} else { | ||
// regular browser | ||
return components[safe:components.count - 2]?.components(separatedBy: "/").first | ||
} | ||
} | ||
} | ||
|
||
private extension String { | ||
subscript(_ range: NSRange) -> String { | ||
let start = index(startIndex, offsetBy: range.lowerBound) | ||
let end = index(startIndex, offsetBy: range.upperBound) | ||
let subString = self[start..<end] | ||
return String(subString) | ||
} | ||
|
||
func splitByFirst(_ delimiter: Character) -> (String?, String?) { | ||
guard let delimiterIndex = firstIndex(of: delimiter) else { | ||
return (nil, nil) | ||
} | ||
let before = String(prefix(upTo: delimiterIndex)) | ||
let after = String(suffix(from: index(after: delimiterIndex))) | ||
return (before, after) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.