/* * Copyright (C) 2012 The Android Open Source Project * * 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. */ package com.motorolamobility.studio.android.db.devices.model; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.datatools.sqltools.result.ResultsViewAPI; import org.eclipse.datatools.sqltools.result.core.IResultManagerListener; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.osgi.util.NLS; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.console.IOConsoleOutputStream; import com.motorola.studio.android.adt.DDMSFacade; import com.motorola.studio.android.adt.DDMSUtils; import com.motorola.studio.android.common.log.StudioLogger; import com.motorola.studio.android.common.utilities.EclipseUtils; import com.motorola.studio.android.common.utilities.FileUtil; import com.motorolamobility.studio.android.db.core.CanRefreshStatus; import com.motorolamobility.studio.android.db.core.DbCoreActivator; import com.motorolamobility.studio.android.db.core.exception.MotodevDbException; import com.motorolamobility.studio.android.db.core.model.DbModel; import com.motorolamobility.studio.android.db.core.model.TableModel; import com.motorolamobility.studio.android.db.core.ui.AbstractDbResultManagerAdapter; import com.motorolamobility.studio.android.db.core.ui.DbNode; import com.motorolamobility.studio.android.db.core.ui.IDbNode; import com.motorolamobility.studio.android.db.core.ui.ITableNode; import com.motorolamobility.studio.android.db.core.ui.ITreeNode; import com.motorolamobility.studio.android.db.devices.DbDevicesPlugin; import com.motorolamobility.studio.android.db.devices.i18n.DbDevicesNLS; /** * This class represents a tree node for a given SQLite3 database file located on a Android device. */ public class DeviceDbNode extends DbNode implements IDbNode { /** * */ private static final int REMOTE_OPERATIONS_TIMEOUT = 2000; private final IPath remoteDbPath; private final String serialNumber; private IResultManagerListener resultManagerListener; private String localFileMd5; public boolean isDirty; private class ResultManagerListener extends AbstractDbResultManagerAdapter { /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.AbstractDbResultManagerAdapter#statementExecuted(java.lang.String, java.lang.String) */ @Override public void statementExecuted(String profilename, String sqlStatement) { if ((model != null) && model.getProfileName().equals(profilename)) { if ((!sqlStatement.equals("Group Execution")) //$NON-NLS-1$ && (sqlStatement.trim().toLowerCase().indexOf("select") != 0) //$NON-NLS-1$ && (!sqlStatement.trim().equals(""))) //$NON-NLS-1$ { IStatus status = checkMd5Sum(true); if (status.isOK()) { status = pushLocalDbFile(); } if (!status.isOK()) { isDirty = true; } } } } }; /** * Creates a new DeviceDbNode for an already existent SQLite3 database * @param remoteDbPath the SQLite3 database file location at the device * @param parent this node parent */ public DeviceDbNode(IPath remoteDbPath, String serialNumber, ITreeNode parent) { super(parent); setId(serialNumber + "." + remoteDbPath.toString()); //$NON-NLS-1$ this.remoteDbPath = remoteDbPath; this.serialNumber = serialNumber; setName(remoteDbPath.lastSegment()); ImageDescriptor icon = DbDevicesPlugin.imageDescriptorFromPlugin(DbCoreActivator.PLUGIN_ID, DbNode.ICON_PATH); setIcon(icon); setTooltip(NLS.bind(DbDevicesNLS.DeviceDbNode_Tootip_Prefix, remoteDbPath.toString())); } /** * Creates a new DeviceDbNode by creating a new SQLite3 database file if requested. * This constructor will create a local temp file with the new SQLite3 database. the temp file will then be copied to the remotePath at the device. * @param remoteDbPath The SQLite database File location at the device * @param parent The parent of the new node. * @param create set this flag to true if you want to create a new db file, if the flag is false the behavior is the same as the constructor DeviceDbNode(IPath remoteDbPath, String serialNumber, ITreeNode parent) * @throws MotodevDbException if a problem occurred during database creation. */ public DeviceDbNode(IPath remoteDbPath, String serialNumber, ITreeNode parent, boolean create) throws MotodevDbException { this(remoteDbPath, serialNumber, parent); if (create) { try { File tempFile = getLocalTempFile(); Path localDbPath = null; if (tempFile != null) { localDbPath = new Path(tempFile.getAbsolutePath()); model = new DbModel(localDbPath, create, true); IStatus status = pushLocalDbFile(false); if (!status.isOK()) { deleteLocalDbModel(); throw new MotodevDbException( DbDevicesNLS.DeviceDbNode_Create_Device_Db_Failed); } } else { throw new MotodevDbException( DbDevicesNLS.DeviceDbNode_Could_Not_Create_DeviceDbNode); } } catch (IOException e) { throw new MotodevDbException( DbDevicesNLS.DeviceDbNode_Could_Not_Create_DeviceDbNode, e); } } } /** * @return * @throws IOException */ private File getLocalTempFile() throws IOException { IPreferenceStore preferenceStore = DbDevicesPlugin.getDefault().getPreferenceStore(); File tempLocationFile = null; if (!preferenceStore.isDefault(DbDevicesPlugin.DB_TEMP_PATH_PREFERENCE)) { String tempLocation = preferenceStore.getString(DbDevicesPlugin.DB_TEMP_PATH_PREFERENCE); tempLocationFile = new File(tempLocation); if (!tempLocationFile.isDirectory() || !FileUtil.canWrite(tempLocationFile)) { EclipseUtils.showErrorDialog(DbDevicesNLS.ERR_DbUtils_Local_Db_Title, NLS.bind(DbDevicesNLS.ERR_DbUtils_Local_Db_Msg, tempLocation)); preferenceStore.setToDefault(DbDevicesPlugin.DB_TEMP_PATH_PREFERENCE); } } //If tempLocationFile is null the file will be created on system's default temp dir. File tempFile = File.createTempFile(serialNumber + "_" + remoteDbPath.segment(1) + "_" + getName(), //$NON-NLS-1$ //$NON-NLS-2$ "db", tempLocationFile); //$NON-NLS-1$ tempFile.deleteOnExit(); return tempFile; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#connect() */ @Override public IStatus connect() { IStatus status = null; File tempFile = null; try { tempFile = getLocalTempFile(); status = pullRemoteTempFile(tempFile); } catch (IOException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Create_Temp_Local_Db_Failed, e); } if ((model != null) && status.isOK()) //Local model already exists, we must verify the md5 and update the localDbModel if needed. { try { String newMd5Sum = FileUtil.calculateMd5Sum(tempFile); if (!newMd5Sum.equals(localFileMd5)) { deleteLocalDbModel(); //Remote file has been changed. localDbModel must be updated } } catch (IOException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Calculate_Local_Md5_Failed, e); } } //model will be null if the remote file has been changed. if ((model == null) && status.isOK()) { try { model = new DbModel(Path.fromOSString(tempFile.getAbsolutePath())); } catch (MotodevDbException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, e.getMessage()); } } if ((model != null) && status.isOK()) { try { localFileMd5 = getLocalMd5Sum(); model.connect(); } catch (IOException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Calculate_Local_Md5_Failed, e); } } if (status.isOK()) { if (resultManagerListener == null) { resultManagerListener = new ResultManagerListener(); ResultsViewAPI.getInstance().getResultManager() .addResultManagerListener(resultManagerListener); } isDirty = false; } setNodeStatus(status); return status != null ? status : Status.OK_STATUS; } /** * @return * @throws IOException */ private String getLocalMd5Sum() throws IOException { return FileUtil.calculateMd5Sum(model.getDbPath().toFile()); } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#disconnect() */ @Override public IStatus disconnect() { IStatus status = Status.OK_STATUS; if ((model != null) && model.isConnected()) { boolean canDisconnect = true; status = closeAssociatedEditors(); canDisconnect = status.isOK(); if (canDisconnect) { status = model.disconnect(); if (status.isOK()) { deleteLocalDbModel(); if (resultManagerListener != null) { ResultsViewAPI.getInstance().getResultManager() .removeResultManagerListener(resultManagerListener); resultManagerListener = null; } } clear(); setNodeStatus(status); } } return status; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#createTables(java.util.List) */ @Override public IStatus createTables(List<TableModel> tables) { IStatus status; status = checkMd5Sum(true); if (status.isOK()) { status = super.createTables(tables); if (status.isOK()) { pushLocalDbFile(); } } return status; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#createTable(com.motorolamobility.studio.android.db.core.model.TableModel) */ @Override public IStatus createTable(TableModel table) { IStatus status; status = checkMd5Sum(true); if (status.isOK()) { status = super.createTable(table); if (status.isOK()) { pushLocalDbFile(); } } return status; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#deleteTable(java.lang.String) */ @Override public IStatus deleteTable(ITableNode tableNode) { IStatus status; status = checkMd5Sum(true); if (status.isOK()) { status = super.deleteTable(tableNode); if (status.isOK()) { pushLocalDbFile(); } } return status; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.IDbNode#deleteDb() */ @Override public IStatus deleteDb() { IStatus status = null; try { closeAssociatedEditors(true, forceCloseEditors); DDMSFacade.deleteFile(serialNumber, remoteDbPath.toString()); disconnect(); } catch (IOException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, NLS.bind(DbDevicesNLS.DeviceDbNode_Delete_Remote_File_Failed, remoteDbPath.toString(), DDMSFacade.getNameBySerialNumber(serialNumber))); } return status != null ? status : Status.OK_STATUS; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.AbstractTreeNode#canRefresh() */ @Override public IStatus canRefresh() { IStatus status = null; if (isDirty) { status = checkMd5Sum(false); if (!status.isOK()) { status = new CanRefreshStatus(CanRefreshStatus.ASK_USER | CanRefreshStatus.CANCELABLE, DbDevicesPlugin.PLUGIN_ID, NLS.bind( DbDevicesNLS.DeviceDbNode_DBOutOfSync_Refresh_Message, getName())); } } else { Set<IEditorPart> associatedEditors = getAssociatedEditors(); if (!associatedEditors.isEmpty()) { status = new CanRefreshStatus(CanRefreshStatus.ASK_USER, DbDevicesPlugin.PLUGIN_ID, NLS.bind(DbDevicesNLS.DeviceDbNode_RefreshQuestion, getName())); } } return status != null ? status : Status.OK_STATUS; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.AbstractTreeNode#refresh() */ @Override public void refresh() { if (model != null) { if (model.isConnected()) { IStatus checkMd5Sum = checkMd5Sum(false); if (!checkMd5Sum.isOK()) { model.disconnect(); deleteLocalDbModel(); clear(); } } } IStatus status = Status.OK_STATUS; if ((model == null) || !model.isConnected()) { status = connect(); //Force getting a fresh device db file } if (status.isOK()) { super.refresh(); } } private boolean deleteLocalDbModel() { IStatus deleteDb = model.deleteDb(); model = null; return deleteDb.isOK(); } private IStatus pullRemoteTempFile(File tempFile) { IStatus status = null; IOConsoleOutputStream stream = null; try { IPath localDbPath = new Path(tempFile.getAbsolutePath()); List<File> localList = Arrays.asList(new File[] { localDbPath.toFile() }); List<String> remoteList = Arrays.asList(new String[] { remoteDbPath.toString() }); stream = EclipseUtils.getStudioConsoleOutputStream(false); status = DDMSFacade.pullFiles(serialNumber, localList, remoteList, REMOTE_OPERATIONS_TIMEOUT, new NullProgressMonitor(), stream); } catch (Exception e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Create_Temp_Local_Db_Failed, e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { StudioLogger.error("Could not close stream: ", e.getMessage()); //$NON-NLS-1$ } } } return status != null ? status : Status.OK_STATUS; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.AbstractTreeNode#refresh(boolean) */ @Override public void refresh(boolean canRefreshYesResponse) { if (canRefreshYesResponse) { closeAssociatedEditors(false, true); } else { pushLocalDbFile(false); } refresh(); } private IStatus pushLocalDbFile() { return pushLocalDbFile(true); } private IStatus pushLocalDbFile(boolean warnUser) { IStatus status = null; IOConsoleOutputStream stream = null; try { IPath localDbPath = model.getDbPath(); File localDbFile = localDbPath.toFile(); List<File> localList = Arrays.asList(new File[] { localDbFile }); List<String> remoteList = Arrays.asList(new String[] { remoteDbPath.toString() }); stream = EclipseUtils.getStudioConsoleOutputStream(false); status = DDMSFacade.pushFiles(serialNumber, localList, remoteList, REMOTE_OPERATIONS_TIMEOUT, new NullProgressMonitor(), stream); if (status.isOK()) { isDirty = false; } //Update the local Md5Sum everytime the file is pushed to the device. localFileMd5 = FileUtil.calculateMd5Sum(localDbFile); String appName = getParent().getName(); if (warnUser) { boolean applicationRunning = DDMSFacade.isApplicationRunning(serialNumber, appName); if (applicationRunning) { EclipseUtils.showInformationDialog( DbDevicesNLS.DeviceDbNode_Application_Running_Msg_Title, NLS .bind(DbDevicesNLS.DeviceDbNode_Application_Running_Msg_Text, appName)); } } } catch (Exception e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, NLS.bind( DbDevicesNLS.DeviceDbNode_Push_Local_File_To_Device_Failed, serialNumber), e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { StudioLogger.error("Could not close stream: ", e.getMessage()); //$NON-NLS-1$ } } } return status != null ? status : Status.OK_STATUS; } /** * @param status * @return */ private IStatus checkMd5Sum(boolean warnUser) { File tempFile = null; IStatus status = null; if (localFileMd5 != null) //It will be null during create Db process. { try { tempFile = getLocalTempFile(); //Create a new tempFile, different from the local db model file, in order to compare MD5 sum. status = pullRemoteTempFile(tempFile); String newMd5Sum = FileUtil.calculateMd5Sum(tempFile); if (!localFileMd5.equals(newMd5Sum)) { if (warnUser) { boolean canOverwrite = EclipseUtils.showQuestionDialog( DbDevicesNLS.DeviceDbNode_Remote_File_Modified_Title, NLS.bind( DbDevicesNLS.DeviceDbNode_Remote_File_Modified_Msg, getName())); if (!canOverwrite) { status = new Status(IStatus.CANCEL, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_User_Canceled_Overwrite); } } else { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Md5Sum_Differs); } } } catch (IOException e) { status = new Status(IStatus.ERROR, DbDevicesPlugin.PLUGIN_ID, DbDevicesNLS.DeviceDbNode_Create_Temp_Local_Db_Failed, e); } finally { if (tempFile != null) { tempFile.delete(); } } } return status != null ? status : Status.OK_STATUS; } public boolean remoteFileExists() { boolean remoteFileExists = false; try { remoteFileExists = DDMSUtils.remoteFileExists(serialNumber, remoteDbPath.toString()); } catch (IOException e) { //Return false on error } return remoteFileExists; } /** * @return the remoteDbPath */ public IPath getRemoteDbPath() { return remoteDbPath; } /* (non-Javadoc) * @see com.motorolamobility.studio.android.db.core.ui.DbNode#clean() */ @Override public void cleanUp() { if (DDMSFacade.isDeviceOnline(serialNumber)) { super.cleanUp(); } else { closeAssociatedEditors(true, forceCloseEditors); clear(); } } }