/* ############################################################################### # # # Copyright (C) 2011-2016 OpenMEAP, Inc. # # Credits to Jonathan Schang & Rob Thacher # # # # Released under the LGPLv3 # # # # OpenMEAP is free software: you can redistribute it and/or modify # # it under the terms of the GNU Lesser General Public License as published # # by the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # OpenMEAP is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU Lesser General Public License for more details. # # # # You should have received a copy of the GNU Lesser General Public License # # along with OpenMEAP. If not, see <http://www.gnu.org/licenses/>. # # # ############################################################################### */ package com.openmeap.thinclient.update; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; import com.openmeap.constants.FormConstants; import com.openmeap.http.HttpRequestException; import com.openmeap.http.HttpRequestExecuter; import com.openmeap.http.HttpRequestExecuterFactory; import com.openmeap.http.HttpResponse; import com.openmeap.protocol.ApplicationManagementService; import com.openmeap.protocol.WebServiceException; import com.openmeap.protocol.dto.Application; import com.openmeap.protocol.dto.ApplicationInstallation; import com.openmeap.protocol.dto.ConnectionOpenRequest; import com.openmeap.protocol.dto.ConnectionOpenResponse; import com.openmeap.protocol.dto.SLIC; import com.openmeap.protocol.dto.UpdateHeader; import com.openmeap.protocol.dto.UpdateType; import com.openmeap.thinclient.AppMgmtClientFactory; import com.openmeap.thinclient.LocalStorage; import com.openmeap.thinclient.LocalStorageException; import com.openmeap.thinclient.OmMainActivity; import com.openmeap.thinclient.OmWebView; import com.openmeap.thinclient.SLICConfig; import com.openmeap.util.GenericRuntimeException; import com.openmeap.util.Utils; /** * Handles all the business logic for performing an update. Pulled into SLIC * core for re-use between Android and RIM OS * * @author schang */ public class UpdateHandler { private SLICConfig config = null; private LocalStorage storage = null; private OmMainActivity activity = null; private Object interruptLock = new Object(); private Boolean interrupt = Boolean.FALSE; public UpdateHandler(OmMainActivity activity, SLICConfig config, LocalStorage storage) { this.activity = activity; this.setSLICConfig(config); this.setLocalStorage(storage); } private void setLocalStorage(LocalStorage storage2) { this.storage = storage2; } private void setSLICConfig(SLICConfig config2) { this.config = config2; } public void handleUpdate() throws WebServiceException { UpdateHeader update = checkForUpdate(); if (update != null) { this.handleUpdate(update); } } public UpdateHeader checkForUpdate() throws WebServiceException { // we'll go ahead and flip the flag that we tried to update now // at the beginning of our intent config.setLastUpdateAttempt(new Long(new Date().getTime())); // put together the communication coordination request ConnectionOpenRequest request = getConnectionOpenRequest(); ConnectionOpenResponse response = makeConnectionOpenRequest(request); // we'll use this from now till the next update config.setLastAuthToken(response.getAuthToken()); return response.getUpdate(); } public ConnectionOpenResponse makeConnectionOpenRequest( ConnectionOpenRequest request) throws WebServiceException { // phone home ApplicationManagementService client = AppMgmtClientFactory .newDefault(config.getAppMgmtServiceUrl()); ConnectionOpenResponse response = null; response = client.connectionOpen(request); return response; } /** * Uses the configuration to put together a ConnectionOpenRequest object. * * @return */ public ConnectionOpenRequest getConnectionOpenRequest() { ConnectionOpenRequest request = new ConnectionOpenRequest(); request.setApplication(new Application()); request.getApplication().setInstallation(new ApplicationInstallation()); request.setSlic(new SLIC()); Application app = request.getApplication(); app.setName(config.getApplicationName()); app.setVersionId(config.getApplicationVersion()); app.setHashValue(config.getArchiveHash() != null ? config .getArchiveHash() : ""); ApplicationInstallation dev = request.getApplication() .getInstallation(); dev.setUuid(config.getDeviceUuid()); SLIC slic = request.getSlic(); slic.setVersionId(SLICConfig.SLIC_VERSION); return request; } /** * Interface so that the HTML5 application can provide a call-back for * update status. * * @author schang */ public interface StatusChangeHandler { /** * Gets called each percent of completion throughout the duration of the * download * * @param header * @param bytesRemaining * @param complete */ public void onStatusChange(UpdateStatus update); } public void handleUpdate(UpdateHeader updateHeader) { handleUpdate(updateHeader, null); } /** * Handles processing an update requested by the application management * service * * @param updateHeader */ public void handleUpdate(final UpdateHeader updateHeader, final StatusChangeHandler eventHandler) { final UpdateStatus update = new UpdateStatus(updateHeader, 0, false); if (eventHandler != null) { Thread activeUpdateThread = new Thread(new Runnable() { public void run() { try { _handleUpdate(update, eventHandler); } catch (Exception e) { UpdateException ue = null; if (e instanceof UpdateException) { ue = (UpdateException) e; } else { ue = new UpdateException(UpdateResult.UNDEFINED, e.getMessage(), e); } config.setLastUpdateResult(ue.getUpdateResult() .toString()); update.setError(ue); eventHandler.onStatusChange(update); } } }); activeUpdateThread.start(); } else { try { _handleUpdate(update, eventHandler); } catch (Exception e) { UpdateException ue = null; if (e instanceof UpdateException) { ue = (UpdateException) e; } else { ue = new UpdateException(UpdateResult.UNDEFINED, e.getMessage(), e); } config.setLastUpdateResult(ue.getUpdateResult().toString()); throw new GenericRuntimeException(ue.getMessage(), ue); } } } public void clearInterruptFlag() { synchronized (interruptLock) { interrupt = Boolean.FALSE; } } public void interruptRunningUpdate() { synchronized (interruptLock) { interrupt = Boolean.TRUE; } } private void _handleUpdate(UpdateStatus update, StatusChangeHandler eventHandler) throws UpdateException { String lastUpdateResult = config.getLastUpdateResult(); boolean hasTimedOut = hasUpdatePendingTimedOut(); if (!hasTimedOut && lastUpdateResult != null && lastUpdateResult.compareTo(UpdateResult.PENDING.toString()) == 0) { return; } else { config.setLastUpdateResult(UpdateResult.PENDING.toString()); } UpdateHeader updateHeader = update.getUpdateHeader(); // if the new version is the original version, // then we'll just update the app version, delete internal storage and // return String versionId = updateHeader.getVersionIdentifier(); if (config.isVersionOriginal(versionId).booleanValue()) { _revertToOriginal(update, eventHandler); return; } if (!deviceHasEnoughSpace(update).booleanValue()) { // TODO: whether this is a deal breaker or not should be // configurable. client should have the ability to override default // behavior. default behavior should be informative throw new UpdateException(UpdateResult.OUT_OF_SPACE, "Need more space to install than is available"); } try { // TODO: whether this is a deal breaker or not should be // configurable. client should have the ability to override default // behavior. default behavior should be informative if (!downloadToArchive(update, eventHandler).booleanValue()) { return; } } catch (Exception ioe) { // TODO: whether this is a deal breaker or not should be // configurable. client should have the ability to override default // behavior. default behavior should be informative throw new UpdateException(UpdateResult.IO_EXCEPTION, "An issue occurred downloading the archive", ioe); } if (!archiveIsValid(update).booleanValue()) { throw new UpdateException(UpdateResult.HASH_MISMATCH, "The import archive integrity check failed"); } installArchive(update); // at this point, the archive should be of no use to us try { storage.deleteImportArchive(); } catch (LocalStorageException lse) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "Could not delete import archive", lse); } try { storage.resetStorage(); } catch (LocalStorageException lse) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "Could not reset storage", lse); } config.setLastUpdateResult(UpdateResult.SUCCESS.toString()); config.setApplicationVersion(update.getUpdateHeader() .getVersionIdentifier()); config.setArchiveHash(update.getUpdateHeader().getHash().getValue()); String newPrefix = storage.getStorageRoot() + update.getUpdateHeader().getHash().getValue(); config.setStorageLocation(newPrefix); config.setApplicationUpdated(Boolean.TRUE); if (eventHandler != null) { update.setComplete(true); eventHandler.onStatusChange(update); } else { activity.restart(); } } public Boolean deviceHasEnoughSpace(UpdateStatus update) throws UpdateException { // test to make sure the device has enough space for the installation Long avail; try { avail = storage.getBytesFree(); } catch (LocalStorageException e) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "Could not determine the number of bytes available", e); } return new Boolean(avail.longValue() > update.getUpdateHeader() .getInstallNeeds().longValue()); } /** * * @param update * @param eventHandler * @return true if completed, false if interrupted * @throws IOException */ public Boolean downloadToArchive(UpdateStatus update, StatusChangeHandler eventHandler) throws UpdateException { // download the file to import.zip OutputStream os = null; InputStream is = null; HttpRequestExecuter requester = HttpRequestExecuterFactory.newDefault(); HttpResponse updateRequestResponse; try { updateRequestResponse = requester.get(update.getUpdateHeader() .getUpdateUrl()); } catch (HttpRequestException e) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "An issue occurred fetching the update archive", e); } if (updateRequestResponse.getStatusCode() != 200) throw new UpdateException(UpdateResult.RESPONSE_STATUS_CODE, "Status was " + updateRequestResponse.getStatusCode() + ", expecting 200"); try { os = storage.getImportArchiveOutputStream(); is = updateRequestResponse.getResponseBody(); byte[] bytes = new byte[1024]; int count = is.read(bytes); int contentLength = (int) updateRequestResponse.getContentLength(); int contentDownloaded = 0; int lastContentDownloaded = contentDownloaded; int percent = contentLength / 100; while (count != (-1)) { os.write(bytes, 0, count); count = is.read(bytes); contentDownloaded += count; if (eventHandler != null && lastContentDownloaded + percent < contentDownloaded) { update.setBytesDownloaded(contentDownloaded); eventHandler.onStatusChange(update); lastContentDownloaded = contentDownloaded; } synchronized (interruptLock) { if (interrupt.booleanValue()) { clearInterruptFlag(); throw new UpdateException(UpdateResult.INTERRUPTED, "Download of archive was interrupted"); } } } } catch (IOException lse) { throw new UpdateException(UpdateResult.IO_EXCEPTION, lse.getMessage(), lse); } catch (LocalStorageException lse) { throw new UpdateException(UpdateResult.IO_EXCEPTION, lse.getMessage(), lse); } finally { try { storage.closeOutputStream(os); storage.closeInputStream(is); } catch (LocalStorageException e) { throw new UpdateException(UpdateResult.IO_EXCEPTION, e.getMessage(), e); } // have to hang on to the requester till the download is complete, // so that we can retain control over when the connection manager // shut's down requester.shutdown(); } return Boolean.TRUE; } public Boolean archiveIsValid(UpdateStatus update) throws UpdateException { try { // validate the zip file against the hash of the response InputStream fis = null; try { fis = storage.getImportArchiveInputStream(); String hashValue = Utils.hashInputStream(update .getUpdateHeader().getHash().getAlgorithm().value(), fis); if (!hashValue.equals(update.getUpdateHeader().getHash() .getValue())) { return Boolean.FALSE; } return Boolean.TRUE; } finally { storage.closeInputStream(fis); } } catch (Exception e) { throw new UpdateException(UpdateResult.UNDEFINED, "The archive failed validation", e); } } public void installArchive(UpdateStatus update) throws UpdateException { try { storage.unzipImportArchive(update); } catch (LocalStorageException e) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "The archive failed to install", e); } } private boolean hasUpdatePendingTimedOut() { Integer pendingTimeout = config.getUpdatePendingTimeout(); Long lastAttempt = config.getLastUpdateAttempt(); Long currentTime = new Long(new Date().getTime()); if (lastAttempt != null) { return currentTime.longValue() > lastAttempt.longValue() + (pendingTimeout.intValue() * 1000); } return true; } public void initialize(OmWebView webView) { // if this application is configured to fetch updates, // then check for them now activity.setReadyForUpdateCheck(false); Boolean shouldPerformUpdateCheck = activity.getConfig() .shouldPerformUpdateCheck(); webView = webView != null ? webView : activity.createDefaultWebView(); activity.runOnUiThread(new InitializeWebView(webView)); if (shouldPerformUpdateCheck != null && shouldPerformUpdateCheck.equals(Boolean.TRUE)) { new Thread(new UpdateCheck(webView)).start(); } } private void _revertToOriginal(UpdateStatus update, StatusChangeHandler eventHandler) throws UpdateException { config.setApplicationVersion(update.getUpdateHeader() .getVersionIdentifier()); config.setArchiveHash(update.getUpdateHeader().getHash().getValue()); try { storage.resetStorage(); } catch (LocalStorageException lse) { throw new UpdateException(UpdateResult.IO_EXCEPTION, "Could not reset storage", lse); } config.setLastUpdateResult(UpdateResult.SUCCESS.toString()); if (eventHandler != null) { update.setComplete(true); eventHandler.onStatusChange(update); } else { activity.restart(); } return; } private static String SOURCE_ENCODING = FormConstants.CHAR_ENC_DEFAULT; private static String CONTENT_TYPE = FormConstants.CONT_TYPE_HTML; private class InitializeWebView implements Runnable { private OmWebView webView; public InitializeWebView(OmWebView webView) { this.webView = webView; } public void run() { // here after, everything is handled by the html and javascript try { Boolean justUpdated = config.getApplicationUpdated(); if (justUpdated != null && justUpdated.booleanValue() == true) { config.setApplicationUpdated(Boolean.FALSE); } activity.setWebView(webView); String baseUrl = config.getAssetsBaseUrl(); // checking whether initial ArchieveHash is available if (config.getArchiveHash() != null && !config.getArchiveHash().equals("")) { webView.loadUrl("file:///"+config.getAssetsBaseUrl()+"/"+config.getStorageLocation()+"/index.html"); } else { //means new installation it is,hence, load from assets. webView.loadUrl(baseUrl + "index.html"); } activity.setContentView(webView); } catch (Exception e) { throw new GenericRuntimeException(e); } } } private class UpdateCheck implements Runnable { final private OmWebView webView; public UpdateCheck(OmWebView webView) { this.webView = webView; } public void run() { int count = 0; // TODO: timeout here should be configurable while (!activity.getReadyForUpdateCheck() && count < 500) { try { Thread.sleep(10); } catch (InterruptedException e) { ; } count++; } UpdateHeader update = null; WebServiceException err = null; try { update = checkForUpdate(); } catch (WebServiceException wse) { err = wse; } if (update != null && update.getType() == UpdateType.IMMEDIATE) { try { activity.runOnUiThread(new Runnable() { public void run() { activity.doToast( "APPLICATION UPDATE\n\nThe application will restart once the update is completed.", true); // webView.clearView(); } }); handleUpdate(update); storage.setupSystemProperties(); update = null; } catch (Exception e) { err = new WebServiceException( WebServiceException.TypeEnum.CLIENT_UPDATE, e.getMessage(), e); if (activity.getReadyForUpdateCheck()) { try { webView.setUpdateHeader(null, err, storage.getBytesFree()); } catch (LocalStorageException e2) { throw new GenericRuntimeException(e); } } } } else { try { webView.setUpdateHeader(update, err, storage.getBytesFree()); } catch (LocalStorageException e) { throw new GenericRuntimeException(e); } } } } }