/* * Created by Angel Leon (@gubatron), Alden Torres (aldenml) * Copyright (c) 2011-2013, FrostWire(R). All rights reserved. * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package com.bt.download.android.gui; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.util.HashSet; import java.util.Map; import java.util.Set; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; import com.bt.download.android.R; import com.bt.download.android.core.ConfigurationManager; import com.bt.download.android.core.Constants; import com.bt.download.android.core.HttpFetcher; import com.bt.download.android.core.SystemPaths; import com.bt.download.android.gui.services.Engine; import com.bt.download.android.gui.util.OSUtils; import com.bt.download.android.gui.util.UIUtils; import com.frostwire.util.StringUtils; import com.frostwire.util.ByteUtils; import com.frostwire.util.JsonUtils; import com.frostwire.uxstats.UXStats; import com.frostwire.uxstats.UXStatsConf; /** * * @author gubatron * @author aldenml * */ public final class SoftwareUpdater { public interface ConfigurationUpdateListener { void onConfigurationUpdate(); } private static final String TAG = "FW.SoftwareUpdater"; private static final long UPDATE_MESSAGE_TIMEOUT = 30 * 60 * 1000; // 30 minutes private static final String UPDATE_ACTION_OTA = "ota"; private static final String UPDATE_ACTION_MARKET = "market"; private boolean oldVersion; private String latestVersion; private Update update; private long updateTimestamp; private AsyncTask<Void, Void, Boolean> updateTask; private final Set<ConfigurationUpdateListener> configurationUpdateListeners; private static SoftwareUpdater instance; public static SoftwareUpdater instance() { if (instance == null) { instance = new SoftwareUpdater(); } return instance; } private SoftwareUpdater() { this.oldVersion = false; this.latestVersion = Constants.FROSTWIRE_VERSION_STRING; this.configurationUpdateListeners = new HashSet<ConfigurationUpdateListener>(); } public boolean isOldVersion() { return oldVersion; } public String getLatestVersion() { return latestVersion; } public void checkForUpdate(final Context context) { long now = System.currentTimeMillis(); if (now - updateTimestamp < UPDATE_MESSAGE_TIMEOUT) { return; } updateTimestamp = now; updateTask = new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(Void... params) { try { byte[] jsonBytes = new HttpFetcher(Constants.SERVER_UPDATE_URL).fetch(); update = JsonUtils.toObject(new String(jsonBytes), Update.class); latestVersion = update.v; String[] latestVersionArr = latestVersion.split("\\."); // lv = latest version byte[] lv = new byte[] { Byte.valueOf(latestVersionArr[0]), Byte.valueOf(latestVersionArr[1]), Byte.valueOf(latestVersionArr[2]) }; // mv = my version byte[] mv = Constants.FROSTWIRE_VERSION; oldVersion = isFrostWireOld(mv, lv); updateConfiguration(update); if (oldVersion) { if (update.a == null) { update.a = UPDATE_ACTION_OTA; // make it the old behavior } if (update.a.equals(UPDATE_ACTION_OTA)) { // did we download the newest already? if (downloadedLatestFrostWire(update.md5)) { return true; } // didn't download it? go get it now else { new HttpFetcher(update.u).save(SystemPaths.getUpdateApk()); if (downloadedLatestFrostWire(update.md5)) { return true; } } } else if (update.a.equals(UPDATE_ACTION_MARKET)) { return update.m != null; } } } catch (Throwable e) { Log.e(TAG, "Failed to check/retrieve/update the update information", e); } return false; } @Override protected void onPostExecute(Boolean result) { if (result && !isCancelled()) { notifyUpdate(context); } //nav menu always needs to be updated after we read the config. notifyConfigurationUpdateListeners(); } }; updateTask.execute(); } public void addConfigurationUpdateListener(ConfigurationUpdateListener listener) { try { configurationUpdateListeners.add(listener); } catch (Throwable t) { } } private void notifyUpdate(final Context context) { try { if (update.a == null) { update.a = UPDATE_ACTION_OTA; // make it the old behavior } if (update.a.equals(UPDATE_ACTION_OTA)) { if (!SystemPaths.getUpdateApk().exists()) { return; } String message = StringUtils.getLocaleString(update.updateMessages, context.getString(R.string.update_message)); UIUtils.showYesNoDialog(context, R.drawable.app_icon, message, R.string.update_title, new OnClickListener() { public void onClick(DialogInterface dialog, int which) { Engine.instance().stopServices(false); UIUtils.openFile(context, SystemPaths.getUpdateApk().getAbsolutePath(), Constants.MIME_TYPE_ANDROID_PACKAGE_ARCHIVE); } }); } else if (update.a.equals(UPDATE_ACTION_MARKET)) { String message = StringUtils.getLocaleString(update.marketMessages, context.getString(R.string.update_message)); UIUtils.showYesNoDialog(context, R.drawable.app_icon, message, R.string.update_title, new OnClickListener() { public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(update.m)); context.startActivity(intent); } }); } } catch (Throwable e) { Log.e(TAG, "Failed to notify update", e); } } /** * * @param md5 * - Expected MD5 hash as a string. * @return */ private boolean downloadedLatestFrostWire(String md5) { if (!SystemPaths.getUpdateApk().exists()) { return false; } return checkMD5(SystemPaths.getUpdateApk(), md5); } /** * mv = my version * lv = latest version * * returns true if mv is older. */ private boolean isFrostWireOld(byte[] mv, byte[] lv) { if (mv[0] < lv[0]) { return true; } if (mv[0] == lv[0] && mv[1] < lv[1]) { return true; } if (mv[0] == lv[0] && mv[1] == lv[1] && mv[2] < lv[2]) { return true; } return false; } private static String getMD5(File f) { try { MessageDigest m = MessageDigest.getInstance("MD5"); // We read the file in buffers so we don't // eat all the memory in case we have a huge plugin. byte[] buf = new byte[65536]; int num_read; InputStream in = new BufferedInputStream(new FileInputStream(f)); while ((num_read = in.read(buf)) != -1) { m.update(buf, 0, num_read); } in.close(); String result = new BigInteger(1, m.digest()).toString(16); // pad with zeros if until it's 32 chars long. if (result.length() < 32) { int paddingSize = 32 - result.length(); for (int i = 0; i < paddingSize; i++) { result = "0" + result; } } return result; } catch (Exception e) { return null; } } private static boolean checkMD5(File f, String expectedMD5) { if (expectedMD5 == null) { return false; } if (expectedMD5.length() != 32) { return false; } String checkedMD5 = getMD5(f); if (checkedMD5 == null) { return false; } return checkedMD5.trim().equalsIgnoreCase(expectedMD5.trim()); } private void updateConfiguration(Update update) { if (update.config == null) { return; } ConfigurationManager.instance().setBoolean(Constants.PREF_KEY_GUI_SUPPORT_FROSTWIRE_THRESHOLD, ByteUtils.randomInt(0, 100) < update.config.supportThreshold); if (update.config.activeSearchEngines != null && update.config.activeSearchEngines.keySet() != null) { for (String name : update.config.activeSearchEngines.keySet()) { SearchEngine engine = SearchEngine.forName(name); if (engine != null) { engine.setActive(update.config.activeSearchEngines.get(name)); } } } ConfigurationManager.instance().setBoolean(Constants.PREF_KEY_GUI_SHOW_TV_MENU_ITEM, update.config.tv); ConfigurationManager.instance().setBoolean(Constants.PREF_KEY_GUI_INITIALIZE_OFFERCAST_LOCKSCREEN, update.config.offercastLockScreen); ConfigurationManager.instance().setBoolean(Constants.PREF_KEY_GUI_INITIALIZE_APPIA, update.config.appia); ConfigurationManager.instance().setBoolean(Constants.PREF_KEY_GUI_USE_APPIA_SEARCH, update.config.appiaSearch); if (update.config.uxEnabled && ConfigurationManager.instance().getBoolean(Constants.PREF_KEY_UXSTATS_ENABLED)) { String url = "http://ux.frostwire.com/aux"; String os = OSUtils.getOSVersionString(); String fwversion = Constants.FROSTWIRE_VERSION_STRING; String fwbuild = Constants.FROSTWIRE_BUILD; int period = update.config.uxPeriod; int minEntries = update.config.uxMinEntries; int maxEntries = update.config.uxMaxEntries; UXStatsConf context = new UXStatsConf(url, os, fwversion, fwbuild, period, minEntries, maxEntries); UXStats.instance().setContext(context); } } private void notifyConfigurationUpdateListeners() { for (ConfigurationUpdateListener listener : configurationUpdateListeners) { try { listener.onConfigurationUpdate(); } catch (Throwable t) { } } } private static class Update { public String v; public String u; public String md5; /** * Address from the market */ public String m; /** * Action for the update message * - "ota" is download from 'u' (a regular http) * - "market" go to market page */ public String a; public Map<String, String> updateMessages; public Map<String, String> marketMessages; public Config config; } private static class Config { public int supportThreshold = 100; public Map<String, Boolean> activeSearchEngines; public boolean tv = true; public boolean appia = true; public boolean appiaSearch = true; public boolean offercastLockScreen = true; // ux stats public boolean uxEnabled = false; public int uxPeriod = 3600; public int uxMinEntries = 10; public int uxMaxEntries = 10000; } public void removeConfigurationUpdateListener(Object slideMenuFragment) { if (configurationUpdateListeners.size() > 0) { configurationUpdateListeners.remove(slideMenuFragment); } } }