io.barracks.ota.client.PackageDownloadService.java Source code

Java tutorial

Introduction

Here is the source code for io.barracks.ota.client.PackageDownloadService.java

Source

/*
 *    Copyright 2016 Barracks Solutions Inc.
 *
 *    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 io.barracks.ota.client;

import android.app.IntentService;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;

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.security.DigestException;
import java.security.DigestInputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import io.barracks.ota.client.api.UpdateDetails;
import io.barracks.ota.client.api.UpdateDownloadApi;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;

/**
 * This service is used to handle the download of a package in the background.<br>
 * It uses the {@link LocalBroadcastManager} to send updates of the download, using the categories
 * for {@link PackageDownloadService#DOWNLOAD_SUCCESS success},
 * {@link PackageDownloadService#DOWNLOAD_PROGRESS progress} and
 * {@link PackageDownloadService#DOWNLOAD_ERROR failure} for a specific {@link PackageDownloadService#ACTION_DOWNLOAD_PACKAGE action}
 *
 * @see io.barracks.ota.client.helper.BarracksHelper
 */
public class PackageDownloadService extends IntentService {
    /**
     * Defines the action used to start the download of a package.
     *
     * @see Intent#setAction(String)
     */
    public static final String ACTION_DOWNLOAD_PACKAGE = "io.barracks.ota.client.DOWNLOAD_PACKAGE";

    /**
     * This key is used to specify the {@link UpdateDetails details} used as a reference for downloading a package.
     */
    public static final String EXTRA_UPDATE_DETAILS = "updateDetails";
    /**
     * This key is used to specify the API key used for downloading a package.
     */
    public static final String EXTRA_API_KEY = "apiKey";
    /**
     * This key is used to specify the temporary destination when downloading a package.
     */
    public static final String EXTRA_TMP_DEST = "tmpDest";
    /**
     * This key is used to specify the final destination when downloading a package.
     */
    public static final String EXTRA_FINAL_DEST = "finalDest";
    /**
     * This key is used to report an {@link Throwable exception} thrown during the download.
     */
    public static final String EXTRA_EXCEPTION = "exception";
    /**
     * This key is used to report of the progress of a download
     */
    public static final String EXTRA_PROGRESS = "progress";
    /**
     * This key is used to report the callback's identifier
     */
    public static final String EXTRA_CALLBACK = "callback";

    /**
     * Category used to notify when a download is complete.
     */
    public static final String DOWNLOAD_SUCCESS = "io.barracks.ota.client.DOWNLOAD_SUCCESS";
    /**
     * Category used to notify when a download has failed.
     */
    public static final String DOWNLOAD_ERROR = "io.barracks.ota.client.DOWNLOAD_ERROR";
    /**
     * Category used to notify when a download is in progress.
     */
    public static final String DOWNLOAD_PROGRESS = "io.barracks.ota.client.DOWNLOAD_PROGRESS";

    /**
     * Intent filter used by {@link android.content.BroadcastReceiver} to register to the {@link LocalBroadcastManager}
     */
    public static final IntentFilter ACTION_DOWNLOAD_PACKAGE_FILTER;

    static {
        ACTION_DOWNLOAD_PACKAGE_FILTER = new IntentFilter(ACTION_DOWNLOAD_PACKAGE);
        ACTION_DOWNLOAD_PACKAGE_FILTER.addCategory(DOWNLOAD_SUCCESS);
        ACTION_DOWNLOAD_PACKAGE_FILTER.addCategory(DOWNLOAD_ERROR);
        ACTION_DOWNLOAD_PACKAGE_FILTER.addCategory(DOWNLOAD_PROGRESS);
    }

    public PackageDownloadService() {
        this(PackageDownloadService.class.getSimpleName());
    }

