| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/devtools/device/android_device_manager.h" |
| |
| namespace { |
| |
| #define SEPARATOR "======== output separator ========" |
| |
| const char kAllCommands[] = "shell:" |
| "getprop ro.product.model\n" |
| "echo " SEPARATOR "\n" |
| "dumpsys window policy\n" |
| "echo " SEPARATOR "\n" |
| "ps\n" |
| "echo " SEPARATOR "\n" |
| "cat /proc/net/unix\n" |
| "echo " SEPARATOR "\n" |
| "dumpsys user\n"; |
| |
| const char kSeparator[] = SEPARATOR; |
| |
| #undef SEPARATOR |
| |
| const char kScreenSizePrefix[] = "mStable="; |
| const char kUserInfoPrefix[] = "UserInfo{"; |
| |
| const char kDevToolsSocketSuffix[] = "_devtools_remote"; |
| |
| const char kChromeDefaultName[] = "Chrome"; |
| const char kChromeDefaultSocket[] = "chrome_devtools_remote"; |
| |
| const char kWebViewSocketPrefix[] = "webview_devtools_remote"; |
| const char kWebViewNameTemplate[] = "WebView in %s"; |
| |
| struct BrowserDescriptor { |
| const char* package; |
| const char* socket; |
| const char* display_name; |
| }; |
| |
| const BrowserDescriptor kBrowserDescriptors[] = { |
| { |
| "com.google.android.apps.chrome", |
| kChromeDefaultSocket, |
| "Chromium" |
| }, |
| { |
| "com.chrome.canary", |
| kChromeDefaultSocket, |
| "Chrome Canary" |
| }, |
| { |
| "com.chrome.dev", |
| kChromeDefaultSocket, |
| "Chrome Dev" |
| }, |
| { |
| "com.chrome.beta", |
| kChromeDefaultSocket, |
| "Chrome Beta" |
| }, |
| { |
| "com.android.chrome", |
| kChromeDefaultSocket, |
| kChromeDefaultName |
| }, |
| { |
| "org.chromium.android_webview.shell", |
| "webview_devtools_remote", |
| "WebView Test Shell" |
| }, |
| { |
| "org.chromium.content_shell_apk", |
| "content_shell_devtools_remote", |
| "Content Shell" |
| }, |
| { |
| "org.chromium.chrome", |
| kChromeDefaultSocket, |
| "Chromium" |
| }, |
| }; |
| |
| const BrowserDescriptor* FindBrowserDescriptor(const std::string& package) { |
| size_t count = base::size(kBrowserDescriptors); |
| for (size_t i = 0; i < count; i++) { |
| if (kBrowserDescriptors[i].package == package) |
| return &kBrowserDescriptors[i]; |
| } |
| return nullptr; |
| } |
| |
| bool BrowserCompare(const AndroidDeviceManager::BrowserInfo& a, |
| const AndroidDeviceManager::BrowserInfo& b) { |
| size_t count = base::size(kBrowserDescriptors); |
| for (size_t i = 0; i < count; i++) { |
| bool isA = kBrowserDescriptors[i].display_name == a.display_name; |
| bool isB = kBrowserDescriptors[i].display_name == b.display_name; |
| if (isA != isB) |
| return isA; |
| if (isA && isB) |
| break; |
| } |
| return a.socket_name < b.socket_name; |
| } |
| |
| using StringMap = std::map<std::string, std::string>; |
| |
| void MapProcessesToPackages(const std::string& response, |
| StringMap* pid_to_package, |
| StringMap* pid_to_user) { |
| // Parse 'ps' output which on Android looks like this: |
| // |
| // USER PID PPID VSIZE RSS WCHAN PC ? NAME |
| // |
| for (const base::StringPiece& line : |
| base::SplitStringPiece(response, "\n", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| std::vector<std::string> fields = |
| base::SplitString(line, " \r", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (fields.size() < 9) |
| continue; |
| std::string pid = fields[1]; |
| (*pid_to_package)[pid] = fields[8]; |
| (*pid_to_user)[pid] = fields[0]; |
| } |
| } |
| |
| StringMap MapSocketsToProcesses(const std::string& response) { |
| // Parse 'cat /proc/net/unix' output which on Android looks like this: |
| // |
| // Num RefCount Protocol Flags Type St Inode Path |
| // 00000000: 00000002 00000000 00010000 0001 01 331813 /dev/socket/zygote |
| // 00000000: 00000002 00000000 00010000 0001 01 358606 @xxx_devtools_remote |
| // 00000000: 00000002 00000000 00010000 0001 01 347300 @yyy_devtools_remote |
| // |
| // We need to find records with paths starting from '@' (abstract socket) |
| // and containing the channel pattern ("_devtools_remote"). |
| StringMap socket_to_pid; |
| for (const base::StringPiece& line : |
| base::SplitStringPiece(response, "\n", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| std::vector<std::string> fields = |
| base::SplitString(line, " \r", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (fields.size() < 8) |
| continue; |
| if (fields[3] != "00010000" || fields[5] != "01") |
| continue; |
| std::string path_field = fields[7]; |
| if (path_field.size() < 1 || path_field[0] != '@') |
| continue; |
| size_t socket_name_pos = path_field.find(kDevToolsSocketSuffix); |
| if (socket_name_pos == std::string::npos) |
| continue; |
| |
| std::string socket = path_field.substr(1); |
| |
| std::string pid; |
| size_t socket_name_end = socket_name_pos + strlen(kDevToolsSocketSuffix); |
| if (socket_name_end < path_field.size() && |
| path_field[socket_name_end] == '_') { |
| pid = path_field.substr(socket_name_end + 1); |
| } |
| socket_to_pid[socket] = pid; |
| } |
| return socket_to_pid; |
| } |
| |
| gfx::Size ParseScreenSize(base::StringPiece str) { |
| std::vector<base::StringPiece> pairs = |
| base::SplitStringPiece(str, "-", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (pairs.size() != 2) |
| return gfx::Size(); |
| |
| int width; |
| int height; |
| std::vector<base::StringPiece> numbers = |
| base::SplitStringPiece(pairs[1].substr(1, pairs[1].size() - 2), ",", |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| if (numbers.size() != 2 || |
| !base::StringToInt(numbers[0], &width) || |
| !base::StringToInt(numbers[1], &height)) |
| return gfx::Size(); |
| |
| return gfx::Size(width, height); |
| } |
| |
| gfx::Size ParseWindowPolicyResponse(const std::string& response) { |
| for (const base::StringPiece& line : |
| base::SplitStringPiece(response, "\r", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| size_t pos = line.find(kScreenSizePrefix); |
| if (pos != base::StringPiece::npos) { |
| return ParseScreenSize( |
| line.substr(pos + strlen(kScreenSizePrefix))); |
| } |
| } |
| return gfx::Size(); |
| } |
| |
| StringMap MapIdsToUsers(const std::string& response) { |
| // Parse 'dumpsys user' output which looks like this: |
| // Users: |
| // UserInfo{0:Test User:13} serialNo=0 |
| // Created: <unknown> |
| // Last logged in: +17m18s871ms ago |
| // UserInfo{10:User with : (colon):10} serialNo=10 |
| // Created: +3d4h35m1s139ms ago |
| // Last logged in: +17m26s287ms ago |
| StringMap id_to_username; |
| for (const base::StringPiece& line : |
| base::SplitStringPiece(response, "\r", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| size_t pos = line.find(kUserInfoPrefix); |
| if (pos != std::string::npos) { |
| base::StringPiece fields = line.substr(pos + strlen(kUserInfoPrefix)); |
| size_t first_pos = fields.find_first_of(":"); |
| size_t last_pos = fields.find_last_of(":"); |
| if (first_pos != std::string::npos && last_pos != std::string::npos) { |
| std::string id = fields.substr(0, first_pos).as_string(); |
| std::string name = fields.substr(first_pos + 1, |
| last_pos - first_pos - 1).as_string(); |
| id_to_username[id] = name; |
| } |
| } |
| } |
| return id_to_username; |
| } |
| |
| std::string GetUserName(const std::string& unix_user, |
| const StringMap id_to_username) { |
| // Parse username as returned by ps which looks like 'u0_a31' |
| // where '0' is user id and '31' is app id. |
| if (!unix_user.empty() && unix_user[0] == 'u') { |
| size_t pos = unix_user.find('_'); |
| if (pos != std::string::npos) { |
| auto it = id_to_username.find(unix_user.substr(1, pos - 1)); |
| if (it != id_to_username.end()) |
| return it->second; |
| } |
| } |
| return std::string(); |
| } |
| |
| AndroidDeviceManager::BrowserInfo::Type |
| GetBrowserType(const std::string& socket) { |
| if (base::StartsWith(socket, kChromeDefaultSocket, |
| base::CompareCase::SENSITIVE)) { |
| return AndroidDeviceManager::BrowserInfo::kTypeChrome; |
| } |
| |
| if (base::StartsWith(socket, kWebViewSocketPrefix, |
| base::CompareCase::SENSITIVE)) { |
| return AndroidDeviceManager::BrowserInfo::kTypeWebView; |
| } |
| |
| return AndroidDeviceManager::BrowserInfo::kTypeOther; |
| } |
| |
| void ReceivedResponse(const AndroidDeviceManager::DeviceInfoCallback& callback, |
| int result, |
| const std::string& response) { |
| AndroidDeviceManager::DeviceInfo device_info; |
| if (result < 0) { |
| callback.Run(device_info); |
| return; |
| } |
| std::vector<std::string> outputs = base::SplitStringUsingSubstr( |
| response, kSeparator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (outputs.size() != 5) { |
| callback.Run(device_info); |
| return; |
| } |
| device_info.connected = true; |
| device_info.model = outputs[0]; |
| device_info.screen_size = ParseWindowPolicyResponse(outputs[1]); |
| StringMap pid_to_package; |
| StringMap pid_to_user; |
| MapProcessesToPackages(outputs[2], &pid_to_package, &pid_to_user); |
| StringMap socket_to_pid = MapSocketsToProcesses(outputs[3]); |
| StringMap id_to_username = MapIdsToUsers(outputs[4]); |
| std::set<std::string> used_pids; |
| for (const auto& pair : socket_to_pid) |
| used_pids.insert(pair.second); |
| |
| for (const auto& pair : pid_to_package) { |
| std::string pid = pair.first; |
| std::string package = pair.second; |
| if (used_pids.find(pid) == used_pids.end()) { |
| const BrowserDescriptor* descriptor = FindBrowserDescriptor(package); |
| if (descriptor) |
| socket_to_pid[descriptor->socket] = pid; |
| } |
| } |
| |
| for (const auto& pair : socket_to_pid) { |
| std::string socket = pair.first; |
| std::string pid = pair.second; |
| std::string package; |
| auto pit = pid_to_package.find(pid); |
| if (pit != pid_to_package.end()) |
| package = pit->second; |
| |
| AndroidDeviceManager::BrowserInfo browser_info; |
| browser_info.socket_name = socket; |
| browser_info.type = GetBrowserType(socket); |
| browser_info.display_name = |
| AndroidDeviceManager::GetBrowserName(socket, package); |
| |
| auto uit = pid_to_user.find(pid); |
| if (uit != pid_to_user.end()) |
| browser_info.user = GetUserName(uit->second, id_to_username); |
| |
| device_info.browser_info.push_back(browser_info); |
| } |
| std::sort(device_info.browser_info.begin(), |
| device_info.browser_info.end(), |
| &BrowserCompare); |
| callback.Run(device_info); |
| } |
| |
| } // namespace |
| |
| // static |
| std::string AndroidDeviceManager::GetBrowserName(const std::string& socket, |
| const std::string& package) { |
| if (package.empty()) { |
| // Derive a fallback display name from the socket name. |
| std::string name = socket.substr(0, socket.find(kDevToolsSocketSuffix)); |
| name[0] = base::ToUpperASCII(name[0]); |
| return name; |
| } |
| |
| const BrowserDescriptor* descriptor = FindBrowserDescriptor(package); |
| if (descriptor) |
| return descriptor->display_name; |
| |
| if (GetBrowserType(socket) == |
| AndroidDeviceManager::BrowserInfo::kTypeWebView) |
| return base::StringPrintf(kWebViewNameTemplate, package.c_str()); |
| |
| return package; |
| } |
| |
| // static |
| void AndroidDeviceManager::QueryDeviceInfo( |
| const RunCommandCallback& command_callback, |
| const DeviceInfoCallback& callback) { |
| command_callback.Run( |
| kAllCommands, |
| base::Bind(&ReceivedResponse, callback)); |
| } |