/* * Copyright 2013 WonderMedia Technologies, Inc. All Rights Reserved. * * This PROPRIETARY SOFTWARE is the property of WonderMedia Technologies, Inc. * and may contain trade secrets and/or other confidential information of * WonderMedia Technologies, Inc. This file shall not be disclosed to any third party, * in whole or in part, without prior written consent of WonderMedia. * * THIS PROPRIETARY SOFTWARE AND ANY RELATED DOCUMENTATION ARE PROVIDED AS IS, * WITH ALL FAULTS, AND WITHOUT WARRANTY OF ANY KIND EITHER EXPRESS OR IMPLIED, * AND WonderMedia TECHNOLOGIES, INC. DISCLAIMS ALL EXPRESS OR IMPLIED WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. */ package com.wmt.wondertv; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONObject; import com.wmt.wondertv.netmedia.WonderDB; import com.wmt.wondertv.setting.UpdateConfig; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.util.Log; public final class UpdateManager { private static final String TAG = "UpdateManager"; private static boolean DEBUG = true; private static final String CAB_EXT = ".zip"; public static final String NAME_HOT = "hot"; public static final String NAME_VIDEO = "video"; public static final String NAME_TV = "tv"; public static final String NAME_APK = "apk"; //start modify by tangzefang public static final String NAME_SCREENSHORTS = "screenshots"; public static int LOCAL_VERSION = -1; //end modify by tangzefang private static final String SERV_UPDATE_FOLDER="update/"; private static final String STORAGE_PATH = "/data/data/" + UpdateManager.class.getPackage().getName() + "/download/"; private static ArrayList<Listener> sListeners = new ArrayList<UpdateManager.Listener>(); private static UpdateInfo sUpdateInfo = new UpdateInfo(); public static final class UpdateInfo { public long busywaittime; public String serverRedirect; public String updateToken; //other time stamp are save to UpdateItem public String toString() { //TODO: more... return "token=" + updateToken; } private static long parseLong(final JSONObject obj, String node, long defValue){ try { return Long.parseLong(obj.getString(node)); } catch(Exception e){ if(DEBUG) Log.v(TAG, node + "parse error:", e); return defValue; } } private static String parseString(final JSONObject obj, String node, String defValue){ try { return obj.getString(node); } catch(Exception e){ if(DEBUG) Log.v(TAG, node + "parse error:", e); return defValue; } } private void updateFromJson(final JSONObject obj) { serverRedirect = parseString(obj, "serverRedirect", ""); busywaittime = parseLong(obj, "busywaittime", 3600*1000); updateToken = parseString(obj, "updateToken", ".*"); } }; public static final class UpdateItem { UpdateItem(String name) { this.name = name; this.localVersion = Util.getPropertyKeyLong(this.name, 0); this.serverVersion = 0; } public String name; public long serverVersion; public File unzipFolder; /* unzipped folder can be used now */ private int index; private long localVersion; /* a time stamp version */ public String toString() { return name + ",version=" + localVersion + ",serverVersion=" + serverVersion; } }; private static final ArrayList<UpdateItem> sUpdateItems = new ArrayList<UpdateItem>(); private static final HandlerThread sUpdateThread = new HandlerThread("Update"); private static UpdateHandler sUpdateHandler; private static File sUpdateTempDir; private static final int MSG_GETUPDATEINFO = 1; private static final int MSG_UPDATE_CATALOG = 100; private static final int MANUAL_UPDATE = 1; public static void startup(Context context){ if( null != sUpdateHandler) return; sUpdateTempDir = context.getDir("update", Context.MODE_WORLD_READABLE); if(DEBUG) Log.d(TAG, "sUpdateTempDir is " + sUpdateTempDir.getAbsolutePath()); sUpdateItems.add( new UpdateItem(NAME_HOT)); sUpdateItems.add( new UpdateItem(NAME_VIDEO)); sUpdateItems.add( new UpdateItem(NAME_TV)); //modify by tangzefang sUpdateItems.add(new UpdateItem(NAME_SCREENSHORTS)); UpdateItem apk = new UpdateItem(NAME_APK); sUpdateItems.add( apk); for(int i = 0; i < sUpdateItems.size(); i++){ sUpdateItems.get(i).index = i; } //if not download apk, use the current apk version. // apk is special, we get it from current apk version. // note: we won't re-download as we checked later. apk.localVersion = UpdateConfig.getVerCode(context); //modify by tangzefang LOCAL_VERSION = UpdateConfig.getVerCode(context); sUpdateThread.start(); sUpdateHandler = new UpdateHandler(sUpdateThread.getLooper()); Message msg = sUpdateHandler.obtainMessage(MSG_GETUPDATEINFO); //delay few seconds to start talk with server sUpdateHandler.sendMessageDelayed(msg, 3000); } public static interface Listener { /** * report server's update info * @param info null if server error */ public void onServerUpdateInfo(final UpdateInfo info); /** * report this item is need to download or not * @param item * @param needUpdate */ public void onUpdateNeed(final UpdateItem item, final boolean needUpdate); /** * * @param item the downloading item * @param newPrecent 0-100, -1 if error. * When precent reach to 100, soon you will get onUpdateAvaliable callback. * So don't do real update in this callback function. */ public void onDownloadProgress(final UpdateItem item, final int precent); /** * item.unzipFolder now contains unzip content can be upgrade now. * @param item */ public void onUpdateAvaliable(final UpdateItem item, final UpdateInfo updateInfo); }; /** * register listener * @param l Listener object * can be null if interest all items. */ public static void registerListener(Listener l){ synchronized (sListeners) { sListeners.add(l); } } //Note: don't call this function in Listener's callback otherwise dead-lock happens. public static void unregisterListener(Listener l){ synchronized (sListeners) { sListeners.remove(l); } } /** * return listener */ public static ArrayList<Listener> getListener(){ synchronized (sListeners) { return sListeners; } } /** * clean listener */ public static void cleanListener(){ synchronized (sListeners) { sListeners.clear(); } } public static void manualRefresh(){ cleanPendingMessages(); //then start the new one. Message msg = sUpdateHandler.obtainMessage(MSG_GETUPDATEINFO); msg.arg2 = MANUAL_UPDATE; /* 1 means manual update */ //msg.sendToTarget(); //delay few seconds to start talk with server sUpdateHandler.sendMessageDelayed(msg, 2000); } public static File getItemFolder(String name){ long version = Util.getPropertyKeyLong(name, 0); return getUnZipFolder(name, version); } public static InputStream getDownloaded(final File file) { FileInputStream inputStream = null; if (file.exists() && file.isFile()) { Log.v(TAG, "getDownloaded:" + file.getPath()); try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { Log.v(TAG, "getDownloaded file is not existing:" + file.getPath()); } return inputStream; } public static InputStream openUpdateXMLFile(String name) { File file = getItemFolder(name); file = new File(file, name + ".xml"); FileInputStream inputStream = null; if (file.exists() && file.isFile()) { Log.v(TAG, "openUpdateXMLFile:" + file.getPath()); try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } } else { Log.v(TAG, "openUpdateXMLFile not existing:" + file.getPath()); } return inputStream; } //Used for apk mostly. //apk.zip contains a changes.txt + apk. public static String getChangeLog(String name) { File file = getItemFolder(name); file = new File(file, "changes.txt"); return Util.getFileString(file); } public static String getDefaultDictory() { return STORAGE_PATH; } public static void updateDone(String name) { if(DEBUG) Log.v(TAG, name + " updated done"); long version = Util.getPropertyKeyLong(name, 0); //now we clean the old folders start with name_xxx File latest = getUnZipFolder(name, version); cleanUnusedFolder(name, latest); //update item's local version too. for(UpdateItem item: sUpdateItems) { if( item.name.equals(name)){ item.localVersion = version; break; } } } /////////////////////////////////////////////////////////////////////////////////////// //// Blow are implementation /////////////////////////////////////////////////////////////////////////////////////// public static void cleanUnusedFolder(String prefix, File exclude) { for(File f : sUpdateTempDir.listFiles()){ if( f.isDirectory() && f.getName().startsWith(prefix + "_") && !f.getAbsolutePath().equals(exclude.getAbsolutePath())) { if(DEBUG) Log.v(TAG, "Delete unused folder:" + f.getAbsolutePath()); Util.deleteRecursive(f); } } } //remove all old messages private static void cleanPendingMessages() { sUpdateHandler.removeMessages(MSG_GETUPDATEINFO); for(int i = 0; i < sUpdateItems.size(); i++) { sUpdateHandler.removeMessages(MSG_UPDATE_CATALOG + i); } } static void onServerUpdateInfo(UpdateInfo info) { synchronized (sListeners) { for (Listener l : sListeners) l.onServerUpdateInfo(info); } } static void onUpdateNeed(UpdateItem item, boolean needUpdate){ synchronized (sListeners) { for (Listener l : sListeners) l.onUpdateNeed(item, needUpdate); } } static void onDownloadProgress(UpdateItem item, int newPrecent){ synchronized (sListeners) { for (Listener l : sListeners) l.onDownloadProgress(item, newPrecent); } } static void onUpdateAvaliable(UpdateItem item, UpdateInfo updateInfo){ synchronized (sListeners) { for (Listener l : sListeners) l.onUpdateAvaliable(item, updateInfo); } } private static File getUnZipFolder(String name, long version){ if(version == -1) { //for temp purpose to download return new File(sUpdateTempDir, name + "_" + "temp"); } else { return new File(sUpdateTempDir, name + "_" + version); } } private static final class UpdateHandler extends Handler { private long mAutoUpdateTimer; private long FAIL_RETRY_TIMER = 5 * 60 * 1000; /* default 5 minutes if failed */ public UpdateHandler(Looper looper) { super(looper); } private boolean getUpdateInfo(Message msg) throws Exception { String jsonText = ""; mAutoUpdateTimer = FAIL_RETRY_TIMER; cleanPendingMessages(); try { if(DEBUG) Log.v(TAG, "start to get server update info..."); String updateURL = WonderDB.DB_UPDATE2_PHP; if( Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { updateURL += "os=41"; } else { updateURL += "os=42"; } jsonText = Network.getWonderTVText(updateURL); if (jsonText == null || jsonText.length() == 0){ onServerUpdateInfo(null); for(UpdateItem item : sUpdateItems) { if(item.name.equals(NAME_APK)) continue; if( !getItemFolder(item.name).exists()){ mAutoUpdateTimer = 10 * 1000; //check later after 10s if any item not exist break; } } Log.w(TAG, "server update info return empty, check after " + (mAutoUpdateTimer/1000)); return false; } jsonText = String.format("[%1$s]", jsonText); final JSONArray array = new JSONArray(jsonText); if (array.length() == 0) { Log.w(TAG, "json decode from server php wrong:" + jsonText); onServerUpdateInfo(null); return false; } final JSONObject obj = array.getJSONObject(0); sUpdateInfo.updateFromJson(obj); if(DEBUG) Log.v(TAG, "server update info:" + sUpdateInfo); onServerUpdateInfo(sUpdateInfo); //check the updateToken if( !Network.getWonderTVId().matches(sUpdateInfo.updateToken)){ Log.v(TAG, "No token to update:" + Network.getWonderTVId() + "," + sUpdateInfo.updateToken); mAutoUpdateTimer = sUpdateInfo.busywaittime; return true; } for(int i = 0; i < sUpdateItems.size(); i++) { UpdateItem item = sUpdateItems.get(i); //start modify by tangzefang patch for upgrade if(NAME_APK.equals(item.name)) item.localVersion = LOCAL_VERSION; else item.localVersion = Util.getPropertyKeyLong(item.name, 0); //end modify by tangzefang item.serverVersion = Long.parseLong(obj.getString(item.name)); Log.d(TAG, item.name + " local version " + item.localVersion + ",server version " + item.serverVersion); if ( item.serverVersion > item.localVersion || /* localVersion uses old time stamp, force a update then */ item.localVersion > 1000000 ) { //check if we really need to download it or not Log.d(TAG, item.name + " need to update"); File f = getUnZipFolder(item.name, item.serverVersion); if( f.exists()) { //item has been downloaded last time, just update is OK. Log.d(TAG, item.name + " ver " + item.serverVersion + " exists, update now"); Util.setPropertyKeyValue(item.name, String.valueOf(item.serverVersion)); item.unzipFolder = f; onUpdateAvaliable(item, sUpdateInfo); } else { File cur = getUnZipFolder(item.name, item.localVersion); cleanUnusedFolder(item.name, cur); //start to download this item now. Message itemMsg = sUpdateHandler.obtainMessage(MSG_UPDATE_CATALOG + i, item); onUpdateNeed(item, true); sUpdateHandler.sendMessage(itemMsg); Log.d(TAG, item.name + " start to download ver " + item.serverVersion); } } else { Log.d(TAG, item.name + " no need to update."); onUpdateNeed(item, false); } } //good. talk to server later. mAutoUpdateTimer = sUpdateInfo.busywaittime; return true; } catch(Exception e){ Log.d(TAG, "Exception when getupdateinfo", e); onServerUpdateInfo(null); for(UpdateItem item : sUpdateItems) { if(item.name.equals(NAME_APK)) continue; if( !getItemFolder(item.name).exists()){ mAutoUpdateTimer = 10 * 1000; //check later after 10s if any item not exist Log.w(TAG, item.name + " not exist, check after " + (mAutoUpdateTimer/1000) + "s"); break; } } } return false; } boolean executeZipUpdate(UpdateItem item) throws Exception { HttpURLConnection c = null; FileOutputStream fos = null; try { String urlPath; if( item.name.equals(NAME_APK)){ if(Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) urlPath = SERV_UPDATE_FOLDER + item.name + "41_" + item.serverVersion + CAB_EXT; else urlPath = SERV_UPDATE_FOLDER + item.name + "42_" + item.serverVersion + CAB_EXT; } else { urlPath = SERV_UPDATE_FOLDER + item.name + "_" + item.serverVersion + CAB_EXT; } onDownloadProgress(item, 0); if( sUpdateInfo.serverRedirect.length() > 1) { Log.v(TAG, item.name + " download update from redirect server:" + sUpdateInfo.serverRedirect); c = Network.createConnection(sUpdateInfo.serverRedirect, urlPath); } else { Log.v(TAG, item.name + " download update from direct server"); c = Network.getWonderTVConnection(urlPath); } if (c == null) { Log.w(TAG, item.name + " can't create connection " + sUpdateInfo.serverRedirect); onDownloadProgress(item, -1); return false; } final File zipFile = new File(sUpdateTempDir, item.name + CAB_EXT); //modify by tangzefang File tempUnZipFolder = getUnZipFolder(item.name, -1); Log.d(TAG, item.name + " reading update file from " + urlPath + "..."); InputStream inputStream = new BufferedInputStream(c.getInputStream()); int readLen = 0; final byte[] buf = new byte[8192]; fos = new FileOutputStream(zipFile); int total = c.getContentLength(); int read = 0; int lastPrecent = 0; while ((readLen = inputStream.read(buf)) != -1) { fos.write(buf, 0, readLen); read += readLen; if (total != -1) { int newPrecent = read * 100 / total; if (newPrecent != lastPrecent) { if( newPrecent != 100) { onDownloadProgress(item, newPrecent); lastPrecent = newPrecent; } } } } fos.flush(); //start modify by tangzefang if(item.name.equals(NAME_SCREENSHORTS)){ tempUnZipFolder = new File("/mnt/local/.screenshots/"); tempUnZipFolder.setExecutable(true,false); tempUnZipFolder.setReadable(true,false); tempUnZipFolder.setWritable(true,false); } //end modify by tangzefang Util.deleteRecursive(tempUnZipFolder); if (Util.unZipFile(zipFile.getAbsolutePath(), tempUnZipFolder.getAbsolutePath())) { zipFile.delete(); if(item.name.equals(NAME_APK)) { //special process for the apk permission tempUnZipFolder.setReadable(true, false); tempUnZipFolder.setExecutable(true, false); File newPath = new File(tempUnZipFolder, UpdateConfig.UPDATE_SAVENAME); File[] files = tempUnZipFolder.listFiles(); for(File xf : files) { if( xf.getAbsolutePath().endsWith(".apk")) { xf.renameTo(newPath); break; } } newPath.setReadable(true, false); } //Until now the download and unzip are done. //rename ~hot to hot123, this is the final folder to indicate item has been downloaded. File unZipFolder = getUnZipFolder(item.name, item.serverVersion); Util.deleteRecursive(unZipFolder); //start modify by tangzefang if(!item.name.equals(NAME_SCREENSHORTS)) tempUnZipFolder.renameTo(unZipFolder); else unZipFolder = tempUnZipFolder; //end modify by tangzefang Util.setPropertyKeyValue(item.name, String.valueOf(item.serverVersion)); //start modify by tangzefang if(item.name.equals(NAME_SCREENSHORTS)) item.localVersion = Util.getPropertyKeyLong(item.name, 0); //end modify by tangzefang //right now even the program crash we will still use the new version now. Log.v(TAG, item.name + " unzip file to " + unZipFolder.getAbsolutePath() + " successfully"); item.unzipFolder = unZipFolder; onDownloadProgress(item, 100); onUpdateAvaliable(item, sUpdateInfo); return true; } else { Log.w(TAG, item.name + " unZipFile error at:" + zipFile); zipFile.delete(); onDownloadProgress(item, -1); } } catch (Exception e) { onDownloadProgress(item, -1); throw e; } finally { if (fos != null) try { fos.close(); } catch (IOException e) { } if (c != null) c.disconnect(); } return false; } @Override public void handleMessage(Message msg) { try { switch(msg.what) { case MSG_GETUPDATEINFO: getUpdateInfo(msg); //restart the new update check always. Message getUpdateInfo = sUpdateHandler.obtainMessage(MSG_GETUPDATEINFO); sUpdateHandler.sendMessageDelayed(getUpdateInfo, mAutoUpdateTimer); if(DEBUG) Log.v(TAG, "scheduler next getupdateinfo after " + (mAutoUpdateTimer/1000) + "s"); break; default: UpdateItem item = (UpdateItem)msg.obj; if(!executeZipUpdate(item)){ //download xxx.zip failed, try again later. Message itemUpdate = sUpdateHandler.obtainMessage(MSG_UPDATE_CATALOG + item.index); sUpdateHandler.sendMessageDelayed(itemUpdate, FAIL_RETRY_TIMER); if(DEBUG) Log.v(TAG, item.name + " scheduler next update as error"); } break; } } catch(Exception e) { Log.w(TAG, "UpdateHandler exception", e); } } } }