    /**
     * Creates a {@link PackageDownloadService}.
     *
     * @param name Used to name the worker thread, important only for debugging.
     */
    public PackageDownloadService(String name) {
        super(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onHandleIntent(Intent intent) {
        switch (intent.getAction()) {
        case ACTION_DOWNLOAD_PACKAGE:
            downloadPackage(intent.getStringExtra(EXTRA_API_KEY), intent.getStringExtra(EXTRA_TMP_DEST),
                    intent.getStringExtra(EXTRA_FINAL_DEST),
                    intent.<UpdateDetails>getParcelableExtra(EXTRA_UPDATE_DETAILS),
                    intent.getIntExtra(EXTRA_CALLBACK, -1));
            break;
        }
    }

    /**
     * This method is proceeding with the download and notifying the rest of the application using
     * the {@link LocalBroadcastManager}.
     *
     * @param apiKey    The API key provided by the Barracks platform.
     * @param tmpDest   The temporary path for the download.
     * @param finalDest The final path for the download.
     * @param update    The {@link UpdateDetails} retrieved from the Barracks platform.
     * @param callback  The callback identifier.
     */
    private void downloadPackage(String apiKey, String tmpDest, String finalDest, UpdateDetails update,
            int callback) {
        File tmp = TextUtils.isEmpty(tmpDest) ? new File(getFilesDir(), Defaults.DEFAULT_TMP_DL_DESTINATION)
                : new File(tmpDest);
        File destination = TextUtils.isEmpty(finalDest)
                ? new File(getFilesDir(), Defaults.DEFAULT_FINAL_DL_DESTINATION)
                : new File(finalDest);
        Retrofit retrofit = new Retrofit.Builder().baseUrl(Defaults.DEFAULT_BASE_URL).build();
        UpdateDownloadApi loader = retrofit.create(UpdateDownloadApi.class);
        Call<ResponseBody> call = loader.downloadUpdate(update.getPackageInfo().getUrl(), apiKey);

        // Setup the files to be loaded and moved
        if (!setupFile(tmp) || !setupFile(destination)) {
            notifyError(update,
                    new IOException("Failed to setup " + tmp.getPath() + " or " + destination.getPath()), callback);
            return;
        }

        // Initiate the transfer
        OutputStream os = null;
        try {
            os = new FileOutputStream(tmp);
            Response<ResponseBody> response = call.execute();
            if (!response.isSuccessful()) {
                notifyError(update, new IOException("Call to : " + call.request().url().toString() + " failed : "
                        + response.code() + " " + response.message()), callback);
                return;
            }
            InputStream is = response.body().byteStream();
            int read;
            int total = 0;
            byte buff[] = new byte[1024];
            while ((read = is.read(buff)) != -1) {
                os.write(buff, 0, read);
                total += read;
                notifyProgress(update, (int) (total * 100 / update.getPackageInfo().getSize()), callback);
            }
            checkPackageIntegrity(update, tmp);
            moveToFinalDestination(tmp, destination);
        } catch (IOException | GeneralSecurityException e) {
            notifyError(update, e, callback);
            return;
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        notifySuccess(update, destination, callback);
    }

    /**
     * Convenience method for notifying the application of a download completion using the {@link LocalBroadcastManager}
     *
     * @param details     The {@link UpdateDetails} retrieved from the Barracks platform.
     * @param destination The destination where the file has been moved.
     * @param callback    The callback identifier.
     */
    private void notifySuccess(UpdateDetails details, File destination, int callback) {
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
        manager.sendBroadcast(new Intent(ACTION_DOWNLOAD_PACKAGE).addCategory(DOWNLOAD_SUCCESS)
                .putExtra(EXTRA_UPDATE_DETAILS, details).putExtra(EXTRA_CALLBACK, callback)
                .putExtra(EXTRA_FINAL_DEST, destination.getPath()));
    }

    /**
     * Convenience method for notifying the application of a download failure using the {@link LocalBroadcastManager}
     *
     * @param details   The {@link UpdateDetails} retrieved from the Barracks platform.
     * @param exception The exception caught during the process.
     * @param callback  The callback identifier.
     */
    private void notifyError(UpdateDetails details, Exception exception, int callback) {
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
        manager.sendBroadcast(new Intent(ACTION_DOWNLOAD_PACKAGE).addCategory(DOWNLOAD_ERROR)
                .putExtra(EXTRA_UPDATE_DETAILS, details).putExtra(EXTRA_CALLBACK, callback)
                .putExtra(EXTRA_EXCEPTION, exception));
    }

    /**
     * Convenience method for notifying the application of a download progress using the {@link LocalBroadcastManager}
     *
     * @param details  The {@link UpdateDetails} retrieved from the Barracks platform.
     * @param progress The progress percentage.
     * @param callback The callback identifier.
     */
    private void notifyProgress(UpdateDetails details, int progress, int callback) {
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
        manager.sendBroadcast(new Intent(ACTION_DOWNLOAD_PACKAGE).addCategory(DOWNLOAD_PROGRESS)
                .putExtra(EXTRA_UPDATE_DETAILS, details).putExtra(EXTRA_CALLBACK, callback)
                .putExtra(EXTRA_PROGRESS, progress));
    }

    /**
     * Prepares a file for a download : deletes it, creates the parent directories.
     *
     * @param file The file to be prepared.
     * @return True if the file has been successfully prepared, false otherwise.
     */
    protected boolean setupFile(File file) {
        // Check if the destination exists
        if (file.exists()) {
            if (!file.delete()) {
                return false;
            }
        }
        // Check if the parent directory exists or can be created and is a directory
        File tmpParent = file.getParentFile();
        return (tmpParent.mkdirs() || tmpParent.exists()) && tmpParent.isDirectory();
    }

    /**
     * This method checks the package's <code>file</code> integrity.<br>
     * It uses the md5 provided in the <code>details</code> parameter.
     *
     * @param details The {@link UpdateDetails} retrieved from the Barracks platform.
     * @param file    The file which was downloaded.
     * @throws IOException              If an exception is raised while accessing the file.
     * @throws GeneralSecurityException If the hash verification fails.
     */
    protected void checkPackageIntegrity(UpdateDetails details, File file)
            throws IOException, GeneralSecurityException {
        InputStream is = null;
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5");
            is = new FileInputStream(file);
            is = new DigestInputStream(is, md);
            byte[] buffer = new byte[8192];
            while (is.read(buffer) != -1) {
            }
        } catch (NoSuchAlgorithmException | IOException e) {
            throw e;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        if (md != null) {
            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            if (!sb.toString().equals(details.getPackageInfo().getMd5())) {
                throw new DigestException(
                        "Wrong file signature " + sb.toString() + " - " + details.getPackageInfo().getMd5());
            }
        }
    }

    /**
     * This method moves the <code>temporary</code> file to its <code>destination</code>.<br>
     * If the file can't be moved, the method tries to copy it.
     *
     * @param temporary   The temporary file.
     * @param destination The destination file.
     * @throws IOException If an exception occurs during the copy phase.
     */
    protected void moveToFinalDestination(File temporary, File destination) throws IOException {
        if (!temporary.renameTo(destination)) {
            FileInputStream fis = null;
            FileOutputStream fos = null;
            try {
                fis = new FileInputStream(temporary);
                fos = new FileOutputStream(destination);
                byte[] buffer = new byte[8192];
                int read;
                while ((read = fis.read(buffer)) != -1) {
                    fos.write(buffer, 0, read);
                }
            } catch (IOException e) {
                throw e;
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new Binder();
    }

    private class Binder extends android.os.Binder {
        PackageDownloadService getService() {
            // TODO return a wrapper exposing only the necessary methods
            return PackageDownloadService.this;
        }
    }
}