2

I am working on an app for my company's internal use which will collect performance stats from network and post them on our Grafana server. The app works fine with this context, but there is a problem: App will run on a phone at a datacenter and it will be very difficult to access it if we need to update the app for adding features. Also the phone will not have internet access. So I won't be able to update the app manually , or using Google Play. I thought of writing a function to check a static URL and when we put an updated apk there, it would download it and install.

I wrote this class (copying from another Stackoverflow question):

class updateApp extends AsyncTask<String, Void, String> {
    protected String doInBackground(String... sUrl) {
        File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
        if(!file.mkdir()){
        }
        File f = new File(file.getAbsolutePath(),"YourApp.apk");
        Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
        String path = fUri.getPath();
        try {
            URL url = new URL(sUrl[0]);
            URLConnection connection = url.openConnection();
            connection.connect();
            InputStream input = new BufferedInputStream(url.openStream());
            OutputStream output = new FileOutputStream(path);

            byte data[] = new byte[1024];
            int count;
            while ((count = input.read(data)) != -1) {
                output.write(data, 0, count);
            }

            output.flush();
            output.close();
            input.close();
        } catch (Exception e) {
            Log.e("YourApp", "Well that didn't work out so well...");
            Log.e("YourApp", e.getMessage());
        }
        return path;
    }

    @Override
    protected void onPostExecute(String path) {
        Intent i = new Intent();
        i.setAction(Intent.ACTION_VIEW);
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
        File f = new File(file.getAbsolutePath(),"YourApp.apk");
        Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
        i.setDataAndType(fUri, "application/vnd.android.package-archive" );
        myCtx.startActivity(i);
    }
}

It seems to download the file successfully. And then it sends the file in the intent to the installer (I can see this because the packageinstaller selection prompt comes) But then it does not install the new apk.

Since the previous Stackoverflow question is 7 years old, I thought that updating with no user interaction may be forbidden in new API levels.

But I am not sure. How can I troubleshoot this further ?

Also, I am open to any suggestions to achieve this, maybe something making use of older API levels or anything that would solve the "updating with no internet access through an internal static URL" issue.

Thanks.

a_local_nobody
  • 7,947
  • 5
  • 29
  • 51
  • welcome to stack overflow :) asking for 3rd party libraries/software or anything else will get your question closed, as we don't provide support for those here. because this isn't the main point of your question, I've removed it – a_local_nobody Oct 05 '20 at 13:36
  • What you're trying to achieve would be better reached by using "Device Owner" and "Device Admin" functionalites in Android. This answer should provide some details: https://stackoverflow.com/a/37153867/2698179 – keag Oct 05 '20 at 14:13
  • Also, while this isn't directly relevant to the theme of the question, I will point out that Android as a platform is not really reliable for applications that you expect to run for long periods of time without intervention. If you'd like to chat briefly about the situation, I expect I could direct you to a solution that would be more reliable and cause less issues than attempting to fight with Android for what you need. – keag Oct 05 '20 at 14:25
  • @keag , I didn'T understand what you suggest. Maybe it is because English is not my native language. What do you propose actually ? – yasin tavukcuoglu Oct 05 '20 at 15:05
  • Android isn't good for this type of use. If I was to design this system from scratch I would spin up a linux virtual machine and handle this data with Python. However, if you intend to use Android for these purposes, using the information from my above link would be best. – keag Oct 05 '20 at 15:08
  • @keag. thanks. I will read about device owner and related permissions. I guess that would solve my problem if a silent install is possible with that permission. I want to use Android device for the collector app because it will simulate a mobile TV app to collect performance stats. – yasin tavukcuoglu Oct 05 '20 at 15:14
  • @keag , I couldn't find how to make the app device owner through the app. There are commands to do it using ADB. is it the only way ? – yasin tavukcuoglu Oct 05 '20 at 20:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222563/discussion-between-keag-and-yasin-tavukcuoglu). – keag Oct 05 '20 at 20:41
  • @keag , I wrote you back on chat. – yasin tavukcuoglu Oct 06 '20 at 17:56

1 Answers1

1

I followed recommendation from @keag and it worked.

1. With no "root" on the device, I made the app "device-owner" For this I added a device admin receiver class. SampleAdminReceiver.class:

import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class SampleAdminReceiver extends DeviceAdminReceiver {
    void showToast(Context context, CharSequence msg) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onEnabled(Context context, Intent intent) {
        showToast(context, "Device admin enabled");
    }
    @Override
    public void onDisabled(Context context, Intent intent) {
        showToast(context, "Device admin disabled");
    }
}

added receiver to the manifest:

    <receiver
        android:name=".SampleAdminReceiver"
        android:description="@string/app_name"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_DEVICE_ADMIN" >
        <meta-data
            android:name="android.app.device_admin"
            android:resource="@xml/device_admin_receiver" />
        <intent-filter>
            <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
        </intent-filter>
    </receiver>

Then using the adb interface I run the following dpm command:

$ dpm set-device-owner com.sample.app/.SampleAdminReceiver

Added following permission to manifest :

<uses-permission android:name="android.permission.INSTALL_PACKAGES" />

The with the following function I am able to install the apk from URL:

public static boolean installPackageX(final Context context, final String url)
            throws IOException {
        //Use an async task to run the install package method
        AsyncTask<Void,Void,Void> task = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                try {
                    PackageInstaller packageInstaller = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        packageInstaller = context.getPackageManager().getPackageInstaller();
                    }
                    PackageInstaller.SessionParams params = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        params = new PackageInstaller.SessionParams(
                                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
                    }

                    // set params
                    int sessionId = 0;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        sessionId = packageInstaller.createSession(params);
                    }
                    PackageInstaller.Session session = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        session = packageInstaller.openSession(sessionId);
                    }
                    OutputStream out = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        out = session.openWrite("COSU", 0, -1);
                    }
                    //get the input stream from the url
                    HttpURLConnection apkConn = (HttpURLConnection) new URL(url).openConnection();
                    InputStream in = apkConn.getInputStream();
                    byte[] buffer = new byte[65536];
                    int c;
                    while ((c = in.read(buffer)) != -1) {
                        out.write(buffer, 0, c);
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        session.fsync(out);
                    }
                    in.close();
                    out.close();
                    //you can replace this intent with whatever intent you want to be run when the applicaiton is finished installing
                    //I assume you have an activity called InstallComplete
                    Intent intent = new Intent(context, MainActivity.class);
                    intent.putExtra("info", "somedata");  // for extra data if needed..
                    Random generator = new Random();
                    PendingIntent i = PendingIntent.getActivity(context, generator.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        session.commit(i.getIntentSender());
                    }
                } catch (Exception ex){
                    ex.printStackTrace();
                    Log.e("AppStore","Error when installing application. Error is " + ex.getMessage());
                }

                return null;
            }
        };
        task.execute(null,null);
        return true;
    }

After that, it is just a matter of automating the process.

Btw, following code in the app is useful for removing "device owner" property.

    DevicePolicyManager dpm = (DevicePolicyManager) getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
    dpm.clearDeviceOwnerApp(getApplicationContext().getPackageName());