blob: b2d703046f09ad07ea2742279895274452722a00 [file] [log] [blame]
package org.jetbrains.android.database;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.SyncService;
import com.android.tools.idea.ddms.DevicePropertyUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.io.URLUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* @author Eugene.Kudelevsky
*/
class AndroidDbUtil {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.database.AndroidDbUtil");
public static final Object DB_SYNC_LOCK = new Object();
public static final String TEMP_REMOTE_DB_PATH = "/data/local/tmp/intellij_temp_db_file";
public static final String TEMP_REMOTE_GET_MODIFICATION_TIME_TOOL_PATH =
"/data/local/tmp/intellij_native_tools/get_modification_time";
public static final long DB_COPYING_TIMEOUT_SEC = 30;
public static final int SHELL_COMMAND_TIMEOUT_SECONDS = 2;
private static final String DEVICE_ID_EMULATOR_PREFIX = "EMULATOR_";
private static final String DEVICE_ID_SERIAL_NUMBER_PREFIX = "SERIAL_NUMBER_";
private static final Pattern RUN_AS_UNKNOWN_PACKAGE_ERROR_PATTERN = Pattern.compile("run-as: Package '\\S+' is unknown");
private AndroidDbUtil() {
}
public static boolean uploadDatabase(@NotNull IDevice device,
@NotNull String packageName,
@NotNull String dbName,
boolean external,
@NotNull String localDbPath,
@NotNull final ProgressIndicator progressIndicator,
@NotNull AndroidDbErrorReporter errorReporter) {
try {
final SyncService syncService = device.getSyncService();
try {
syncService.pushFile(localDbPath, TEMP_REMOTE_DB_PATH, new MySyncProgressMonitor(progressIndicator));
}
finally {
syncService.close();
}
final String remoteDbPath = getDatabaseRemoteFilePath(packageName, dbName, external);
final String remoteDbDirPath = remoteDbPath.substring(0, remoteDbPath.lastIndexOf('/'));
MyShellOutputReceiver outputReceiver = new MyShellOutputReceiver(progressIndicator, device);
device.executeShellCommand(getRunAsPrefix(packageName, external) +
"mkdir " + remoteDbDirPath, outputReceiver,
DB_COPYING_TIMEOUT_SEC, TimeUnit.SECONDS);
String output = outputReceiver.getOutput();
if (!output.isEmpty() && !output.startsWith("mkdir failed")) {
errorReporter.reportError(output);
return false;
}
// recreating is needed for Genymotion emulator (IDEA-114732)
if (!external && !recreateRemoteFile(device, packageName, remoteDbPath, errorReporter, progressIndicator)) {
return false;
}
outputReceiver = new MyShellOutputReceiver(progressIndicator, device);
device.executeShellCommand(getRunAsPrefix(packageName, external) + "cat " + TEMP_REMOTE_DB_PATH + " >" + remoteDbPath,
outputReceiver, DB_COPYING_TIMEOUT_SEC, TimeUnit.SECONDS);
output = outputReceiver.getOutput();
if (!output.isEmpty()) {
errorReporter.reportError(output);
return false;
}
progressIndicator.checkCanceled();
}
catch (Exception e) {
errorReporter.reportError(e);
return false;
}
return true;
}
@NotNull
private static String getRunAsPrefix(@NotNull String packageName, boolean external) {
return external ? "" : "run-as " + packageName + " ";
}
private static boolean recreateRemoteFile(IDevice device,
String packageName,
String remotePath,
AndroidDbErrorReporter errorReporter,
ProgressIndicator progressIndicator) throws Exception {
MyShellOutputReceiver outputReceiver = new MyShellOutputReceiver(progressIndicator, device);
device.executeShellCommand("run-as " + packageName + " rm " + remotePath,
outputReceiver, DB_COPYING_TIMEOUT_SEC, TimeUnit.SECONDS);
String output = outputReceiver.getOutput();
if (!output.isEmpty() && !output.startsWith("rm failed")) {
errorReporter.reportError(output);
return false;
}
outputReceiver = new MyShellOutputReceiver(progressIndicator, device);
device.executeShellCommand("run-as " + packageName + " touch " + remotePath,
outputReceiver, DB_COPYING_TIMEOUT_SEC, TimeUnit.SECONDS);
output = outputReceiver.getOutput();
if (!output.isEmpty()) {
errorReporter.reportError(output);
return false;
}
return true;
}
public static boolean downloadDatabase(@NotNull IDevice device,
@NotNull String packageName,
@NotNull String dbName,
boolean external,
@NotNull File localDbFile,
@NotNull final ProgressIndicator progressIndicator,
@NotNull AndroidDbErrorReporter errorReporter) {
try {
final MyShellOutputReceiver receiver = new MyShellOutputReceiver(progressIndicator, device);
device.executeShellCommand(getRunAsPrefix(packageName, external) + "cat " +
getDatabaseRemoteFilePath(packageName, dbName, external) + " >" +
TEMP_REMOTE_DB_PATH, receiver,
DB_COPYING_TIMEOUT_SEC, TimeUnit.SECONDS);
final String output = receiver.getOutput();
if (!output.isEmpty()) {
errorReporter.reportError(output);
return false;
}
progressIndicator.checkCanceled();
final File parent = localDbFile.getParentFile();
if (!parent.exists()) {
if (!parent.mkdirs()) {
errorReporter.reportError("cannot create directory '" + parent.getPath() + "'");
return false;
}
}
final SyncService syncService = device.getSyncService();
try {
syncService.pullFile(TEMP_REMOTE_DB_PATH, localDbFile.getPath(), new MySyncProgressMonitor(progressIndicator));
}
finally {
syncService.close();
}
}
catch (Exception e) {
errorReporter.reportError(e);
return false;
}
return true;
}
@Nullable
public static AndroidDbConnectionInfo checkDataSource(@NotNull AndroidDataSource dataSource,
@NotNull AndroidDebugBridge debugBridge,
@NotNull AndroidDbErrorReporter errorReporter) {
final AndroidDataSource.State state = dataSource.getState();
final String deviceId = state.deviceId;
if (deviceId == null) {
errorReporter.reportError("device is not specified");
return null;
}
final IDevice device = getDeviceById(debugBridge, deviceId);
if (device == null) {
errorReporter.reportError("device '" + getPresentableNameFromDeviceId(deviceId) + "' is not connected");
return null;
}
if (!device.isOnline()) {
errorReporter.reportError("the device is not online");
return null;
}
final String packageName = dataSource.getState().packageName;
if (packageName == null || packageName.length() == 0) {
errorReporter.reportError("package name is not specified");
return null;
}
final String dbName = dataSource.getState().databaseName;
if (dbName == null || dbName.length() == 0) {
errorReporter.reportError("database name is not specified");
return null;
}
return new AndroidDbConnectionInfo(device, packageName, dbName, dataSource.getState().external);
}
@Nullable
private static IDevice getDeviceById(@NotNull AndroidDebugBridge debugBridge, @NotNull String deviceId) {
for (IDevice device : debugBridge.getDevices()) {
if (deviceId.equals(getDeviceId(device))) {
return device;
}
}
return null;
}
private static boolean installGetModificationTimeTool(@NotNull IDevice device,
@NotNull AndroidDbErrorReporter reporter,
@NotNull ProgressIndicator progressIndicator) {
String abi = device.getProperty("ro.product.cpu.abi");
if (abi == null) {
abi = "armeabi";
}
final String urlStr = "/native_tools/" + abi + "/get_modification_time";
final URL url = AndroidDbUtil.class.getResource(urlStr);
if (url == null) {
LOG.error("Cannot find resource " + urlStr);
return false;
}
final String remoteToolPath = TEMP_REMOTE_GET_MODIFICATION_TIME_TOOL_PATH;
if (!pushGetModificationTimeTool(device, url, reporter, progressIndicator, remoteToolPath)) {
return false;
}
final String chmodResult = executeSingleCommand(device, reporter, "chmod 755 " + remoteToolPath);
if (chmodResult == null) {
return false;
}
if (!chmodResult.isEmpty()) {
reporter.reportError(chmodResult);
return false;
}
return true;
}
private static boolean pushGetModificationTimeTool(@NotNull IDevice device,
@NotNull URL url,
@NotNull AndroidDbErrorReporter reporter,
@NotNull ProgressIndicator progressIndicator,
@NotNull String remotePath) {
final File toolLocalCopy;
try {
toolLocalCopy = FileUtil.createTempFile("android_get_modification_time_tool", "tmp");
}
catch (IOException e) {
reporter.reportError(e);
return false;
}
try {
if (!copyResourceToFile(url, toolLocalCopy, reporter)) {
return false;
}
try {
final SyncService service = device.getSyncService();
try {
service.pushFile(toolLocalCopy.getPath(), remotePath,
new MySyncProgressMonitor(progressIndicator));
}
finally {
service.close();
}
}
catch (Exception e) {
reporter.reportError(e);
return false;
}
}
finally {
FileUtil.delete(toolLocalCopy);
}
return true;
}
private static boolean copyResourceToFile(@NotNull URL url, @NotNull File file, @NotNull AndroidDbErrorReporter reporter) {
try {
final InputStream is = new BufferedInputStream(URLUtil.openStream(url));
final OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
try {
FileUtil.copy(is, os);
}
finally {
is.close();
os.close();
}
}
catch (IOException e) {
reporter.reportError(e);
return false;
}
return true;
}
@Nullable
public static Long getModificationTime(@NotNull IDevice device,
@NotNull final String packageName,
@NotNull String dbName,
boolean external,
@NotNull AndroidDbErrorReporter errorReporter,
@NotNull ProgressIndicator progressIndicator) {
final String path = TEMP_REMOTE_GET_MODIFICATION_TIME_TOOL_PATH;
final String lsResult = executeSingleCommand(device, errorReporter, "ls " + path);
if (lsResult == null) {
return null;
}
boolean reinstalled = false;
if (!lsResult.equals(path)) {
if (!installGetModificationTimeTool(device, errorReporter, progressIndicator)) {
return null;
}
reinstalled = true;
}
Long l = doGetModificationTime(device, packageName, dbName, external, errorReporter);
if (l != null) {
return l;
}
if (!reinstalled) {
// get_modification_time tools seems to be broken, so reinstall it for future
installGetModificationTimeTool(device, errorReporter, progressIndicator);
}
return null;
}
@Nullable
private static Long doGetModificationTime(@NotNull IDevice device,
@NotNull String packageName,
@NotNull String dbName,
boolean external,
@NotNull AndroidDbErrorReporter errorReporter) {
final String command = getRunAsPrefix(packageName, external) + TEMP_REMOTE_GET_MODIFICATION_TIME_TOOL_PATH +
" " + getDatabaseRemoteFilePath(packageName, dbName, external);
final String s = executeSingleCommand(device, errorReporter, command);
if (s == null) {
return null;
}
try {
return Long.parseLong(s);
}
catch (NumberFormatException e) {
errorReporter.reportError(s);
return null;
}
}
@Nullable
private static String executeSingleCommand(@NotNull IDevice device,
@NotNull AndroidDbErrorReporter errorReporter,
@NotNull String command) {
final MyShellOutputReceiver receiver = new MyShellOutputReceiver(null, device);
try {
device.executeShellCommand(command, receiver, SHELL_COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
catch (Exception e) {
errorReporter.reportError(e);
return null;
}
return receiver.getOutput();
}
@Nullable
public static String getDeviceId(@NotNull IDevice device) {
if (device.isEmulator()) {
String avdName = device.getAvdName();
return avdName == null ? null : DEVICE_ID_EMULATOR_PREFIX + replaceByDirAllowedName(avdName);
}
else {
final String serialNumber = device.getSerialNumber();
if (serialNumber != null && serialNumber.length() > 0) {
return DEVICE_ID_SERIAL_NUMBER_PREFIX + replaceByDirAllowedName(serialNumber);
}
final String manufacturer = DevicePropertyUtil.getManufacturer(device, "");
final String model = DevicePropertyUtil.getModel(device, "");
if (manufacturer.length() > 0 || model.length() > 0) {
return replaceByDirAllowedName(manufacturer + "_" + model);
}
return null;
}
}
@NotNull
public static String getPresentableNameFromDeviceId(@NotNull String deviceId) {
if (deviceId.startsWith(DEVICE_ID_EMULATOR_PREFIX)) {
return "emulator: " + deviceId.substring(DEVICE_ID_EMULATOR_PREFIX.length());
}
if (deviceId.startsWith(DEVICE_ID_SERIAL_NUMBER_PREFIX)) {
return "serial: " + deviceId.substring(DEVICE_ID_SERIAL_NUMBER_PREFIX.length());
}
return deviceId;
}
@NotNull
private static String replaceByDirAllowedName(@NotNull String s) {
final StringBuilder builder = new StringBuilder();
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (!Character.isJavaIdentifierPart(c)) {
c = '_';
}
builder.append(c);
}
return builder.toString();
}
@NotNull
public static String getInternalDatabasesRemoteDirPath(@NotNull String packageName) {
return "/data/data/" + packageName + "/databases";
}
@NotNull
public static String getDatabaseRemoteFilePath(@NotNull String packageName, @NotNull String dbName, boolean external) {
if (dbName.startsWith("/")) {
dbName = dbName.substring(1);
}
if (!external) {
return getInternalDatabasesRemoteDirPath(packageName) + "/" + dbName;
}
return "$EXTERNAL_STORAGE/Android/data/" + packageName + "/" + dbName;
}
private static class MyShellOutputReceiver extends MultiLineReceiver {
@Nullable private final ProgressIndicator myProgressIndicator;
private final StringBuilder myOutputBuilder = new StringBuilder();
private final boolean myAndroid43;
public MyShellOutputReceiver(@Nullable ProgressIndicator progressIndicator, @NotNull IDevice device) {
myProgressIndicator = progressIndicator;
myAndroid43 = "18".equals(device.getProperty("ro.build.version.sdk"));
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
String s = line.trim();
if (s.length() > 0) {
LOG.debug("ADB_SHELL: " + s);
if (myOutputBuilder.length() > 0) {
myOutputBuilder.append('\n');
}
myOutputBuilder.append(s);
if (myAndroid43 && RUN_AS_UNKNOWN_PACKAGE_ERROR_PATTERN.matcher(s).matches()) {
myOutputBuilder.append(". \nUnfortunately database support doesn't work for Android 4.3 devices because of the bug " +
"https://code.google.com/p/android/issues/detail?id=58373");
}
}
}
}
@Override
public boolean isCancelled() {
return myProgressIndicator != null && myProgressIndicator.isCanceled();
}
@NotNull
public String getOutput() {
return myOutputBuilder.toString();
}
}
private static class MySyncProgressMonitor implements SyncService.ISyncProgressMonitor {
private final ProgressIndicator myProgressIndicator;
public MySyncProgressMonitor(@NotNull ProgressIndicator progressIndicator) {
myProgressIndicator = progressIndicator;
}
@Override
public void start(int totalWork) {
}
@Override
public void stop() {
}
@Override
public boolean isCanceled() {
return myProgressIndicator.isCanceled();
}
@Override
public void startSubTask(String name) {
}
@Override
public void advance(int work) {
}
}
}