Java tutorial
// // Copyright (c) 2012 lenik terenin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.autoupdateapk; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Observable; import java.util.Set; import java.util.zip.CRC32; import java.util.zip.Checksum; import org.apache.http.HttpEntity; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.util.EntityUtils; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.provider.Settings.Secure; import android.util.Log; public class AutoUpdateApk extends Observable { /** * This class is supposed to be instantiated in any of your activities or, * better yet, in Application subclass. Something along the lines of: * * <pre> * private AutoUpdateApk aua; <-- you need to add this line of code * * public void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.main); * * aua = new AutoUpdateApk(getApplicationContext()); <-- and add this line too * </pre> * * @param ctx * parent activity context * @param apiURL * server API path may be relative to server (eg. /myapi/updater) * or absolute, depending on server implementation : relative * path and server is mandatory if server's implementation * provides relative paths. (http://www.auto-update-apk.com/ * provides an existing server at {@link #PUBLIC_API_URL} ) * @param server * server name and port (eg. myserver.domain.com:8123 ). Should * be null when using absolutes apiPath. */ public AutoUpdateApk(Context ctx, String apiPath, String server) { setupVariables(ctx); this.server = server; this.apiPath = apiPath; } public AutoUpdateApk(Context ctx, String apiURL) { setupVariables(ctx); this.server = null; this.apiPath = apiURL; } // set icon for notification popup (default = application icon) // public static void setIcon(int icon) { appIcon = icon; } // set name to display in notification popup (default = application label) // public static void setName(String name) { appName = name; } // set Notification flags (default = Notification.FLAG_AUTO_CANCEL | // Notification.FLAG_NO_CLEAR) // public static void setNotificationFlags(int flags) { NOTIFICATION_FLAGS = flags; } /** * set update interval (in milliseconds). * * there are nice constants in this file: MINUTES, HOURS, DAYS you may use * them to specify update interval like: 5 * DAYS * * please, don't specify update interval below 1 hour, this might be * considered annoying behaviour and result in service suspension */ public void setUpdateInterval(long interval) { // if( interval > 60 * MINUTES ) { updateInterval = interval; // } else { // Log_e(TAG, "update interval is too short (less than 1 hour)"); // } } // software updates will use WiFi/Ethernet only (default mode) // public static void disableMobileUpdates() { mobile_updates = false; } // software updates will use any internet connection, including mobile // might be a good idea to have 'unlimited' plan on your 3.75G connection // public static void enableMobileUpdates() { mobile_updates = true; } // call this if you want to perform update on demand // (checking for updates more often than once an hour is not recommended // and polling server every few minutes might be a reason for suspension) // public void checkUpdatesManually() { checkUpdates(true); // force update check } public static final String AUTOUPDATE_CHECKING = "autoupdate_checking"; public static final String AUTOUPDATE_NO_UPDATE = "autoupdate_no_update"; public static final String AUTOUPDATE_GOT_UPDATE = "autoupdate_got_update"; public static final String AUTOUPDATE_HAVE_UPDATE = "autoupdate_have_update"; public static final String PUBLIC_API_URL = "http://www.auto-update-apk.com/check"; public void clearSchedule() { schedule.clear(); } public void addSchedule(int start, int end) { schedule.add(new ScheduleEntry(start, end)); } // // ---------- everything below this line is private and does not belong to // the public API ---------- // protected final static String TAG = "AutoUpdateApk"; private final static String ANDROID_PACKAGE = "application/vnd.android.package-archive"; protected final String server; protected final String apiPath; protected static Context context = null; protected static SharedPreferences preferences; private final static String LAST_UPDATE_KEY = "last_update"; private static long last_update = 0; private static int appIcon = android.R.drawable.ic_popup_reminder; private static int versionCode = 0; // as low as it gets private static String packageName; private static String appName; private static int device_id; public static final long MINUTES = 60 * 1000; public static final long HOURS = 60 * MINUTES; public static final long DAYS = 24 * HOURS; // 3-4 hours in dev.mode, 1-2 days for stable releases private long updateInterval = 3 * HOURS; // how often to check private static boolean mobile_updates = false; // download updates over wifi // only private final static Handler updateHandler = new Handler(); protected final static String UPDATE_FILE = "update_file"; protected final static String SILENT_FAILED = "silent_failed"; private final static String MD5_TIME = "md5_time"; private final static String MD5_KEY = "md5"; private static int NOTIFICATION_ID = 0xDEADBEEF; private static int NOTIFICATION_FLAGS = Notification.FLAG_AUTO_CANCEL | Notification.FLAG_NO_CLEAR; private static long WAKEUP_INTERVAL = 500; private class ScheduleEntry { public int start; public int end; public ScheduleEntry(int start, int end) { this.start = start; this.end = end; } } private static List<ScheduleEntry> schedule = new ArrayList<ScheduleEntry>(); private Runnable periodicUpdate = new Runnable() { @Override public void run() { checkUpdates(false); updateHandler.removeCallbacks(periodicUpdate); // remove whatever // others may have // posted updateHandler.postDelayed(this, WAKEUP_INTERVAL); } }; private BroadcastReceiver connectivity_receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { NetworkInfo currentNetworkInfo = (NetworkInfo) intent .getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); // do application-specific task(s) based on the current network // state, such // as enabling queuing of HTTP requests when currentNetworkInfo is // connected etc. boolean not_mobile = currentNetworkInfo.getTypeName().equalsIgnoreCase("MOBILE") ? false : true; if (currentNetworkInfo.isConnected() && (mobile_updates || not_mobile)) { checkUpdates(false); updateHandler.postDelayed(periodicUpdate, updateInterval); } else { updateHandler.removeCallbacks(periodicUpdate); // no network // anyway } } }; private void setupVariables(Context ctx) { context = ctx; packageName = context.getPackageName(); preferences = context.getSharedPreferences(packageName + "_" + TAG, Context.MODE_PRIVATE); device_id = crc32(Secure.getString(context.getContentResolver(), Secure.ANDROID_ID)); last_update = preferences.getLong("last_update", 0); NOTIFICATION_ID += crc32(packageName); // schedule.add(new ScheduleEntry(0,24)); ApplicationInfo appinfo = context.getApplicationInfo(); if (appinfo.icon != 0) { appIcon = appinfo.icon; } else { Log_w(TAG, "unable to find application icon"); } //appName = context.getString(appinfo.labelRes); NON! appName = (String) context.getPackageManager().getApplicationLabel(appinfo); if (appName != null) { Log_w(TAG, "application name = " + appName); } else { Log_w(TAG, "unable to find application label"); } if (new File(appinfo.sourceDir).lastModified() > preferences.getLong(MD5_TIME, 0)) { preferences.edit().putString(MD5_KEY, MD5Hex(appinfo.sourceDir)).commit(); preferences.edit().putLong(MD5_TIME, System.currentTimeMillis()).commit(); String update_file = preferences.getString(UPDATE_FILE, ""); if (update_file.length() > 0) { if (new File(context.getFilesDir().getAbsolutePath() + "/" + update_file).delete()) { preferences.edit().remove(UPDATE_FILE).remove(SILENT_FAILED).commit(); } } } // hack preferences.edit().remove(SILENT_FAILED).commit(); raise_notification(); if (haveInternetPermissions()) { context.registerReceiver(connectivity_receiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } } private boolean checkSchedule() { if (schedule.size() == 0) return true; // empty schedule always fits int now = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); for (ScheduleEntry e : schedule) { if (now >= e.start && now < e.end) return true; } return false; } private class CheckUpdateTask extends AsyncTask<Void, Void, String[]> { private DefaultHttpClient httpclient = new DefaultHttpClient(); private HttpPost post; private List<String> retrieved = new LinkedList<String>(); public CheckUpdateTask() { if (server != null) { post = new HttpPost(server + apiPath); } else { post = new HttpPost(apiPath); } } protected String[] doInBackground(Void... v) { long start = System.currentTimeMillis(); HttpParams httpParameters = new BasicHttpParams(); // set the timeout in milliseconds until a connection is established // the default value is zero, that means the timeout is not used int timeoutConnection = 3000; HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); // set the default socket timeout (SO_TIMEOUT) in milliseconds // which is the timeout for waiting for data int timeoutSocket = 5000; HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); httpclient.setParams(httpParameters); try { StringEntity params = new StringEntity("pkgname=" + appName // /*packageName*/ NON! + "&version=" + versionCode + "&md5=" + preferences.getString(MD5_KEY, "0") + "&id=" + String.format("%08x", device_id)); post.setHeader("Content-Type", "application/x-www-form-urlencoded"); post.setEntity(params); String response = EntityUtils.toString(httpclient.execute(post).getEntity(), "UTF-8"); Log_v(TAG, "got a reply from update server"); String[] result = response.split("\n"); if (result.length > 1 && result[0].equalsIgnoreCase("have update")) { if (!retrieved.contains(result[1])) { synchronized (retrieved) { if (!retrieved.contains(result[1])) { retrieved.add(result[1]); HttpGet get = new HttpGet((server != null) ? server + result[1] : result[1]); HttpEntity entity = httpclient.execute(get).getEntity(); Log_v(TAG, "got a package from update server"); if (entity.getContentType().getValue().equalsIgnoreCase(ANDROID_PACKAGE)) { String fname = result[1].substring(result[1].lastIndexOf('/') + 1) + ".apk"; Log_v(TAG, fname); FileOutputStream fos = context.openFileOutput(fname, Context.MODE_WORLD_READABLE); entity.writeTo(fos); fos.close(); result[1] = fname; } if (result.length > 2 && result[2] != null) { try { versionCode = Integer.parseInt(result[2]); } catch (NumberFormatException nfe) { Log_e(TAG, "Invalide version code", nfe); } } setChanged(); notifyObservers(AUTOUPDATE_GOT_UPDATE); } } } } else { setChanged(); notifyObservers(AUTOUPDATE_NO_UPDATE); Log_v(TAG, "no update available"); } return result; } catch (ParseException e) { Log_e(TAG, e.getMessage()); } catch (ClientProtocolException e) { Log_e(TAG, e.getMessage()); } catch (IOException e) { Log_e(TAG, e.getMessage()); } finally { httpclient.getConnectionManager().shutdown(); long elapsed = System.currentTimeMillis() - start; Log_v(TAG, "update check finished in " + elapsed + "ms"); } return null; } protected void onPreExecute() { // show progress bar or something Log_v(TAG, "checking if there's update on the server"); } protected void onPostExecute(String[] result) { // kill progress bar here if (result != null) { if (result[0].equalsIgnoreCase("have update")) { preferences.edit().putString(UPDATE_FILE, result[1]).commit(); String update_file_path = context.getFilesDir().getAbsolutePath() + "/" + result[1]; preferences.edit().putString(MD5_KEY, MD5Hex(update_file_path)).commit(); preferences.edit().putLong(MD5_TIME, System.currentTimeMillis()).commit(); } raise_notification(); } else { Log_v(TAG, "no reply from update server"); } } } private void checkUpdates(boolean forced) { long now = System.currentTimeMillis(); if (forced || (last_update + updateInterval) < now && checkSchedule()) { new CheckUpdateTask().execute(); last_update = System.currentTimeMillis(); preferences.edit().putLong(LAST_UPDATE_KEY, last_update).commit(); this.setChanged(); this.notifyObservers(AUTOUPDATE_CHECKING); } } protected void raise_notification() { String ns = Context.NOTIFICATION_SERVICE; NotificationManager nm = (NotificationManager) context.getSystemService(ns); // nm.cancel( NOTIFICATION_ID ); // tried this, but it just doesn't do // the trick =( nm.cancelAll(); String update_file = preferences.getString(UPDATE_FILE, ""); if (update_file.length() > 0) { setChanged(); notifyObservers(AUTOUPDATE_HAVE_UPDATE); // raise notification Notification notification = new Notification(appIcon, appName + " update", System.currentTimeMillis()); notification.flags |= NOTIFICATION_FLAGS; CharSequence contentTitle = appName + " update available"; CharSequence contentText = "Select to install"; Intent notificationIntent = new Intent(Intent.ACTION_VIEW); notificationIntent.setDataAndType( Uri.parse("file://" + context.getFilesDir().getAbsolutePath() + "/" + update_file), ANDROID_PACKAGE); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); nm.notify(NOTIFICATION_ID, notification); } else { nm.cancel(NOTIFICATION_ID); } } private String MD5Hex(String filename) { final int BUFFER_SIZE = 8192; byte[] buf = new byte[BUFFER_SIZE]; int length; try { FileInputStream fis = new FileInputStream(filename); BufferedInputStream bis = new BufferedInputStream(fis); MessageDigest md = java.security.MessageDigest.getInstance("MD5"); while ((length = bis.read(buf)) != -1) { md.update(buf, 0, length); } bis.close(); byte[] array = md.digest(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < array.length; ++i) { sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3)); } Log_v(TAG, "md5sum: " + sb.toString()); return sb.toString(); } catch (Exception e) { Log_e(TAG, e.getMessage()); } return "md5bad"; } private boolean haveInternetPermissions() { Set<String> required_perms = new HashSet<String>(); required_perms.add("android.permission.INTERNET"); required_perms.add("android.permission.ACCESS_WIFI_STATE"); required_perms.add("android.permission.ACCESS_NETWORK_STATE"); PackageManager pm = context.getPackageManager(); String packageName = context.getPackageName(); int flags = PackageManager.GET_PERMISSIONS; PackageInfo packageInfo = null; try { packageInfo = pm.getPackageInfo(packageName, flags); versionCode = Integer.parseInt(packageInfo.versionName); } catch (NameNotFoundException e) { Log_e(TAG, e.getMessage()); } if (packageInfo.requestedPermissions != null) { for (String p : packageInfo.requestedPermissions) { // Log_v(TAG, "permission: " + p.toString()); required_perms.remove(p); } if (required_perms.size() == 0) { return true; // permissions are in order } // something is missing for (String p : required_perms) { Log_e(TAG, "required permission missing: " + p); } } Log_e(TAG, "INTERNET/WIFI access required, but no permissions are found in Manifest.xml"); return false; } private static int crc32(String str) { byte bytes[] = str.getBytes(); Checksum checksum = new CRC32(); checksum.update(bytes, 0, bytes.length); return (int) checksum.getValue(); } // logging facilities to enable easy overriding. thanks, Dan! // protected void Log_v(String tag, String message) { Log_v(tag, message, null); } protected void Log_v(String tag, String message, Throwable e) { log("v", tag, message, e); } protected void Log_d(String tag, String message) { Log_d(tag, message, null); } protected void Log_d(String tag, String message, Throwable e) { log("d", tag, message, e); } protected void Log_i(String tag, String message) { Log_d(tag, message, null); } protected void Log_i(String tag, String message, Throwable e) { log("i", tag, message, e); } protected void Log_w(String tag, String message) { Log_w(tag, message, null); } protected void Log_w(String tag, String message, Throwable e) { log("w", tag, message, e); } protected void Log_e(String tag, String message) { Log_e(tag, message, null); } protected void Log_e(String tag, String message, Throwable e) { log("e", tag, message, e); } protected void log(String level, String tag, String message, Throwable e) { if (message == null) { return; } if (level.equalsIgnoreCase("v")) { if (e == null) android.util.Log.v(tag, message); else android.util.Log.v(tag, message, e); } else if (level.equalsIgnoreCase("d")) { if (e == null) android.util.Log.d(tag, message); else android.util.Log.d(tag, message, e); } else if (level.equalsIgnoreCase("i")) { if (e == null) android.util.Log.i(tag, message); else android.util.Log.i(tag, message, e); } else if (level.equalsIgnoreCase("w")) { if (e == null) android.util.Log.w(tag, message); else android.util.Log.w(tag, message, e); } else { if (e == null) android.util.Log.e(tag, message); else android.util.Log.e(tag, message, e); } } }