io.github.apkcloud.devicedetector.DeviceDetector Maven / Gradle / Ivy
Show all versions of DeviceDetector Show documentation
package io.github.apkcloud.devicedetector;
import io.github.apkcloud.devicedetector.parser.OperatingSystem;
import io.github.apkcloud.devicedetector.parser.VendorFragment;
import io.github.apkcloud.devicedetector.parser.bot.AbstractBotParser;
import io.github.apkcloud.devicedetector.parser.client.*;
import io.github.apkcloud.devicedetector.parser.device.*;
import io.github.apkcloud.devicedetector.entity.Bot;
import io.github.apkcloud.devicedetector.parser.bot.BotParser;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DeviceDetector {
/**
* 设备检测器的当前版本号
*/
public final String VERSION = "6.1.3";
/**
* 常量,未知浏览器/操作系统的值
*/
public final String UNKNOWN = "UNK";
/**
* 保存所有注册的客户端类型
*/
protected List clientTypes = new ArrayList<>();
/**
* 用于解析的 UserAgent
*/
protected String userAgent = "";
/**
* 已解析的 ClientHints
*/
protected ClientHints clientHints = null;
/**
* 解析 UA 后的操作系统数据
*/
protected Map os = new HashMap<>();
/**
* 解析 UA 后的客户端数据
*/
protected Map client = null;
/**
* 解析 UA 后的设备类型
*/
protected Integer device;
/**
* 解析 UA 后的设备品牌数据
*/
protected String brand = "";
/**
* 解析 UA 后的设备型号数据
*/
protected String model = "";
/**
* 如果解析 UA 的结果是机器人,则保留机器人信息
* (在这种情况下,所有其他信息属性将保持为空)
*
* 如果 $discardBotInformation 设置为 true,则此属性将设置为
* 如果已解析的 UA 被识别为 bot,则为 true,附加信息将不可用
*
* 如果 $skipBotDetection 设置为 true,则不会执行机器人检测,而 isBot 会
* 一直是 false
*/
protected Bot bot = null;
protected boolean discardBotInformation = false;
protected boolean skipBotDetection = false;
protected List clientParsers = new ArrayList<>();
protected List deviceParsers = new ArrayList<>();
public List botParsers = new ArrayList<>();
private boolean parsed = false;
private static DeviceDetector deviceDetector = null;
public DeviceDetector() {
addClientParser(new FeedReader()); // 用于检测 Feed阅读器 的客户端解析器
addClientParser(new MobileApp()); // 用于检测 移动应用程序 的客户端解析器
addClientParser(new MediaPlayer()); // 用于检测 媒体播放器 的客户端解析器
addClientParser(new PIM()); // 用于检测 PIM(个人信息管理器)的客户端解析器
addClientParser(new Browser()); // 用于检测 浏览器 的客户端解析器
addClientParser(new Library()); // 用于检测 工具和软件 的客户端解析器
addDeviceParser(new HbbTv()); // 用于检测 HbbTV(混合广播宽带电视)的设备解析器
addDeviceParser(new ShellTv()); // 用于检测 ShellTV(未知)的设备解析器
addDeviceParser(new Notebook()); // 用于检测 来自Facebook的UserAgent中的笔记本电脑 的设备解析器
addDeviceParser(new Console()); // 用于检测 游戏机 的设备解析器
addDeviceParser(new CarBrowser()); // 用于检测 车载浏览器 的设备解析器
addDeviceParser(new Camera()); // 用于检测 相机 的设备解析器
addDeviceParser(new PortableMediaPlayer()); // 用于检测 便携式媒体播放器 的设备解析器
addDeviceParser(new Mobile()); // 用于检测 移动 的设备解析器
addBotParser(new BotParser()); // 用于检测 机器人 的解析器
}
public DeviceDetector(String userAgent) {
this();
if (!isNullOrEmpty(userAgent)) {
setUserAgent(userAgent);
}
}
public DeviceDetector(String userAgent, ClientHints clientHints) {
this(userAgent);
if (clientHints != null) {
setClientHints(clientHints);
}
}
public boolean callMethod(String methodName) throws NoSuchMethodException {
for (Map.Entry device : AbstractDeviceParser.getAvailableDeviceTypes().entrySet()) {
String deviceName = device.getKey();
Integer deviceType = device.getValue();
if (methodName.equalsIgnoreCase("is" + deviceName.replace(" ", ""))) {
return Objects.equals(getDevice(), deviceType);
}
}
for (String client : clientTypes) {
if (methodName.equalsIgnoreCase("is" + client.replace(" ", ""))) {
return Objects.equals(getClient("type"), client);
}
}
throw new NoSuchMethodException("Method " + methodName + " not found");
}
/**
* 设置要解析的 UserAgent
*/
public void setUserAgent(String userAgent) {
if (!this.userAgent.equals(userAgent)) {
this.reset();
}
this.userAgent = userAgent;
}
/**
* 设置已解析的浏览器 ClientHints
*/
public void setClientHints(ClientHints clientHints) {
if (this.clientHints != clientHints) {
this.reset();
}
this.clientHints = clientHints;
}
public void addClientParser(AbstractClientParser parser) {
clientParsers.add(parser);
clientTypes.add(parser.getName());
}
public List getClientParsers() {
return clientParsers;
}
public void addDeviceParser(AbstractDeviceParser parser) {
deviceParsers.add(parser);
}
public List getDeviceParsers(){
return deviceParsers;
}
public void addBotParser(AbstractBotParser parser) {
botParsers.add(parser);
}
public List getBotParsers() {
return botParsers;
}
/**
* 设置是否忽略额外的机器人信息
* 如果信息被忽略,则只能检查 UA 是否被检测为机器人。
* (忽略信息可以加快检测速度)
*/
public void discardBotInformation(boolean discard) {
discardBotInformation = discard;
}
/**
* 设置是否跳过机器人检测。
* 如果我们希望将机器人作为简单的客户端进行处理,则需要它。所以我们可以检测它是否是移动客户端,
* 或桌面,或其他任何东西。默认情况下,不会为机器人检索所有这些信息。
*/
public void skipBotDetection(boolean skip) {
skipBotDetection = skip;
}
/**
* 如果解析的 UA 被识别为 Bot,则返回 true
*/
public boolean isBot(){
return bot != null;
}
/**
* 如果解析的 UA 被识别为支持触摸的设备,则返回 true
*
* 注意:这仅适用于 Windows 8 平板电脑
*
* @return boolean
*/
public boolean isTouchEnabled() {
String regex = "Touch";
return matchUserAgent(regex);
}
/**
* 如果解析的 UA 被检测为移动设备,则返回 true
*
* @return boolean
*/
public boolean isMobile() {
// ClientHints 指示移动设备
if (clientHints != null && clientHints.isMobile()) {
return true;
}
// 移动设备类型
List list = Arrays.asList(
AbstractDeviceParser.DEVICE_TYPE_FEATURE_PHONE,
AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE,
AbstractDeviceParser.DEVICE_TYPE_TABLET,
AbstractDeviceParser.DEVICE_TYPE_PHABLET,
AbstractDeviceParser.DEVICE_TYPE_CAMERA,
AbstractDeviceParser.DEVICE_TYPE_PORTABLE_MEDIA_PAYER);
if (list.contains(device)) {
return true;
}
// 非移动设备类型
list = Arrays.asList(
AbstractDeviceParser.DEVICE_TYPE_TV,
AbstractDeviceParser.DEVICE_TYPE_SMART_DISPLAY,
AbstractDeviceParser.DEVICE_TYPE_CONSOLE);
if (list.contains(device)) {
return false;
}
// 检查仅适用于移动设备的浏览器
if (usesMobileBrowser()) {
return true;
}
String osName = getOs("name");
if (isNullOrEmpty(osName) || UNKNOWN.equals(osName)) {
return false;
}
return !isBot() && !isDesktop();
}
/**
* 返回解析后的 UA 是否被识别为桌面设备。
* 桌面设备是指所有未知类型且运行桌面操作系统的设备。
*
* @return boolean
* @see OperatingSystem#desktopOsArray
*
*/
public boolean isDesktop() {
String osName = getOsAttribute("name");
if (isNullOrEmpty(osName) || UNKNOWN.equals(osName)) {
return false;
}
// 检查仅适用于移动设备的浏览器
if (usesMobileBrowser()) {
return false;
}
return OperatingSystem.isDesktopOs(osName);
}
public Map getOs() {
return os;
}
/**
* 返回从解析的 UA 中提取的操作系统数据
*
* 如果提供了 attr 参数,将只返回该属性的值
*
* @param attr 要返回的属性(可选)
* @return String
*/
public String getOs(String attr) {
return getOsAttribute(attr);
}
public Map getClient() {
return client;
}
/**
* 返回从解析的 UA 中提取的客户端数据
*
* 如果指定了 attr,只返回该属性
*
* @param attr 要返回的属性(可选)
* @return String
*/
public String getClient(String attr) {
return getClientAttribute(attr);
}
/**
* 返回从解析的 UA 中提取的设备类型
*
* @return Integer
* @see AbstractDeviceParser#deviceTypes
*/
public Integer getDevice() {
return device;
}
/**
* 返回从解析的 UA 中提取的设备类型名称
*
* @return String 设备类型名称
* @see AbstractDeviceParser#deviceTypes
*/
public String getDeviceTypeName() {
if (getDevice() != null) {
return AbstractDeviceParser.getDeviceTypeName(getDevice());
}
return "";
}
/**
* 返回从解析的 UA 中提取的设备品牌
*
* @return String
* @see AbstractDeviceParser#deviceBrands
* @deprecated 自 4.0 版本起 - 短代码可能会在下一个主要版本中删除
*/
public String getBrand() {
return AbstractDeviceParser.getShortCode(brand);
}
/**
* 返回从解析的 UA 中提取的完整设备品牌名称
*
* @return String
* @see AbstractDeviceParser#deviceBrands
*/
public String getBrandName() {
return brand;
}
/**
* 返回从解析的 UA 中提取的设备型号
*/
public String getModel() {
return model;
}
/**
* 返回设置用于解析的 UserAgent
*/
public String getUserAgent() {
return userAgent;
}
/**
* 返回设置用于解析的 ClientHints
*/
public ClientHints getClientHints() {
return clientHints;
}
/**
* 返回从解析的 UA 中提取的机器人信息
*/
public Bot getBot() {
return bot;
}
/**
* 如果 userAgent 已经通过 parse() 方法解析,则返回 true
*/
public boolean isParsed() {
return parsed;
}
/**
* 触发当前 UserAgent 的解析
*/
public void parse() throws Exception {
if (isParsed()) {
return;
}
parsed = true;
// 跳过对空 UserAgent 或不包含任何字母的 UserAgent 的解析(如果未提供 ClientHints)
if ((isNullOrEmpty(userAgent) || !Pattern.compile("([a-z])", Pattern.CASE_INSENSITIVE).matcher(userAgent).find())
&& clientHints == null) {
return;
}
parseBot();
if (isBot()) {
return;
}
parseOs();
/*
解析客户端
客户端可能是浏览器、Feed阅读器、移动应用程序、媒体播放器或
带有可解析的 UA 访问的任何其他应用程序
*/
parseClient();
parseDevice();
}
/**
* 解析 UserAgent 并返回检测到的数据
*
* 注意:该方法仅用于测试或非常小的应用程序
* 要从 DeviceDetector 获得快速结果,您需要自己实现,
* 那应该使用其中一种缓存机制。有关详细信息,请参阅 README.md。
*
* @param ua 要解析的 UserAgent
* @param clientHints 要解析的 ClientHints
* @return {@code Map}
* @deprecated
* @apiNote 这个方法仅供内部使用,不应直接公开给外部调用。
*
*/
public static Map getInfoFromUserAgent(String ua, ClientHints clientHints) throws Exception {
if (deviceDetector == null) {
deviceDetector = new DeviceDetector();
}
deviceDetector.setUserAgent(ua);
deviceDetector.setClientHints(clientHints);
deviceDetector.parse();
if (deviceDetector.isBot()) {
Map botInfo = new HashMap<>();
botInfo.put("user_agent", deviceDetector.getUserAgent());
botInfo.put("bot", deviceDetector.getBot());
return botInfo;
}
/** @var array $client */
Map client = deviceDetector.getClient();
String browserFamily = "Unknown";
if (deviceDetector.callMethod("isBrowser")
&& client != null
&& client.containsKey("family")
&& null != client.get("family")
) {
browserFamily = client.get("family");
}
if (client != null) {
client.remove("short_name");
client.remove("family");
}
/** @var array $os */
Map os = deviceDetector.getOs();
String osFamily = os.getOrDefault("family", "Unknown");
os.remove("short_name");
os.remove("family");
String finalBrowserFamily = browserFamily;
return new HashMap<>(){
{
put("user_agent", deviceDetector.getUserAgent());
put("os", os);
put("client", client);
put("device", new LinkedHashMap<>(){
{
put("type", deviceDetector.getDeviceTypeName());
put("brand", deviceDetector.getBrandName());
put("model", deviceDetector.getModel());
}
});
put("os_family", osFamily);
put("browser_family", finalBrowserFamily);
}
};
}
public String getClientAttribute(String attr) {
if (!client.containsKey(attr)) {
return UNKNOWN;
}
return client.get(attr);
}
public String getOsAttribute(String attr) {
if (!os.containsKey(attr)) {
return UNKNOWN;
}
return os.get(attr);
}
/**
* 判断解析的 UA 是否包含 'Android; Tablet;' 片段
*/
protected boolean hasAndroidTableFragment() {
String regex = "Android( [\\.0-9]+)?; Tablet;";
return matchUserAgent(regex);
}
/**
* 判断解析的 UA 是否包含 'Android; Mobile;' 片段
*/
protected boolean hasAndroidMobileFragment() {
String regex = "Android( [\\.0-9]+)?; Mobile;";
return matchUserAgent(regex);
}
/**
* 判断解析的 UA 是否包含 'Desktop x64;' 或 'Desktop x32;' 或 'Desktop WOW64' 片段
*/
protected boolean hasDesktopFragment() {
String regex = "Desktop (x(?:32|64)|WOW64);";
return matchUserAgent(regex);
}
/**
* 判断解析的 UA 是否包含仅限移动设备使用的浏览器
*/
protected boolean usesMobileBrowser() {
return "browser".equals(getClient("type"))
&& Browser.isMobileOnlyBrowser(getClientAttribute("name"));
}
/**
* 使用 Bot 解析器解析 UA 以获取机器人信息
*/
protected void parseBot() throws Exception {
if (skipBotDetection) {
bot = null;
return;
}
List parsers = getBotParsers();
for (AbstractBotParser parser : parsers) {
parser.setUserAgent(getUserAgent());
parser.setClientHints(getClientHints());
if (discardBotInformation) {
parser.discardDetails();
}
Bot bot = parser.parse();
if (bot != null) {
this.bot = bot;
break;
}
}
}
/**
* 尝试检测客户端(例如浏览器、移动应用程序等)
*/
protected void parseClient() throws Exception {
List parsers = getClientParsers();
for (AbstractClientParser parser : parsers) {
parser.setUserAgent(getUserAgent());
parser.setClientHints(getClientHints());
Map client = parser.parse();
if (!isNullOrEmpty(client)) {
this.client = client;
break;
}
}
}
/**
* 尝试检测设备类型、型号和品牌
*/
protected void parseDevice() throws Exception {
List parsers = getDeviceParsers();
for (AbstractDeviceParser parser : parsers) {
parser.setUserAgent(getUserAgent());
parser.setClientHints(getClientHints());
if (!isNullOrEmpty(parser.parse())) {
device = parser.getDeviceType();
model = parser.getModel();
brand = parser.getBrand();
break;
}
}
// 如果无法从 UserAgent 中解析出型号,则使用 ClientHints 中的型号(如果有的话)
if (clientHints != null && isNullOrEmpty(model)) {
model = clientHints.getModel();
}
// 如果未分配品牌,则尝试通过已知供应商片段进行匹配
if (isNullOrEmpty(brand)) {
VendorFragment vendorParser = new VendorFragment(getUserAgent());
brand = vendorParser.parse().getOrDefault("brand", "");
}
String osName = getOsAttribute("name");
String osFamily = getOsAttribute("family");
String osVersion = getOsAttribute("version");
String clientName = getClientAttribute("name");
// 如果是伪造的 UA,则最好不要将其识别为 Apple 运行 Android OS
if ("Android".equals(osName) && "Apple".equals(brand)) {
device = null;
brand = "";
model = "";
}
// 假设所有运行 iOS / Mac O S的设备都是来自 Apple
if (isNullOrEmpty(brand) && Arrays.asList("iPadOS", "tvOS", "watchOS", "iOS", "Mac").contains(osName)) {
brand = "Apple";
}
/*
Chrome 在 Android 传递设备类型基于关键字 'Mobile'
如果存在关键字,则设备应该是智能手机;否则是平板电脑
参考链接:https://developer.chrome.com/multidevice/user-agent#chrome_for_android_user_agent
注意:我们在此处不检查浏览器(家族),因为可能有使用 Chrome 的移动应用程序,不会检测到浏览器,
但仍然可以检测到。所以我们检查 Chrome 的 UserAgent。
*/
if (device == null && "Android".equals(osFamily) && matchUserAgent("Chrome/[\\.0-9]*")) {
if (matchUserAgent("(?:Mobile|eliboM) Safari/")) {
device = AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE;
} else if (matchUserAgent("(?!Mobile )Safari/")) {
device = AbstractDeviceParser.DEVICE_TYPE_TABLET;
}
}
/*
某些 UA 包含片段 'Pad/APad',我们将这些设备视为平板电脑
*/
if (device != null && AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE == device && matchUserAgent("Pad/APad")) {
device = AbstractDeviceParser.DEVICE_TYPE_TABLET;
}
/*
某些 UA 包含片段 'Android; Tablet;' 或 'Opera Tablet',我们将这些设备视为平板电脑
*/
if (device == null && (hasAndroidTableFragment() || matchUserAgent("Opera Tablet"))) {
device = AbstractDeviceParser.DEVICE_TYPE_TABLET;
}
/*
某些 UserAgent 只包含片段 'Android; Mobile;',我们将这些设备视为智能手机
*/
if (device == null && hasAndroidMobileFragment()) {
device = AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE;
}
/*
Android 3.0 及以上版本仅专为智能手机设计。但由于 3.0(仅适用于平板电脑)发布得太晚,因此出现了一堆运行 2.x 的平板电脑
在 4.0 中,两棵树被合并,适用于智能手机和平板电脑
因此,我们预计所有运行 Android < 2 的设备都是智能手机
运行 Android 3.X 的设备是平板电脑。 Android 2.X 和 4.X+ 的设备类型未知
*/
if (device == null && "Android".equals(osName) && !"".equals(osVersion)) {
if (compareVersions(osVersion, "2.0") == -1) {
device = AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE;
} else if (compareVersions(osVersion, "3.0") >= 0 && compareVersions(osVersion, "4.0") == -1) {
device = AbstractDeviceParser.DEVICE_TYPE_TABLET;
}
}
/*
所有检测到的功能手机运行 Android 的更有可能是智能手机
*/
if (device != null && AbstractDeviceParser.DEVICE_TYPE_FEATURE_PHONE == device && "Android".equals(osFamily)) {
device = AbstractDeviceParser.DEVICE_TYPE_SMARTPHONE;
}
/*
所有未知设备运行Java ME的更有可能是功能手机
*/
if ("Java ME".equals(osName) && device == null) {
device = AbstractDeviceParser.DEVICE_TYPE_FEATURE_PHONE;
}
/*
根据http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx
Internet Explorer 10 引入了 "Touch" UA字符串标记。如果此标记出现在 UA 字符串的末尾,则计算机具有触摸功能,并且正在运行 Windows 8(或更高版本)。
该 UA 字符串将在运行 Windows 8 (RT) 的触摸设备上传输。
由于大多数触摸设备都是平板电脑,只有一小部分是台式机/笔记本电脑,所以我们假设所有运行 Windows 8 的触摸设备都是平板电脑。
*/
if (device == null && ("Windows RT".equals(osName) || ("Windows".equals(osName) && compareVersions(osVersion, "8") >= 0)) && isTouchEnabled()) {
device = AbstractDeviceParser.DEVICE_TYPE_TABLET;
}
/*
所有运行 Opera TV Store 的设备都被视为电视
*/
if (matchUserAgent("Opera TV Store| OMI/")) {
device = AbstractDeviceParser.DEVICE_TYPE_TV;
}
/*
所有包含 Andr0id 字符串的设备都被视为电视
*/
if (matchUserAgent("Andr0id|Android TV|\\(lite\\) TV")) {
device = AbstractDeviceParser.DEVICE_TYPE_TV;
}
/*
所有运行 Tizen TV 或 SmartTV 的设备都被视为电视
*/
if (device == null && matchUserAgent("SmartTV|Tizen.+ TV .+$")) {
device = AbstractDeviceParser.DEVICE_TYPE_TV;
}
/*
运行 Kylo 或 Espital TV 浏览器的设备都被视为电视
*/
if (device == null && Arrays.asList("Kylo", "Espial TV Browser").contains(clientName)) {
device = AbstractDeviceParser.DEVICE_TYPE_TV;
}
/*
所有包含 TV 片段的设备都被视为电视
*/
if (device == null && matchUserAgent("\\(TV;")) {
device = AbstractDeviceParser.DEVICE_TYPE_TV;
}
/*
如果ua字符串中包含桌面,则将设备类型设置为桌面
*/
boolean hasDesktop = AbstractDeviceParser.DEVICE_TYPE_DESKTOP != device && userAgent.contains("Desktop") && hasDesktopFragment();
if (hasDesktop) {
device = AbstractDeviceParser.DEVICE_TYPE_DESKTOP;
}
// 对于运行桌面操作系统且未检测为其他设备类型的所有设备,将设备类型设置为桌面
if (device != null || !isDesktop()) {
return;
}
device = AbstractDeviceParser.DEVICE_TYPE_DESKTOP;
}
public static int compareVersions(String version1, String version2) {
String[] v1Tokens = version1.split("\\.");
String[] v2Tokens = version2.split("\\.");
int length = Math.max(v1Tokens.length, v2Tokens.length);
for (int i = 0; i < length; i++) {
int v1Token = i < v1Tokens.length ? Integer.parseInt(v1Tokens[i]) : 0;
int v2Token = i < v2Tokens.length ? Integer.parseInt(v2Tokens[i]) : 0;
if (v1Token < v2Token) {
return -1;
} else if (v1Token > v2Token) {
return 1;
}
}
return 0;
}
/**
* 尝试检测操作系统
*/
protected void parseOs() throws Exception {
OperatingSystem osParser = new OperatingSystem();
osParser.setUserAgent(this.getUserAgent());
osParser.setClientHints(this.getClientHints());
this.os = osParser.parse();
}
protected boolean matchUserAgent(String regex) {
String pattern = "(?:^|[^A-Z_-])(?:" + regex.replace("/", "\\/") + ")";
Pattern compiledPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
Matcher matcher = compiledPattern.matcher(this.getUserAgent());
return matcher.find();
}
/**
* 重置所有检测到的数据
*/
protected void reset() {
this.bot = null;
this.client = null;
this.device = null;
this.os = null;
this.brand = "";
this.model = "";
this.parsed = false;
}
public static boolean isNullOrEmpty(List list) {
return list == null || list.isEmpty();
}
public static boolean isNullOrEmpty(Map map) {
return map == null || map.isEmpty();
}
public static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
}