| 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) { |
| } |
| } |
| } |