In this post, we will see how to update and install your android app from your own server. I will discuss only important coding part. Suppose you are working in small organisation with member of 30 to 50 members. All members using the company’s app. It’s not a good way to upload your app on Google Play Store for targeting only 30 to 50 devices. Also you have to pay money to Google play store to upload your app. You can use Google play Store if you are targeting large users to use your app. But , if you are working with small team then you have to use your own server if you want to save your money !
Simple logic and steps involved to update and install your android app from your own server:
- You must have your own server.
- Create HTTP connection between app client and server.
- Compare version of app client and and available app on server.
- If the version of app on server is higher then show alert popup message on client app to download and install latest app.
We will see here all steps in detail:
MainActivity.java :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package in.nfluence.otaapp; import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { private final SampleAlarmReceiver alarm = new SampleAlarmReceiver(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); alarm.setAlarm(this); } } |
I have created object ‘alarm’ of class SampleAlarmReceiver. It is used to check new version of app available or not on server. We are running this service for every 5 minutes.
SampleAlarmReceiver.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package in.nfluence.otaapp; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.support.v4.content.WakefulBroadcastReceiver; import java.util.Calendar; public class SampleAlarmReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SampleSchedulingService.class); // Start the service, keeping the device awake while it is launching. startWakefulService(context, service); } // BEGIN_INCLUDE(set_alarm) public void setAlarm(Context context) { AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(context, SampleAlarmReceiver.class); PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0); Calendar calendar = Calendar.getInstance(); alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 300 * 1000, alarmIntent); // Enable {@code SampleBootReceiver} to automatically restart the alarm when the // device is rebooted. ComponentName receiver = new ComponentName(context, SampleBootReceiver.class); PackageManager pm = context.getPackageManager(); pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } // END_INCLUDE(set_alarm) } |
SampleBootReciver.java:
This BroadcastReceiver automatically restarts the alarm when the device is rebooted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package in.nfluence.otaapp; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; /** * This receiver is set to be disabled (android:enabled="false") in the * application's manifest file. When the user sets the alarm, the receiver is enabled. * When the user cancels the alarm, the receiver is disabled, so that rebooting the * device will not trigger this receiver. */ // BEGIN_INCLUDE(autostart) public class SampleBootReceiver extends BroadcastReceiver { private final SampleAlarmReceiver alarm = new SampleAlarmReceiver(); @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) { alarm.setAlarm(context); } } } //END_INCLUDE(autostart) |
SampleSchedulingService.java:
onReceive SampleSchedulingService will start. Here it is the actual code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
package in.nfluence.otaapp; import android.app.IntentService; import android.content.Intent; import android.os.StrictMode; import android.util.Log; import org.json.JSONObject; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; public class SampleSchedulingService extends IntentService { public SampleSchedulingService() { super("SchedulingService"); } @Override protected void onHandleIntent(Intent intent) { if (android.os.Build.VERSION.SDK_INT > 9) { StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); } OutputStream os = null; InputStream is = null; HttpURLConnection conn = null; try { //constants URL url = new URL("http://*******.com/*********/version.php"); JSONObject jsonObject = new JSONObject(); int currentversionCode = BuildConfig.VERSION_CODE; jsonObject.put("version", currentversionCode); String message = jsonObject.toString(); StringBuffer response = null; conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /*milliseconds*/); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("POST"); conn.setDoInput(true); conn.setDoOutput(true); conn.setFixedLengthStreamingMode(message.getBytes().length); //make some HTTP header nicety conn.setRequestProperty("Content-Type", "application/json;charset=utf-8"); conn.setRequestProperty("X-Requested-With", "XMLHttpRequest"); //open conn.connect(); //setup send os = new BufferedOutputStream(conn.getOutputStream()); os.write(message.getBytes()); Log.d("SEND TO SERVER:", String.valueOf(message.getBytes())); //clean up os.flush(); //do somehting with response is = conn.getInputStream(); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; response = new StringBuffer(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } int serverversion= Integer.parseInt(response.toString()); Log.d("SERVER Response:", String.valueOf(Integer.parseInt(response.toString()))); os.close(); is.close(); conn.disconnect(); if(serverversion>currentversionCode){ Intent myIntent = new Intent(this, ShowNote.class); myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(myIntent); } } catch (Exception e){ e.printStackTrace(); } // Release the wake lock provided by the BroadcastReceiver. SampleAlarmReceiver.completeWakefulIntent(intent); // END_INCLUDE(service_onhandle) } } |
Versioning is important to maintain your app upgrade strategy. BuildConfig.VERSION_CODE will show the version code of your app. You can see in build.grandle:
1 2 3 4 5 6 7 |
defaultConfig { applicationId "in.nfluence.otaapp" minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName "1.0" } |
On every update you have to increase the value of versionCode in build.grandle. Higher number indicates more recent version of app.
We are sending this version number using HTTP POST method to server to compare latest version availability on server. Here HttpURLConnection is used to perform HTTP data request and response. If the response string value is higher than current versionCode then new activity(ShowNote.java) will open using Intent.
ShowNote.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
package in.nfluence.otaapp; import android.app.Activity; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import java.io.File; /** * Created by ${ajinkya} on ${2016-04-04}. */ public class ShowNote extends Activity { private BroadcastReceiver receiver; private long enqueue; private DownloadManager dm; boolean isDeleted; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("CAcompAdda"); builder.setIcon(R.mipmap.icon); builder.setMessage("Latest Version is Available. Click on OK to update"); builder.getContext().setTheme(R.style.AppTheme_NoActionBar); builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(getApplicationContext(), "App Downloading...Please Wait", Toast.LENGTH_LONG).show(); dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); File file = new File("/mnt/sdcard/Download/user_manual.apk"); if(file.exists()){ isDeleted = file.delete(); deleteAndInstall(); } else{ firstTimeInstall(); } } }); builder.setNegativeButton("Remind Me Later", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ShowNote.this.finish(); } }); builder.show(); } private void firstTimeInstall() { Log.d("May be 1st Update:","OR deleteed from folder" ); downloadAndInstall(); } private void deleteAndInstall() { if(isDeleted){ Log.d("Deleted Existance file:", String.valueOf(isDeleted)); downloadAndInstall(); }else { Log.d("NOT DELETED:", String.valueOf(isDeleted)); Toast.makeText(getApplicationContext(), "Error in Updating...Please try Later", Toast.LENGTH_LONG).show(); } } private void downloadAndInstall() { DownloadManager.Request request = new DownloadManager.Request( Uri.parse("http://******.com/******/user_manual.apk")); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "user_manual.apk"); enqueue = dm.enqueue(request); receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { Toast.makeText(getApplicationContext(), "Download Completed", Toast.LENGTH_LONG).show(); long downloadId = intent.getLongExtra( DownloadManager.EXTRA_DOWNLOAD_ID, 0); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(enqueue); Cursor c = dm.query(query); if (c.moveToFirst()) { int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS); if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) { String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); Log.d("ainfo", uriString); if(downloadId == c.getInt(0)) { Log.d("DOWNLOAD PATH:", c.getString(c.getColumnIndex("local_uri"))); Log.d("isRooted:",String.valueOf(isRooted())); if(isRooted()==false){ //if your device is not rooted Intent intent_install = new Intent(Intent.ACTION_VIEW); intent_install.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/Download/"+"user_manual.apk")), "application/vnd.android.package-archive"); Log.d("phone path",Environment.getExternalStorageDirectory() + "/Download/"+"user_manual.apk"); startActivity(intent_install); Toast.makeText(getApplicationContext(), "App Installing", Toast.LENGTH_LONG).show(); }else{ //if your device is rooted then you can install or update app in background directly Toast.makeText(getApplicationContext(), "App Installing...Please Wait", Toast.LENGTH_LONG).show(); File file = new File("/mnt/sdcard/Download/user_manual.apk"); Log.d("IN INSTALLER:", "/mnt/sdcard/Download/user_manual.apk"); if(file.exists()){ try { String command; Log.d("IN File exists:","/mnt/sdcard/Download/user_manual.apk"); command = "pm install -r " + "/mnt/sdcard/Download/user_manual.apk"; Log.d("COMMAND:",command); Process proc = Runtime.getRuntime().exec(new String[] { "su", "-c", command }); proc.waitFor(); Toast.makeText(getApplicationContext(), "App Installed Successfully", Toast.LENGTH_LONG).show(); } catch (Exception e) { e.printStackTrace(); } } } } } } c.close(); } } }; registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } private static boolean isRooted() { return findBinary("su"); } public static boolean findBinary(String binaryName) { boolean found = false; if (!found) { String[] places = {"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/","/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"}; for (String where : places) { if ( new File( where + binaryName ).exists() ) { found = true; break; } } } return found; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK)) { finish(); } return super.onKeyDown(keyCode, event); } } |
In above code , if latest update available on server then it will show AlertDialog. If user clicks on OK then it will download and installs the app. I will not discuss whole code . Code is itself explanatory.
Also I have mentioned rooting concept here in above code. Rooting means your device will allow to access to change the software code on the device or install other software(specially custom ROM) that the manufacturer wouldn’t normally allow you to. Please don’t try to root your device . It may harm your device. Generally people root their device to upgrade latest android OS(marshmallow ,nougat or other activity on device).
Come on coding part, if your device is rooted then app will download and update automatically in background.
1 |
command = "pm install -r " + "/mnt/sdcard/Download/user_manual.apk"; |
Above command will execute to direct installation of app without taking extra permissions from user.
If your device is not rooted then it will show to user a installation dialog with app permissions like normal app installation.
version.php :
Put your latest updated app on same directory of server.Note that you have to increment versionCode on client app after each update.
Create a text file updateVersion.txt on same server directory. Write here same versionCode that you already mentioned on updated app. I have created this file only send HTTP response to app client in JSON format.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php $json = file_get_contents('php://input'); $obj = json_decode($json); $version=$obj->{'version'}; $a=file_get_contents("updateVersion.txt"); $intversion = (int)$a; echo $intversion ; ?> |
It is simple way to Update and Install Your Android App From Your Own Server Programmatically. Later you can add your own functionality to track total downloads, how many user installed latest version. But you have to create a web interface to show all data on web page. Use any language like PHP, Node.js or Python to create web application to track this all record. If you have any question then don’t forget to comment below.
Edit:
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="in.nfluence.otaapp" > <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name=".SampleAlarmReceiver"/> <activity android:name=".ShowNote" android:exported="true" android:theme="@android:style/Theme.Translucent.NoTitleBar"></activity> <receiver android:name=".SampleBootReceiver" android:enabled="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> <service android:name=".SampleSchedulingService" /> </application> </manifest> |
Does it work with HTTPS instead of HTTP (protocol)?
Thanks in advance.
hello sir which library you using
It gaves error like “There was a problem parsing the package.”
HI its very nice example and thanks for it , i have folowing error
java.lang.SecurityException: No permission to write to /storage/emulated/0/Download/user_manual.apk: Neither user 10159 nor current process has android.permission.WRITE_EXTERNAL_STORAGE.
the reson must be on android sdk 25 that i am using, but i am very new in android world and struggling to implement run time permissions inside your example. meybe can you give me some tips or show me example with run time permissions with your code if you can ,
Tanks
Hello!
Please, can you show what would be in the updateVersion.txt file. Show an example if possible.
Thank you
When you update app then change ‘versionCode’ in build.grandle with higher integer number than previous value. Upload your app on server. Put version.php and updateVersion.txt file on same directory. Now change the versionCode in updateVersion.txt. Just enter integer number that matches with your updated version of app. for example see here http://prntscr.com/lthir0
Thank you so much for the swift response.
I did what you suggested. But, I run version.php on my xampp to see the output… I get an error “Trying to get property of non-object in version.php on line 5”
:You will get ‘Notice’ if you are running code on local XAMPP. In this case just put @ symbol on line 5 : e.g:
@$version=$obj->{‘version’};
If you try code on actual server then it will work perfectly.
Wow! Thanks a lot. It works! 😉
Please Show me the Manifest File. Showing some errors.
Caused by: java.lang.IllegalArgumentException:
Component class com.vr.appupdate.SampleBootReciver does not exist in com.vr.appupdate
at android.os.Parcel.readException(Parcel.java:2009)
at android.os.Parcel.readException(Parcel.java:1951)
at android.content.pm.IPackageManager$Stub$Proxy.setComponentEnabledSetting(IPackageManager.java:4785)
at android.app.ApplicationPackageManager.setComponentEnabledSetting(ApplicationPackageManager.java:2299)
at com.vr.appupdate.SampleAlarmReceiver.setAlarm(SampleAlarmReceiver.java:43)
at com.vr.appupdate.MainActivity.onCreate(MainActivity.java:13)
My Manifest file:
sorry for late reply !
AndroidManifest.xml added at end of the post.
Hi Ajinkya, can you share the source code?
Hello Tuna,
Currently I don’t have separate source code. It’s working code. I tried in simple way how implement logic in this post. Please understand above steps, logic, code and build your ‘own’ code 🙂
Strange thing :/ I’ve added everything to project, all this java files are in same package as rest of app files. But when I try to run on emulator app crashes instantly with exception java.lang.RunetimeException (IllegalArgumentException: Component class (myComponentName).SampleBootReceiver does not exist in (myComponentName).
When I comment out in my MainActivity line ‘alarm.setAlarm(this);’ my app is working fine (as it should because no new code was invoked).
Any idea why I got this ‘does not exist’ error ? File is exist, it’s name is correct..
Hello TomekM,
Try to clear and rebuild code and Use real device not a emulator.
Hey, You must add section receiver to manifest file with name .SampleBootReceiver. (Look at dot at filename first character!)
Don’t forgot to add in your manifest file
can you please show me your manifest? im having a hard time getting it to works. Thanks!
sorry for late reply !
AndroidManifest.xml added at end of the post.
can you please show me your style?
Hi There Sir Thank you so much for you efforts it is great to work through and understand how everything is working, I am having a little issue in my logcat i get the following
04-18 21:03:12.649 26199-26240/com.angama_2019 W/System.err: java.net.UnknownHostException: Unable to resolve host “myserver.co.za”: No address associated with hostname
Do you maybe know why this would be I have implemented all the code correctly with no errors but i am still not able to get update I am surely missing something obvious!
Thank you for your time in advance!
Hi There again Sir, I figured out it was a problem with the emulator and the wifi but have now encountered the following error
java.lang.NumberFormatException: Invalid int: ” 1″
No matter what i change the number to in the updateVersion.txt it still complains… Any thoughts would b appreciated