Skip to content

Instantly share code, notes, and snippets.

@jjhesk
Last active February 16, 2016 04:25
Show Gist options
  • Save jjhesk/5c1f0e8a77008a196049 to your computer and use it in GitHub Desktop.
Save jjhesk/5c1f0e8a77008a196049 to your computer and use it in GitHub Desktop.
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Observable;
import java.util.Scanner;
import java.util.Set;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
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.Build;
import android.os.Handler;
import android.provider.Settings.Secure;
import android.support.v4.app.NotificationCompat;
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:
//
// 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
//
public AutoUpdateApk(Context ctx) {
setupVariables(ctx);
}
// 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 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 ) {
UPDATE_INTERVAL = 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 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";
// private final static String API_URL = "http://auto-update-apk.appspot.com/check";
private final static String API_URL = "http://www.auto-update-apk.com/check";
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 static long UPDATE_INTERVAL = 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 = 0xBEEF;
private static long WAKEUP_INTERVAL = 15 * MINUTES;
private class ScheduleEntry {
public int start;
public int end;
public ScheduleEntry(int start, int end) {
this.start = start;
this.end = end;
}
}
private static ArrayList<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, UPDATE_INTERVAL);
} 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");
}
if( appinfo.labelRes != 0 ) {
appName = context.getString(appinfo.labelRes);
} 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();
}
}
}
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;
}
// required in order to prevent issues in earlier Android version.
private static void disableConnectionReuseIfNecessary() {
// see HttpURLConnection API doc
if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
}
private static String getResponseText(InputStream inStream) {
// very nice trick from http://weblogs.java.net/blog/pat/archive/2004/10/stupid_scanner_1.html
return new Scanner(inStream).useDelimiter("\\A").next();
}
private class checkUpdateTask extends AsyncTask<Void,Void,String[]> {
protected String[] doInBackground(Void... v) {
long start = System.currentTimeMillis();
disableConnectionReuseIfNecessary();
HttpURLConnection urlConnection = null;
try {
URL url = new URL(API_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
Uri.Builder builder = new Uri.Builder()
.appendQueryParameter("pkgname", packageName)
.appendQueryParameter("version", "" + versionCode)
.appendQueryParameter("md5", preferences.getString( MD5_KEY, "0"))
.appendQueryParameter("id", String.format( "%08x", device_id));
final String postParameters = builder.build().getEncodedQuery();
// set the timeout in milliseconds until a connection is established
// the default value is zero, that means the timeout is not used
conn.setConnectTimeout(3000);
// set the default socket timeout (SO_TIMEOUT) in milliseconds
// which is the timeout for waiting for data
conn.setReadTimeout(3000);
conn.setRequestMethod("POST");
conn.setFixedLengthStreamingMode(postParameters.getBytes().length);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoInput(true);
conn.setDoOutput(true);
//send the POST out
PrintWriter pw = new PrintWriter(conn.getOutputStream());
pw.print(postParameters);
pw.close();
conn.connect();
InputStream in = new BufferedInputStream(conn.getInputStream());
final String result[] = getResponseText(in).split("\n");
in.close();
conn.disconnect();
if( result.length > 1 && result[0].equalsIgnoreCase("have update") ) {
url = new URL(result[1]);
conn = (HttpURLConnection) url.openConnection();
// set the timeout in milliseconds until a connection is established
// the default value is zero, that means the timeout is not used
conn.setConnectTimeout(3000);
// set the default socket timeout (SO_TIMEOUT) in milliseconds
// which is the timeout for waiting for data
conn.setReadTimeout(3000);
conn.setDoInput(true);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK &&
conn.getContentType().equalsIgnoreCase(ANDROID_PACKAGE)) {
in = new BufferedInputStream(conn.getInputStream());
String fname = result[1].substring(result[1].lastIndexOf('/')+1);
FileOutputStream out = context.openFileOutput( fname, Context.MODE_WORLD_READABLE);
byte[] buffer = new byte[4096];
int n;
while ((n = in.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
in.close();
out.close();
result[1] = fname;
} else {
return null; // bad HTTP response or invalid content type
}
conn.disconnect();
setChanged();
notifyObservers(AUTOUPDATE_GOT_UPDATE);
} else {
setChanged();
notifyObservers(AUTOUPDATE_NO_UPDATE);
Log_v(TAG, "no update available");
}
return result;
} catch (MalformedURLException e) {
// handle invalid URL
} catch (SocketTimeoutException e) {
// handle timeout
} catch (IOException e) {
// handle I/0 errors
} finally {
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 + UPDATE_INTERVAL) < 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);
String update_file = preferences.getString(UPDATE_FILE, "");
if( update_file.length() > 0 ) {
setChanged();
notifyObservers(AUTOUPDATE_HAVE_UPDATE);
// raise the notification
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);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setSmallIcon(appIcon);
builder.setTicker(appName + " update");
builder.setContentTitle(contentTitle);
builder.setContentText(contentText);
builder.setContentIntent(contentIntent);
builder.setWhen(System.currentTimeMillis());
builder.setAutoCancel(true);
builder.setOngoing(true);
nm.notify(NOTIFICATION_ID, builder.build());
} else {
//nm.cancel( NOTIFICATION_ID ); // tried this, but it just doesn't do the trick =(
nm.cancelAll();
}
}
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) {
// e.printStackTrace();
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 = packageInfo.versionCode;
} catch (NameNotFoundException e) {
// e.printStackTrace();
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(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);
}
}
}
import java.util.Observable;
import java.util.Observer;
import android.app.Activity;
import android.os.Bundle;
public class AutoUpdateApkActivity extends Activity implements Observer {
// declare updater class member here (or in the Application)
@SuppressWarnings("unused")
private AutoUpdateApk aua;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
aua = new AutoUpdateApk(getApplicationContext()); // <-- don't forget to instantiate
aua.addObserver(this); // see the remark below, next to update() method
}
// you only need to use this method and specify "implements Observer" and use "addObserver()"
// in case you want to closely monitor what's the AutoUpdateApk is doing, otherwise just ignore
// "implements Observer" and "addObserver()" and skip implementing this method.
//
// There are three kinds of update messages sent from AutoUpdateApk (more may be added later):
// AUTOUPDATE_CHECKING, AUTOUPDATE_NO_UPDATE and AUTOUPDATE_GOT_UPDATE, which denote the start
// of update checking process, and two possible outcomes.
//
@Override
public void update(Observable observable, Object data) {
if( ((String)data).equalsIgnoreCase(AutoUpdateApk.AUTOUPDATE_GOT_UPDATE) ) {
android.util.Log.i("AutoUpdateApkActivity", "Have just received update!");
}
if( ((String)data).equalsIgnoreCase(AutoUpdateApk.AUTOUPDATE_HAVE_UPDATE) ) {
android.util.Log.i("AutoUpdateApkActivity", "There's an update available!");
}
}
}
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
// ***************************
// *** WARNING *** WARNING ***
// ***************************
//
// this class is very experimental and intended for people who well understand
// what is going on and what kind of unexpected results or strange behavior may
// occur in some cases. please use AutoUpdateApk instead of this class if in doubt
public class SilentAutoUpdate extends AutoUpdateApk {
// this class is supposed to be instantiated in any of your activities or,
// better yet, in Application subclass. Something along the lines of:
//
// private SilentAutoUpdate sau; <-- you need to add this line of code
//
// public void onCreate(Bundle savedInstanceState) {
// super.onCreate(savedInstanceState);
// setContentView(R.layout.main);
//
// sau = new SilentAutoUpdate(getApplicationContext()); <-- and add this line too
//
SilentAutoUpdate(Context ctx) {
super(ctx);
}
//
// ---------- everything below this line is private and does not belong to the public API ----------
//
protected void raise_notification() {
String update_file = preferences.getString(UPDATE_FILE, "");
boolean silent_update_failed = preferences.getBoolean(SILENT_FAILED, false);
if( update_file.length() > 0 && !silent_update_failed ) {
final String libs = "LD_LIBRARY_PATH=/vendor/lib:/system/lib ";
final String[] commands = {
libs + "pm install -r " + context.getFilesDir().getAbsolutePath() + "/" + update_file,
libs + "am start -n " + context.getPackageName() + "/" + get_main_activity()
};
execute_as_root(commands); // not supposed to return if successful
preferences.edit().putBoolean(SILENT_FAILED, true).commit(); // avoid silent update loop
}
super.raise_notification();
}
// this is not guaranteed to work 100%, should be rewritten.
//
// if your application fails to restart after silent upgrade,
// you may try to replace this function with a simple statement:
//
// return ".YourMainActivity";
//
private String get_main_activity() {
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
try {
final int flags = PackageManager.GET_ACTIVITIES;
PackageInfo packageInfo = pm.getPackageInfo(packageName, flags);
for( ActivityInfo ai : packageInfo.activities ) {
if( ai.exported ) {
return ai.name;
}
}
} catch (NameNotFoundException e) {
e.printStackTrace();
}
Log_e(TAG, "get_main_activity() failed");
return "";
}
private void execute_as_root( String[] commands ) {
try {
// Do the magic
Process p = Runtime.getRuntime().exec( "su" );
InputStream es = p.getErrorStream();
DataOutputStream os = new DataOutputStream(p.getOutputStream());
for( String command : commands ) {
//Log.i(TAG,command);
os.writeBytes(command + "\n");
}
os.writeBytes("exit\n");
os.flush();
os.close();
int read;
byte[] buffer = new byte[4096];
String output = new String();
while ((read = es.read(buffer)) > 0) {
output += new String(buffer, 0, read);
}
p.waitFor();
Log_e(TAG, output.trim() + " (" + p.exitValue() + ")");
} catch (IOException e) {
Log_e(TAG, e.getMessage());
} catch (InterruptedException e) {
Log_e(TAG, e.getMessage());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment