/* * Copyright (C) 2010 Marc A. Paradise * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.bbssh.util; import java.util.Calendar; import java.util.Date; import net.rim.blackberry.api.browser.Browser; import net.rim.device.api.crypto.SHA1Digest; import net.rim.device.api.i18n.ResourceBundle; import net.rim.device.api.i18n.ResourceBundleFamily; import net.rim.device.api.i18n.SimpleDateFormat; import net.rim.device.api.system.ApplicationDescriptor; import net.rim.device.api.system.DeviceInfo; import net.rim.device.api.ui.UiApplication; import net.rim.device.api.ui.component.Dialog; import org.bbssh.i18n.BBSSHResource; import org.bbssh.model.Settings; import org.bbssh.model.SettingsManager; import org.bbssh.platform.PlatformServicesProvider; /** * */ public class Version { public static String getVersionSSHIDString() { return "SSH-2.0-BBSSH_" + getAppVersion() + "." + getBuildNumber() + " BBSSH\n"; } public final static class VersionInfo { String versionString; public String toString() { return versionString; } public long getVersionNo() { return this.versionNo; } private long versionNo = 0; public VersionInfo(String version) throws IllegalArgumentException { version = version.trim(); String[] values = Tools.splitString(version, '.'); if (values.length == 4) { versionNo = Byte.parseByte(values[0]); versionNo = versionNo << 8; versionNo = versionNo | Byte.parseByte(values[1]); versionNo = versionNo << 16; versionNo = versionNo | Byte.parseByte(values[2]); // versionNo = versionNo << 32; // four bytes for build versionNo = versionNo | Integer.parseInt(values[3]); versionString = version + "(" + versionNo + ")"; } else if (values.length == 3) { versionNo = Byte.parseByte(values[0]); versionNo = versionNo << 8; versionNo = versionNo | Byte.parseByte(values[1]); versionNo = versionNo << 16; versionNo = versionNo | Byte.parseByte(values[2]); versionNo = versionNo << 32; versionString = version + "(" + versionNo + ")"; ; } else { throw new IllegalArgumentException("Version in bad format, expected X.Y.Z.b, got " + version); } } // [major] [minor] [tiny] [build #] // 0x FF FF FF FF FF FF FF FF FF private static final long LOW_8_MASK = 0x0000FFFFL; private static final long LOW_16_MASK = 0x0000FFFFL; private static final long LOW_32_MASK = 0xFFFFFFFFL; public String getBuildNumber() { return "" + (versionNo & LOW_32_MASK); } public String getMajor() { return "" + ((versionNo >> 54) & LOW_8_MASK); } public String getMinor() { return "" + ((versionNo >> 48) & LOW_16_MASK); } public String getTiny() { return "" + ((versionNo >> 32) & LOW_16_MASK); } } private static String version; private static VersionInfo versionInfo; private static void loadVersion() { version = ApplicationDescriptor.currentApplicationDescriptor().getVersion(); try { versionInfo = new VersionInfo(version); } catch (IllegalArgumentException e) { versionInfo = new VersionInfo("1.0.0.0"); } } private static String getOSVersionString() { String softwareVersion = DeviceInfo.getSoftwareVersion(); int pos = softwareVersion.indexOf('.'); if (softwareVersion.length() == 0 || pos == -1) return "4.5.0"; // default to minimum supported version. return softwareVersion; } private static String[] SUPPORTED_OS_VERSIONS = new String[] { "71", "70", "60", "50", "47", "46", "45" }; /** * This rather crude method will attempt to create the requested class by working backwards through the various * version strings. As an example, let's say you wnated to create an instance of "Foo" which had a platform-specific * version called Foo_45 for OS 4.5, and a minimal version Foo that was compatibile with 4.3 * * * Let's say you're running on an OS 4.3 device. This method would attempt to create classes as follows: Foo_60 * Foo_50- Foo_47 Foo_46 Foo_45 Foo and would succeed only on "Foo" -- because Foo_45 would not be present in the * classpath of the 4.3 build. If you were runing on a 4.5 or later device, you'd get Foo_45 assuming that Foo_45 * was the latest platform-specific version you'd created. * * If no match is found, it willr eturn a new instance of the base class as provided. In other words as long as the * class name provided is valid and has a public no-args constructor, it will always succeed. No exception is throw * because of this - so if you start seeing null pointer, you'll know where to look (hint: check to make sure you're * using class.getName, and NOT class.toString()). . * * This operation is based on the assumption that the highest available platform version is *always* the one we want * to instantiate. * * @param baseName fully qualified base name of the class instance to create. * @return appropriate instance of the requested class */ public static Object createOSObjectInstance(String baseName) { Class newClass = null; String className = ""; int count = SUPPORTED_OS_VERSIONS.length; for (int x = 0; x < count; x++) { className = baseName + "_" + SUPPORTED_OS_VERSIONS[x]; try { newClass = Class.forName(className); break; } catch (ClassNotFoundException e) { // That's OK - an OS-specific class won't exist for // every OS version. } } // No platform specific class exists to override the base version - so we'll use that one. if (newClass == null) { try { className = baseName; newClass = Class.forName(baseName); } catch (ClassNotFoundException e) { Logger.error("ClassNotFoundException when trying to create class: " + className + " -- did you use SomeClass.class.getName()?"); return null; } } try { String name = newClass.getName(); Logger.debug("Creating platform class instance: " + name); return newClass.newInstance(); } catch (InstantiationException e) { Logger.error("InstantiationException when trying to create class: " + className + " -- is there a no-args constructor?"); } catch (IllegalAccessException e) { Logger.error("IllegalAccessException when trying to create class: " + className + " -- is the no-args constructor public?"); } return null; } public static int getOSMajorVersion() { String softwareVersion = getOSVersionString(); int pos = softwareVersion.indexOf('.'); return Integer.parseInt(softwareVersion.substring(0, pos)); } public static int getOSMinorVersion() { String softwareVersion = getOSVersionString(); int pos = softwareVersion.indexOf('.'); int pos2 = softwareVersion.indexOf('.', pos + 1); return Integer.parseInt(softwareVersion.substring(pos + 1, pos2)); } public static String getAppVersion() { if (version == null) { loadVersion(); } return version; } public static long getVersionNumber() { if (versionInfo == null) { loadVersion(); } return versionInfo.getVersionNo(); } public static String getBuildNumber() { if (versionInfo == null) { loadVersion(); } return versionInfo.getBuildNumber(); } /** * Send a request to the designated BBSSH server to determine if the latest available version is a later one than * our current version. * * This request will also send some basic information to ensure that BBSSH is looking for the correct build for your * device: * <ul> * <li>A one-way hash of your pin which cannot be used to identify you as an individual, but statistically allows us * to know how many individual users we have.</li> * <li>Your hardware name (9000, 9630, 9700, etc)</li> * <li>Your OS version</li> * <li>Your platform version</li> * <li>Current BBSSH version</li> * </ul> * * @return true if your instaled version is at least the same as the one the server provides. */ private static boolean isUpToDate() { ResourceBundle res = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); // Make a non-reversible hash of the data so that we aren't gathering any personally identifiable info // even inadvertantly. int deviceId = DeviceInfo.getDeviceId(); StringBuffer toSend = new StringBuffer(res.getString(Version.isReleaseMode() ? BBSSHResource.URL_UPDATE : BBSSHResource.URL_UPDATE_DEV)); if (SettingsManager.getSettings().isAnonymousUsageStatsEnabled()) { SHA1Digest digest = new SHA1Digest(); digest.update(deviceId & 0xFF); deviceId = (deviceId >> 8); digest.update(deviceId & 0xFF); deviceId = (deviceId >> 8); digest.update(deviceId & 0xFF); deviceId = (deviceId >> 8); digest.update(deviceId & 0xFF); byte[] value = digest.getDigest(); toSend.append("&bbkey="). append(Tools.getBytesAsUnpaddedHexString(value, value.length)); if (DeviceInfo.isSimulator()) { toSend.append("-simulator"); } // @todo - extended capture: // number of macros // number of connections defined // number of times conneccted // hours used // other? } else { // In addition to not sending the unique ID derived from the // user's PIN, we will also send notice that the request is "anonymous". The server-side script // will avoid recording IP address when this is present; and excludes them from any stats, // This also prevents anonymous users from skewing aggregate stats, since we have no way to // know how many times a single anonymous user is checking for updates. toSend.append("&anon=true"); } toSend.append("&bbplatform=").append(DeviceInfo.getPlatformVersion()). append("&bbname=").append(DeviceInfo.getDeviceName()). append("&swversion=").append(DeviceInfo.getSoftwareVersion()).append("&bbssh="). append(getAppVersion()).append("&bbsshos="). append(PlatformServicesProvider.getInstance().getOSVersion()); Logger.info("Update check - GET URL is " + toSend.toString()); try { StringBuffer data = Tools.getHTTPFileContents(toSend.toString()); if (data != null && data.length() > 0) { String[] values = Tools.splitString(data.toString(), '|'); VersionInfo remote = new VersionInfo(values[0]); Logger.warn("Remote Version: " + remote + " Local Version: " + versionInfo); if (remote.versionNo > versionInfo.versionNo) { return false; } } else { Logger.error("Remote file load failed when checking for updates."); } } catch (Throwable ex) { Logger.error("Error checking for updates: " + ex.getMessage() + " url: " + toSend.toString()); } return true; } private static boolean releaseMode; public static void setReleaseMode(boolean b) { releaseMode = b; } public static boolean isReleaseMode() { return releaseMode; } private static boolean updateFound = false; /** * check for updates; if there are any, prompt the user to download them. * * @param force indicates to perform the update check no matter what - bypass user preference * * @return true if an update was available */ public static boolean checkAndPromptForUpdates(final boolean force) { Settings s = SettingsManager.getSettings(); // If 'force' is true the user has specifically // requested the update; otherwise we must verify that we're allowed to check. if (!force) { // Create a target date object that's 24*3600 (number of seconds in a day) Calendar target = Calendar.getInstance(); Calendar now = Calendar.getInstance(); target.setTime(new Date(s.getLastUpdateCheckTime() + ((s.getUpdateCheckInterval() * (24 * 3600)) * 1000))); now.setTime(new Date()); if (now.before(target)) { Logger.warn("Next update check : " + SimpleDateFormat. getInstance(SimpleDateFormat.DATE_DEFAULT).formatLocal(target.getTime().getTime())); return false; } if (!s.isAutoCheckUpdateEnabled() || !s.getRememberOption(Settings.REMEMBER_CHECKED_UPDATE_OK)) { Logger .warn("User has disabled auto-updates or they have not yet agreed to allow auto update checks."); return false; } } s.setLastUpdateCheckTime(new Date().getTime()); SettingsManager.getInstance().commitData(); if (Version.isUpToDate()) { return false; } updateFound = true; if (UiApplication.getUiApplication().isEventThread()) { promptForUpdate(); } else { UiApplication.getUiApplication().invokeAndWait(new Runnable() { public void run() { promptForUpdate(); } }); } return updateFound; } public static void goToDownloadSite() { ResourceBundleFamily res = ResourceBundleFamily.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); int url = Version.isReleaseMode() ? BBSSHResource.URL_DOWNLOAD : BBSSHResource.URL_DOWNLOAD_DEV; Browser.getDefaultSession().displayPage(res.getString(url)); } private static void promptForUpdate() { ResourceBundleFamily res = ResourceBundleFamily.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); int msg = Version.isReleaseMode() ? BBSSHResource.MSG_UPDATE_AVAILABLE : BBSSHResource.MSG_UPDATE_AVAILABLE_DEV; // Usage and update check are actually a single call. So if the // use enables usage reporting but does not include update check, // don't harass them with a popup. In addiotn - make sure that the user has // both been prompted for and approved for the auto update check before we actually run it. if (Dialog.ask(Dialog.D_YES_NO, res.getString(msg)) == Dialog.YES) { goToDownloadSite(); } } public static boolean doesAppVersionMatchOSVersion() { String osVersion = getOSVersionString(); String appVersion = PlatformServicesProvider.getInstance().getOSVersion(); int stop = osVersion.indexOf('.') ; if (stop < 1 || osVersion.length() < stop + 2 || appVersion.length() < stop + 2) return true; // we can't parse this version appVersion = appVersion.substring(0, stop + 2); osVersion = osVersion.substring(0, stop + 2); Logger.warn("OS Version: " + osVersion + " Compare to app version: " + appVersion); return osVersion.equals(appVersion); } }