Java tutorial
/** * Copyright (C) 2010 Regis Montoya (aka r3gis - www.r3gis.fr) * This file is part of CSipSimple. * * CSipSimple 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. * * CSipSimple 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 CSipSimple. If not, see <http://www.gnu.org/licenses/>. */ /** * Info : this code is deeply inspired from CMUpdater source code */ package com.csipsimple.service; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.lang.reflect.Field; import java.net.URI; import java.util.Iterator; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.preference.PreferenceManager; import com.csipsimple.utils.Log; import android.widget.Toast; import com.keyyomobile.android.voip.R; import com.csipsimple.models.RemoteLibInfo; import com.csipsimple.utils.MD5; public class DownloadLibService extends Service { public static final String CURRENT_STACK_URI = "current_stack_uri"; public static final String CURRENT_STACK_VERSION = "current_stack_version"; public static final String CURRENT_STACK_ID = "current_stack_id"; private static final String THIS_FILE = "DownloadLibService"; private static final int BUFFER = 2048; private WifiLock wifiLock; private ConnectivityManager connectivityManager; private ConnectionChangeReceiver connectionChangeReceiver; private boolean connected; private final RemoteCallbackList<IDownloadLibServiceCallback> callbacks = new RemoteCallbackList<IDownloadLibServiceCallback>(); private RemoteLibInfo currentUpdate; private boolean cancellingDownload; private boolean downloading; private long downloadedSize; private long totalSize; private SharedPreferences prefs; // Implement public interface for the service private final IDownloadLibService.Stub mBinder = new IDownloadLibService.Stub() { @Override public void startDownload(RemoteLibInfo lib) throws RemoteException { downloading = true; boolean success = checkForConnectionAndDownload(lib); downloading = false; if (success) { downloadFinished(); } else { downloadError(); } } @Override public boolean isDownloadRunning() throws RemoteException { return downloading; } @Override public boolean cancelDownload() throws RemoteException { return cancelCurrentDownload(); } @Override public RemoteLibInfo getCurrentRemoteLib() throws RemoteException { return currentUpdate; } @Override public void registerCallback(IDownloadLibServiceCallback cb) throws RemoteException { if (cb != null) { callbacks.register(cb); } } @Override public void unregisterCallback(IDownloadLibServiceCallback cb) throws RemoteException { if (cb != null) { callbacks.unregister(cb); } } @Override public RemoteLibInfo getLibForDevice(String uri, String for_what) throws RemoteException { return getLibUpdate(URI.create(uri), for_what); } @Override public boolean installLib(RemoteLibInfo lib) throws RemoteException { return installRemoteLib(lib); } @Override public void forceStopService() throws RemoteException { stopSelf(); } @Override public void stopDownload() throws RemoteException { cancelCurrentDownload(); } }; @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { Log.d(THIS_FILE, "Download Lib Service started"); prefs = PreferenceManager.getDefaultSharedPreferences(this); // Lock wifi if possible to ensure download will be done wifiLock = ((WifiManager) getSystemService(WIFI_SERVICE)) .createWifiLock("com.csipsimple.service.DownloadLibService"); connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); connectionChangeReceiver = new ConnectionChangeReceiver(); registerReceiver(connectionChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); if (activeNetwork != null) { NetworkInfo.State state = activeNetwork.getState(); connected = (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.SUSPENDED); } else { connected = false; } } @Override public void onDestroy() { callbacks.kill(); unregisterReceiver(connectionChangeReceiver); super.onDestroy(); } private boolean checkForConnectionAndDownload(RemoteLibInfo updateToDownload) { currentUpdate = updateToDownload; boolean success; if (wifiLock != null && !wifiLock.isHeld()) { wifiLock.acquire(); } // wait for a data connection while (!connected) { synchronized (connectivityManager) { try { connectivityManager.wait(); break; } catch (InterruptedException e) { Log.e(THIS_FILE, "Error in connectivityManager.wait", e); } } } try { Log.d(THIS_FILE, "we will downlad : " + updateToDownload.getFileName() + " from " + updateToDownload.getDownloadUri().toString()); success = downloadFile(updateToDownload); } catch (RuntimeException ex) { Log.e(THIS_FILE, "RuntimeEx while downloading file", ex); notificateDownloadError(ex.getMessage()); return false; } catch (IOException ex) { Log.e(THIS_FILE, "Exception while downloading file", ex); notificateDownloadError(ex.getMessage()); return false; } finally { wifiLock.release(); } // Be sure to return false if the User canceled the Download if (cancellingDownload) { return false; } else { return success; } } private void notificateDownloadError(String message) { // TODO Auto-generated method stub } private boolean downloadFile(RemoteLibInfo updateInfo) throws IOException { HttpClient httpClient = new DefaultHttpClient(); HttpClient MD5httpClient = new DefaultHttpClient(); HttpUriRequest req, md5req; HttpResponse response, md5response; URI updateURI; File destinationFile = null; File partialDestinationFile = null; String downloadedMD5 = null; String fileName = updateInfo.getFileName(); File filePath = updateInfo.getFilePath(); // Set the Filename to update.zip.partial partialDestinationFile = new File(filePath, fileName + ".part"); destinationFile = new File(filePath, fileName + ".gz"); if (partialDestinationFile.exists()) { partialDestinationFile.delete(); } if (!cancellingDownload) { updateURI = updateInfo.getDownloadUri(); boolean md5Available = true; try { req = new HttpGet(updateURI); md5req = new HttpGet(updateURI + ".md5sum"); // Add no-cache Header, so the File gets downloaded each time req.addHeader("Cache-Control", "no-cache"); md5req.addHeader("Cache-Control", "no-cache"); //Proceed request md5response = MD5httpClient.execute(md5req); response = httpClient.execute(req); //Get responses codes int serverResponse = response.getStatusLine().getStatusCode(); int md5serverResponse = md5response.getStatusLine().getStatusCode(); if (md5serverResponse != HttpStatus.SC_OK) { md5Available = false; } if (serverResponse == HttpStatus.SC_OK) { if (md5Available) { //Get the md5 sum and save it into a string try { HttpEntity temp = md5response.getEntity(); InputStreamReader isr = new InputStreamReader(temp.getContent()); BufferedReader br = new BufferedReader(isr); try { downloadedMD5 = br.readLine().split(" ")[0]; } catch (NullPointerException e) { md5Available = false; } br.close(); isr.close(); if (temp != null) { temp.consumeContent(); } } catch (IOException e) { //Nothing to do, just imagine there is a problem with download md5Available = false; } } // Download Update ZIP if md5sum went ok HttpEntity entity = response.getEntity(); dumpFile(entity, partialDestinationFile, destinationFile); //Will cancel download if (cancellingDownload) { mToastHandler .sendMessage(mToastHandler.obtainMessage(0, R.string.unable_to_download_file, 0)); return false; } if (entity != null && !cancellingDownload) { entity.consumeContent(); } else { entity = null; } if (md5Available) { if (!MD5.checkMD5(downloadedMD5, destinationFile)) { throw new IOException("md5_verification_failed"); } } return true; } } catch (IOException ex) { mToastHandler.sendMessage(mToastHandler.obtainMessage(0, ex.getMessage())); } if (Thread.currentThread().isInterrupted() || !Thread.currentThread().isAlive()) { mToastHandler.sendMessage(mToastHandler.obtainMessage(0, R.string.unable_to_download_file, 0)); return false; } } mToastHandler.sendMessage(mToastHandler.obtainMessage(0, R.string.unable_to_download_file, 0)); return false; } private void dumpFile(HttpEntity entity, File partialDestinationFile, File destinationFile) throws IOException { if (!cancellingDownload) { totalSize = (int) entity.getContentLength(); if (totalSize <= 0) { totalSize = 1024; } byte[] buff = new byte[64 * 1024]; int read = 0; RandomAccessFile out = new RandomAccessFile(partialDestinationFile, "rw"); out.seek(0); InputStream is = entity.getContent(); TimerTask progressUpdateTimerTask = new TimerTask() { @Override public void run() { onProgressUpdate(); } }; Timer progressUpdateTimer = new Timer(); try { // If File exists, set the Progress to it. Otherwise it will be // initial 0 downloadedSize = 0; progressUpdateTimer.scheduleAtFixedRate(progressUpdateTimerTask, 100, 100); while ((read = is.read(buff)) > 0 && !cancellingDownload) { out.write(buff, 0, read); downloadedSize += read; } out.close(); is.close(); if (!cancellingDownload) { partialDestinationFile.renameTo(destinationFile); } } catch (IOException e) { out.close(); try { destinationFile.delete(); } catch (SecurityException ex) { Log.e(THIS_FILE, "Unable to delete downloaded File. Continue anyway.", ex); } } finally { progressUpdateTimer.cancel(); buff = null; } } } private boolean cancelCurrentDownload() { cancellingDownload = true; String fileName = currentUpdate.getFileName(); File filePath = currentUpdate.getFilePath(); File partialDestinationFile = new File(filePath, fileName + ".part"); File destinationFile = new File(filePath, fileName + ".gz"); if (partialDestinationFile.exists()) { partialDestinationFile.delete(); } if (destinationFile.exists()) { destinationFile.delete(); } downloading = false; stopSelf(); return true; } private void onProgressUpdate() { if (!cancellingDownload) { // Update the DownloadProgress updateDownloadProgress(downloadedSize, totalSize); } } private void updateDownloadProgress(final long downloaded, final long total) { final int n = callbacks.beginBroadcast(); for (int i = 0; i < n; i++) { try { callbacks.getBroadcastItem(i).updateDownloadProgress(downloaded, total); } catch (RemoteException e) { //Should not happen } } callbacks.finishBroadcast(); } private void downloadFinished() { final int n = callbacks.beginBroadcast(); for (int i = 0; i < n; i++) { try { callbacks.getBroadcastItem(i).onDownloadFinished(currentUpdate); } catch (RemoteException e) { //Should not happen } } callbacks.finishBroadcast(); } private void downloadError() { final int n = callbacks.beginBroadcast(); for (int i = 0; i < n; i++) { try { callbacks.getBroadcastItem(i).onDownloadError(); } catch (RemoteException e) { //Should not happen } } callbacks.finishBroadcast(); } private RemoteLibInfo getLibUpdate(URI updateServerUri, String for_what) { HttpClient updateHttpClient = new DefaultHttpClient(); HttpUriRequest updateReq = new HttpGet(updateServerUri); updateReq.addHeader("Cache-Control", "no-cache"); Log.d(THIS_FILE, "Get updates from " + updateServerUri.toString()); try { HttpResponse updateResponse; HttpEntity updateResponseEntity = null; updateResponse = updateHttpClient.execute(updateReq); int updateServerResponse = updateResponse.getStatusLine().getStatusCode(); if (updateServerResponse != HttpStatus.SC_OK) { Log.e(THIS_FILE, "can't get updates from site : " + updateResponse.getStatusLine().getReasonPhrase() + " - "); downloadError(); return null; } updateResponseEntity = updateResponse.getEntity(); BufferedReader upLineReader = new BufferedReader( new InputStreamReader(updateResponseEntity.getContent()), 2 * 1024); StringBuffer upBuf = new StringBuffer(); String upLine; while ((upLine = upLineReader.readLine()) != null) { upBuf.append(upLine); } upLineReader.close(); String content = upBuf.toString(); try { JSONObject mainJSONObject = new JSONObject(content); JSONArray coreJSONArray = mainJSONObject.getJSONArray(for_what); JSONObject stack = getCompatibleStack(coreJSONArray); if (stack != null) { return new RemoteLibInfo(stack); } } catch (JSONException e) { Log.e(THIS_FILE, "Unable to parse " + content, e); downloadError(); } } catch (ClientProtocolException e) { downloadError(); } catch (IOException e) { downloadError(); } return null; } private JSONObject getCompatibleStack(JSONArray availableStacks) { int core_count = availableStacks.length(); for (int i = 0; i < core_count; i++) { JSONObject plateform_stack; try { plateform_stack = availableStacks.getJSONObject(i); Log.d(THIS_FILE, "Check if stack " + plateform_stack.getString("id") + " is compatible"); if (isCompatibleStack(plateform_stack.getJSONObject("filters"))) { Log.d(THIS_FILE, "Found : " + plateform_stack.getString("id")); return plateform_stack; } else { Log.d(THIS_FILE, "NOT VALID : " + plateform_stack.getString("id")); } } catch (Exception e) { Log.w(THIS_FILE, "INVALID FILTER FOR"); e.printStackTrace(); } } return null; } private boolean isCompatibleStack(JSONObject filter) throws SecurityException, NoSuchFieldException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException, JSONException { //For each filter keys, we check if the filter is not invalid Iterator<?> iter = filter.keys(); while (iter.hasNext()) { //Each filter key correspond to a android class which values has to be checked String class_filter = (String) iter.next(); //Get this class Class<?> cls = Class.forName(class_filter); //Then for this class, we have to check if each static field matches defined regexp rule Iterator<?> cls_iter = filter.getJSONObject(class_filter).keys(); while (cls_iter.hasNext()) { String field_name = (String) cls_iter.next(); Field field = cls.getField(field_name); //Get the current value on the system String current_value = field.get(null).toString(); //Get the filter for this value String regexp_filter = filter.getJSONObject(class_filter).getString(field_name); //Check if matches if (!Pattern.matches(regexp_filter, current_value)) { Log.d(THIS_FILE, "Regexp not match : " + current_value + " matches /" + regexp_filter + "/"); return false; } } } return true; } private boolean installRemoteLib(RemoteLibInfo lib) { String fileName = lib.getFileName(); File filePath = lib.getFilePath(); File tmp_gz = new File(filePath, fileName + ".gz"); File dest = new File(filePath, fileName); try { if (dest.exists()) { dest.delete(); } RandomAccessFile out = new RandomAccessFile(dest, "rw"); out.seek(0); GZIPInputStream zis = new GZIPInputStream(new FileInputStream(tmp_gz)); int len; byte[] buf = new byte[BUFFER]; while ((len = zis.read(buf)) > 0) { out.write(buf, 0, len); } zis.close(); out.close(); Log.d(THIS_FILE, "Ungzip is in : " + dest.getAbsolutePath()); tmp_gz.delete(); //Update preferences fields with current stack values Editor editor = prefs.edit(); editor.putString(CURRENT_STACK_ID, lib.getId()); editor.putString(CURRENT_STACK_VERSION, lib.getVersion()); editor.putString(CURRENT_STACK_URI, lib.getDownloadUri().toString()); editor.commit(); return true; } catch (IOException e) { Log.e(THIS_FILE, "We failed to install it ", e); } return false; } private Handler mToastHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.arg1 != 0) { Toast.makeText(DownloadLibService.this, msg.arg1, Toast.LENGTH_LONG).show(); } else { Toast.makeText(DownloadLibService.this, (String) msg.obj, Toast.LENGTH_LONG).show(); } } }; // Is called when Network Connection Changes private class ConnectionChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { NetworkInfo currentConnection = connectivityManager.getActiveNetworkInfo(); if (currentConnection != null) { android.net.NetworkInfo.State state = currentConnection.getState(); connected = (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.SUSPENDED); } else { connected = false; } } } }