eu.chainfire.opendelta.UpdateService.java Source code

Java tutorial

Introduction

Here is the source code for eu.chainfire.opendelta.UpdateService.java

Source

/* 
 * Copyright (C) 2013-2014 Jorrit "Chainfire" Jongma
 * Copyright (C) 2013-2014 The OmniROM Project
 */
/* 
 * This file is part of OpenDelta.
 * 
 * OpenDelta is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * OpenDelta is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with OpenDelta. If not, see <http://www.gnu.org/licenses/>.
 */

package eu.chainfire.opendelta;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.wifi.WifiManager;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.StatFs;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;

import eu.chainfire.opendelta.BatteryState.OnBatteryStateListener;
import eu.chainfire.opendelta.NetworkState.OnNetworkStateListener;
import eu.chainfire.opendelta.Scheduler.OnWantUpdateCheckListener;
import eu.chainfire.opendelta.ScreenState.OnScreenStateListener;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
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.json.JSONException;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class UpdateService extends Service implements OnNetworkStateListener, OnBatteryStateListener,
        OnScreenStateListener, OnWantUpdateCheckListener, OnSharedPreferenceChangeListener {
    public static void start(Context context) {
        start(context, null);
    }

    public static void startCheck(Context context) {
        start(context, ACTION_CHECK);
    }

    public static void startFlash(Context context) {
        start(context, ACTION_FLASH);
    }

    private static void start(Context context, String action) {
        Intent i = new Intent(context, UpdateService.class);
        i.setAction(action);
        context.startService(i);
    }

    public static PendingIntent alarmPending(Context context, int id) {
        Intent intent = new Intent(context, UpdateService.class);
        intent.setAction(ACTION_ALARM);
        intent.putExtra(EXTRA_ALARM_ID, id);
        return PendingIntent.getService(context, id, intent, 0);
    }

    public static final String ACTION_SYSTEM_UPDATE_SETTINGS = "android.settings.SYSTEM_UPDATE_SETTINGS";
    public static final String PERMISSION_ACCESS_CACHE_FILESYSTEM = "android.permission.ACCESS_CACHE_FILESYSTEM";
    public static final String PERMISSION_REBOOT = "android.permission.REBOOT";

    public static final String BROADCAST_INTENT = "eu.chainfire.opendelta.intent.BROADCAST_STATE";
    public static final String EXTRA_STATE = "eu.chainfire.opendelta.extra.ACTION_STATE";
    public static final String EXTRA_LAST_CHECK = "eu.chainfire.opendelta.extra.LAST_CHECK";
    public static final String EXTRA_PROGRESS = "eu.chainfire.opendelta.extra.PROGRESS";
    public static final String EXTRA_CURRENT = "eu.chainfire.opendelta.extra.CURRENT";
    public static final String EXTRA_TOTAL = "eu.chainfire.opendelta.extra.TOTAL";
    public static final String EXTRA_FILENAME = "eu.chainfire.opendelta.extra.FILENAME";
    public static final String EXTRA_MS = "eu.chainfire.opendelta.extra.MS";

    public static final String STATE_ACTION_NONE = "action_none";
    public static final String STATE_ACTION_CHECKING = "action_checking";
    public static final String STATE_ACTION_CHECKING_MD5 = "action_checking_md5";
    public static final String STATE_ACTION_SEARCHING = "action_searching";
    public static final String STATE_ACTION_SEARCHING_MD5 = "action_searching_md5";
    public static final String STATE_ACTION_DOWNLOADING = "action_downloading";
    public static final String STATE_ACTION_APPLYING = "action_applying";
    public static final String STATE_ACTION_APPLYING_PATCH = "action_applying_patch";
    public static final String STATE_ACTION_APPLYING_MD5 = "action_applying_md5";
    public static final String STATE_ACTION_READY = "action_ready";
    public static final String STATE_ERROR_DISK_SPACE = "error_disk_space";
    public static final String STATE_ERROR_UNKNOWN = "error_unknown";

    private static final String ACTION_CHECK = "eu.chainfire.opendelta.action.CHECK";
    private static final String ACTION_FLASH = "eu.chainfire.opendelta.action.FLASH";
    private static final String ACTION_ALARM = "eu.chainfire.opendelta.action.ALARM";
    private static final String EXTRA_ALARM_ID = "eu.chainfire.opendelta.extra.ALARM_ID";
    private static final String ACTION_NOTIFICATION_DELETED = "eu.chainfire.opendelta.action.NOTIFICATION_DELETED";

    private static final int NOTIFICATION_BUSY = 1;
    private static final int NOTIFICATION_UPDATE = 2;

    private static final String PREF_READY_FILENAME_NAME = "ready_filename";
    private static final String PREF_READY_FILENAME_DEFAULT = null;

    private static final String PREF_LAST_CHECK_TIME_NAME = "last_check_time";
    private static final long PREF_LAST_CHECK_TIME_DEFAULT = 0L;

    private static final String PREF_LAST_SNOOZE_TIME_NAME = "last_snooze_time";
    private static final long PREF_LAST_SNOOZE_TIME_DEFAULT = 0L;

    private static final long SNOOZE_MS = 24 * AlarmManager.INTERVAL_HOUR;

    public static final String PREF_AUTO_UPDATE_NETWORKS_NAME = "auto_update_networks";
    public static final int PREF_AUTO_UPDATE_NETWORKS_DEFAULT = NetworkState.ALLOW_WIFI
            | NetworkState.ALLOW_ETHERNET;

    public static boolean isStateBusy(String state) {
        return !(state.equals(STATE_ACTION_NONE) || state.equals(STATE_ACTION_READY)
                || state.equals(STATE_ERROR_UNKNOWN) || state.equals(STATE_ERROR_DISK_SPACE));
    }

    private Config config;

    private HandlerThread handlerThread;
    private Handler handler;

    private String state = STATE_ACTION_NONE;

    private NetworkState networkState = null;
    private BatteryState batteryState = null;
    private ScreenState screenState = null;

    private Scheduler scheduler = null;

    private PowerManager.WakeLock wakeLock = null;
    private WifiManager.WifiLock wifiLock = null;

    private NotificationManager notificationManager = null;
    private SharedPreferences prefs = null;

    /*
     * Using reflection voodoo instead calling the hidden class directly, to
     * dev/test outside of AOSP tree
     */
    private boolean setPermissions(String path, int mode, int uid, int gid) {
        try {
            Class<?> FileUtils = getClassLoader().loadClass("android.os.FileUtils");
            Method setPermissions = FileUtils.getDeclaredMethod("setPermissions",
                    new Class[] { String.class, int.class, int.class, int.class });
            return ((Integer) setPermissions.invoke(null,
                    new Object[] { path, Integer.valueOf(mode), Integer.valueOf(uid), Integer.valueOf(gid) }) == 0);
        } catch (Exception e) {
            // A lot of voodoo could go wrong here, return failure instead of
            // crash
            Logger.ex(e);
        }
        return false;
    }

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

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate() {
        super.onCreate();

        config = Config.getInstance(this);

        wakeLock = ((PowerManager) getSystemService(POWER_SERVICE)).newWakeLock(
                config.getKeepScreenOn() ? PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
                        : PowerManager.PARTIAL_WAKE_LOCK,
                "OpenDelta WakeLock");
        wifiLock = ((WifiManager) getSystemService(WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL,
                "OpenDelta WifiLock");

        handlerThread = new HandlerThread("OpenDelta Service Thread");
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper());

        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        prefs = PreferenceManager.getDefaultSharedPreferences(this);

        scheduler = new Scheduler(this, this);

        networkState = new NetworkState();
        networkState.start(this, this,
                prefs.getInt(PREF_AUTO_UPDATE_NETWORKS_NAME, PREF_AUTO_UPDATE_NETWORKS_DEFAULT));

        batteryState = new BatteryState();
        batteryState.start(this, this, 50, true);

        screenState = new ScreenState();
        screenState.start(this, this);

        prefs.registerOnSharedPreferenceChangeListener(this);

        autoState();
    }

    @Override
    public void onDestroy() {
        prefs.unregisterOnSharedPreferenceChangeListener(this);
        networkState.stop();
        batteryState.stop();
        screenState.stop();
        handlerThread.quitSafely();

        super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            if (ACTION_CHECK.equals(intent.getAction())) {
                checkForUpdates(true);
            } else if (ACTION_FLASH.equals(intent.getAction())) {
                flashUpdate();
            } else if (ACTION_ALARM.equals(intent.getAction())) {
                scheduler.alarm(intent.getIntExtra(EXTRA_ALARM_ID, -1));
                autoState();
            } else if (ACTION_NOTIFICATION_DELETED.equals(intent.getAction())) {
                Logger.i("Snoozing for 24 hours");
                prefs.edit().putLong(PREF_LAST_SNOOZE_TIME_NAME, System.currentTimeMillis()).commit();
                autoState();
            }
        }

        return START_STICKY;
    }

    private synchronized void updateState(String state, Float progress, Long current, Long total, String filename,
            Long ms) {
        this.state = state;

        Intent i = new Intent(BROADCAST_INTENT);
        i.putExtra(EXTRA_STATE, state);
        if (progress != null)
            i.putExtra(EXTRA_PROGRESS, progress);
        if (current != null)
            i.putExtra(EXTRA_CURRENT, current);
        if (total != null)
            i.putExtra(EXTRA_TOTAL, total);
        if (filename != null)
            i.putExtra(EXTRA_FILENAME, filename);
        if (ms != null)
            i.putExtra(EXTRA_MS, ms);
        sendStickyBroadcast(i);
    }

    @Override
    public void onNetworkState(boolean state) {
        Logger.d("network state --> %d", state ? 1 : 0);
    }

    @Override
    public void onBatteryState(boolean state) {
        Logger.d("battery state --> %d", state ? 1 : 0);
    }

    @Override
    public void onScreenState(boolean state) {
        Logger.d("screen state --> %d", state ? 1 : 0);
        scheduler.onScreenState(state);
    }

    @Override
    public boolean onWantUpdateCheck() {
        Logger.i("Scheduler requests check for updates");
        return checkForUpdates(false);
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (PREF_AUTO_UPDATE_NETWORKS_NAME.equals(key)) {
            networkState.updateFlags(
                    sharedPreferences.getInt(PREF_AUTO_UPDATE_NETWORKS_NAME, PREF_AUTO_UPDATE_NETWORKS_DEFAULT));
        }
    }

    private void autoState() {
        String filename = prefs.getString(PREF_READY_FILENAME_NAME, PREF_READY_FILENAME_DEFAULT);

        if (filename != null) {
            if (!(new File(filename)).exists()) {
                filename = null;
                prefs.edit().putString(PREF_READY_FILENAME_NAME, PREF_READY_FILENAME_DEFAULT).commit();
            }
        }

        if (filename == null) {
            Logger.i("System up to date");
            stopNotification();
            updateState(STATE_ACTION_NONE, null, null, null, null,
                    prefs.getLong(PREF_LAST_CHECK_TIME_NAME, PREF_LAST_CHECK_TIME_DEFAULT));
        } else {
            Logger.i("Update found: %s", filename);
            updateState(STATE_ACTION_READY, null, null, null, (new File(filename)).getName(),
                    prefs.getLong(PREF_LAST_CHECK_TIME_NAME, PREF_LAST_CHECK_TIME_DEFAULT));

            // check if we're snoozed, using abs for clock changes
            if (Math.abs(System.currentTimeMillis()
                    - prefs.getLong(PREF_LAST_SNOOZE_TIME_NAME, PREF_LAST_SNOOZE_TIME_DEFAULT)) > SNOOZE_MS) {
                startNotification();
            } else {
                stopNotification();
            }
        }
    }

    private PendingIntent getNotificationIntent(boolean delete) {
        if (delete) {
            Intent notificationIntent = new Intent(this, UpdateService.class);
            notificationIntent.setAction(ACTION_NOTIFICATION_DELETED);
            return PendingIntent.getService(this, 0, notificationIntent, 0);
        } else {
            Intent notificationIntent = new Intent(this, MainActivity.class);
            notificationIntent.setAction(ACTION_SYSTEM_UPDATE_SETTINGS);
            return PendingIntent.getActivity(this, 0, notificationIntent, 0);
        }
    }

    private void startNotification() {
        notificationManager.notify(NOTIFICATION_UPDATE,
                (new Notification.Builder(this)).setSmallIcon(R.drawable.stat_notify_update)
                        .setContentTitle(getString(R.string.title))
                        .setContentText(getString(R.string.notify_update_available))
                        .setTicker(getString(R.string.notify_update_available)).setShowWhen(false)
                        .setContentIntent(getNotificationIntent(false)).setDeleteIntent(getNotificationIntent(true))
                        .build());
    }

    private void stopNotification() {
        notificationManager.cancel(NOTIFICATION_UPDATE);
    }

    private byte[] downloadUrlMemory(String url) {
        Logger.d("download: %s", url);
        try {
            HttpParams params = new BasicHttpParams();
            HttpConnectionParams.setConnectionTimeout(params, 10000);
            HttpConnectionParams.setSoTimeout(params, 10000);
            HttpClient client = new DefaultHttpClient(params);
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            int len = (int) response.getEntity().getContentLength();
            if ((len >= 0) && (len < 1024 * 1024)) {
                byte[] ret = new byte[len];
                InputStream in = response.getEntity().getContent();
                int pos = 0;
                while (pos < len) {
                    int r = in.read(ret, pos, len - pos);
                    pos += r;
                    if (r <= 0)
                        return null;
                }
                return ret;
            }
            return null;
        } catch (Exception e) {
            // Download failed for any number of reasons, timeouts, connection
            // drops, etc. Just log it in debugging mode.
            Logger.ex(e);
            return null;
        }
    }

    private boolean downloadUrlFile(String url, File f, String matchMD5,
            DeltaInfo.ProgressListener progressListener) {
        Logger.d("download: %s", url);

        MessageDigest digest = null;
        if (matchMD5 != null) {
            try {
                digest = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                // No MD5 algorithm support
                Logger.ex(e);
            }
        }

        if (f.exists())
            f.delete();
        try {
            HttpParams params = new BasicHttpParams();
            HttpConnectionParams.setConnectionTimeout(params, 10000);
            HttpConnectionParams.setSoTimeout(params, 10000);
            HttpClient client = new DefaultHttpClient(params);
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            long len = (int) response.getEntity().getContentLength();
            long recv = 0;
            if ((len > 0) && (len < 4L * 1024L * 1024L * 1024L)) {
                byte[] buffer = new byte[262144];

                InputStream is = response.getEntity().getContent();
                FileOutputStream os = new FileOutputStream(f, false);
                try {
                    int r;
                    while ((r = is.read(buffer)) > 0) {
                        os.write(buffer, 0, r);
                        if (digest != null)
                            digest.update(buffer, 0, r);

                        recv += (long) r;
                        if (progressListener != null)
                            progressListener.onProgress(((float) recv / (float) len) * 100f, recv, len);
                    }
                } finally {
                    os.close();
                }

                if (digest != null) {
                    String MD5 = new BigInteger(1, digest.digest()).toString(16).toLowerCase(Locale.ENGLISH);
                    while (MD5.length() < 32)
                        MD5 = "0" + MD5;
                    return MD5.equals(matchMD5);
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            // Download failed for any number of reasons, timeouts, connection
            // drops, etc. Just log it in debugging mode.
            Logger.ex(e);
            return false;
        }
    }

    private DeltaInfo.ProgressListener getMD5Progress(String state, String filename) {
        final long[] last = new long[] { 0, SystemClock.elapsedRealtime() };
        final String _state = state;
        final String _filename = filename;

        return new DeltaInfo.ProgressListener() {
            @Override
            public void onProgress(float progress, long current, long total) {
                long now = SystemClock.elapsedRealtime();
                if (now >= last[0] + 16L) {
                    updateState(_state, progress, current, total, _filename,
                            SystemClock.elapsedRealtime() - last[1]);
                    last[0] = now;
                }
            }
        };
    }

    private String findZIPOnSD(DeltaInfo.FileFull zip, File base) {
        Logger.d("scanning: %s", base.getAbsolutePath());
        File[] list = base.listFiles();
        if (list != null) {
            for (File f : list) {
                if (!f.isDirectory() && f.getName().endsWith(".zip")) {
                    Logger.d("checking: %s", f.getAbsolutePath());

                    boolean ok = (zip.match(f, true,
                            getMD5Progress(STATE_ACTION_SEARCHING_MD5, f.getName())) != null);
                    updateState(STATE_ACTION_SEARCHING, null, null, null, null, null);

                    if (ok)
                        return f.getAbsolutePath();
                }
            }

            for (File f : list) {
                if (f.isDirectory()) {
                    String ret = findZIPOnSD(zip, f);
                    if (ret != null)
                        return ret;
                }
            }
        }

        return null;
    }

    private long sizeOnDisk(long size) {
        // Assuming 256k block size here, should be future proof for a little
        // bit
        long blocks = (size + 262143L) / 262144L;
        return blocks * 262144L;
    }

    private boolean downloadDeltaFile(String url_base, DeltaInfo.FileBase fileBase, DeltaInfo.FileSizeMD5 match,
            DeltaInfo.ProgressListener progressListener, boolean force) {
        if (fileBase.getTag() == null) {
            if (force || networkState.getState()) {
                String url = url_base + fileBase.getName();
                String fn = config.getPathBase() + fileBase.getName();
                File f = new File(fn);
                Logger.d("download: %s --> %s", url, fn);

                if (downloadUrlFile(url, f, match.getMD5(), progressListener)) {
                    fileBase.setTag(fn);
                    Logger.d("success");
                    return true;
                } else {
                    f.delete();
                    Logger.d("download error");
                    return false;
                }
            } else {
                Logger.d("aborting download due to network state");
                return false;
            }
        } else {
            Logger.d("have %s already", fileBase.getName());
            return true;
        }
    }

    private Thread getThreadedProgress(String filename, String display, long start, long currentOut,
            long totalOut) {
        final File _file = new File(filename);
        final String _display = display;
        final long _currentOut = currentOut;
        final long _totalOut = totalOut;
        final long _start = start;

        return new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        long current = _currentOut + _file.length();
                        updateState(STATE_ACTION_APPLYING_PATCH, ((float) current / (float) _totalOut) * 100f,
                                current, _totalOut, _display, SystemClock.elapsedRealtime() - _start);

                        Thread.sleep(16);
                    } catch (InterruptedException e) {
                        // We're being told to quit
                        break;
                    }
                }
            }
        });
    }

    private boolean zipadjust(String filenameIn, String filenameOut, long start, long currentOut, long totalOut) {
        Logger.d("zipadjust [%s] --> [%s]", filenameIn, filenameOut);

        // checking filesizes in the background as progress, because these
        // native functions don't have callbacks (yet) to do this

        (new File(filenameOut)).delete();

        Thread progress = getThreadedProgress(filenameOut, (new File(filenameIn)).getName(), start, currentOut,
                totalOut);
        progress.start();

        int ok = Native.zipadjust(filenameIn, filenameOut, 1);

        progress.interrupt();
        try {
            progress.join();
        } catch (InterruptedException e) {
            // We got interrupted in a very short wait, surprising, but not a
            // problem. 'progress' will quit by itself.
            Logger.ex(e);
        }

        Logger.d("zipadjust --> %d", ok);

        return (ok == 1);
    }

    private boolean dedelta(String filenameSource, String filenameDelta, String filenameOut, long start,
            long currentOut, long totalOut) {
        Logger.d("dedelta [%s] --> [%s] --> [%s]", filenameSource, filenameDelta, filenameOut);

        // checking filesizes in the background as progress, because these
        // native functions don't have callbacks (yet) to do this

        (new File(filenameOut)).delete();

        Thread progress = getThreadedProgress(filenameOut, (new File(filenameDelta)).getName(), start, currentOut,
                totalOut);
        progress.start();

        int ok = Native.dedelta(filenameSource, filenameDelta, filenameOut);

        progress.interrupt();
        try {
            progress.join();
        } catch (InterruptedException e) {
            // We got interrupted in a very short wait, surprising, but not a
            // problem. 'progress' will quit by itself.
            Logger.ex(e);
        }

        Logger.d("dedelta --> %d", ok);

        return (ok == 1);
    }

    private boolean checkForUpdates(boolean userInitiated) {
        /*
         * Unless the user is specifically asking to check for updates, we only
         * check for them if we have a connection matching the user's set
         * preferences, we're charging and/or have juice aplenty, and the screen
         * is off
         */

        if ((networkState == null) || (batteryState == null) || (screenState == null))
            return false;

        if (!isStateBusy(state) && (userInitiated
                || (networkState.getState() && batteryState.getState() && !screenState.getState()))) {
            Logger.i("Starting check for updates");
            checkForUpdatesAsync(userInitiated);
            return true;
        } else {
            Logger.i("Ignoring request to check for updates");
        }
        return false;
    }

    private long getDeltaDownloadSize(List<DeltaInfo> deltas) {
        updateState(STATE_ACTION_CHECKING, null, null, null, null, null);

        long deltaDownloadSize = 0L;
        for (DeltaInfo di : deltas) {
            String fn = config.getPathBase() + di.getUpdate().getName();
            if (di.getUpdate().match(new File(fn), true,
                    getMD5Progress(STATE_ACTION_CHECKING_MD5, di.getUpdate().getName())) == di.getUpdate()
                            .getUpdate()) {
                di.getUpdate().setTag(fn);
            } else {
                deltaDownloadSize += di.getUpdate().getUpdate().getSize();
            }
        }

        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);
        {
            if (config.getApplySignature()) {
                String fn = config.getPathBase() + lastDelta.getSignature().getName();
                if (lastDelta.getSignature().match(new File(fn), true,
                        getMD5Progress(STATE_ACTION_CHECKING_MD5, lastDelta.getSignature().getName())) == lastDelta
                                .getSignature().getUpdate()) {
                    lastDelta.getSignature().setTag(fn);
                } else {
                    deltaDownloadSize += lastDelta.getSignature().getUpdate().getSize();
                }
            }
        }

        updateState(STATE_ACTION_CHECKING, null, null, null, null, null);

        return deltaDownloadSize;
    }

    private long getFullDownloadSize(List<DeltaInfo> deltas) {
        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);

        long fullDownloadSize = 0;
        {
            String fn = config.getPathBase() + lastDelta.getOut().getName();
            if (lastDelta.getOut().match(new File(fn), true,
                    getMD5Progress(STATE_ACTION_CHECKING_MD5, lastDelta.getOut().getName())) == lastDelta.getOut()
                            .getOfficial()) {
                lastDelta.getOut().setTag(fn);
            } else {
                fullDownloadSize += lastDelta.getOut().getOfficial().getSize();
            }
            updateState(STATE_ACTION_CHECKING, null, null, null, null, null);
        }
        return fullDownloadSize;
    }

    private long getRequiredSpace(List<DeltaInfo> deltas, boolean getFull) {
        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);

        long requiredSpace = 0;
        if (getFull) {
            requiredSpace += sizeOnDisk(
                    lastDelta.getOut().getTag() != null ? 0 : lastDelta.getOut().getOfficial().getSize());
        } else {
            // The resulting number will be a tad more than worst case what we
            // actually need, but not dramatically so

            for (DeltaInfo di : deltas) {
                if (di.getUpdate().getTag() == null)
                    requiredSpace += sizeOnDisk(di.getUpdate().getUpdate().getSize());
            }
            if (config.getApplySignature()) {
                requiredSpace += sizeOnDisk(lastDelta.getSignature().getUpdate().getSize());
            }

            long biggest = 0;
            for (DeltaInfo di : deltas)
                biggest = Math.max(biggest, sizeOnDisk(di.getUpdate().getApplied().getSize()));

            requiredSpace += 2 * sizeOnDisk(biggest);
        }

        return requiredSpace;
    }

    private String findInitialFile(List<DeltaInfo> deltas, String possibleMatch, boolean[] needsProcessing) {
        // Find the currently flashed ZIP, or a newer one

        DeltaInfo firstDelta = deltas.get(0);

        updateState(STATE_ACTION_SEARCHING, null, null, null, null, null);

        String initialFile = null;

        // Check if an original flashable ZIP is in our preferred location
        String expectedLocation = config.getPathBase() + firstDelta.getIn().getName();
        DeltaInfo.FileSizeMD5 match = null;
        if (expectedLocation.equals(possibleMatch)) {
            match = firstDelta.getIn().match(new File(expectedLocation), false, null);
            if (match != null) {
                initialFile = possibleMatch;
            }
        }

        if (match == null) {
            match = firstDelta.getIn().match(new File(expectedLocation), true,
                    getMD5Progress(STATE_ACTION_SEARCHING_MD5, firstDelta.getIn().getName()));
            if (match != null) {
                initialFile = expectedLocation;
            }
        }
        updateState(STATE_ACTION_SEARCHING, null, null, null, null, null);

        // If the user flashed manually, the file is probably not in our
        // preferred location (assuming it wasn't sideloaded), so search
        // the storages for it.
        if (initialFile == null) {
            // Primary external storage ( == internal storage)
            initialFile = findZIPOnSD(firstDelta.getIn(), Environment.getExternalStorageDirectory());

            if (initialFile == null) {
                // Search secondary external storages ( == sdcards, OTG drives, etc)                 
                String secondaryStorages = System.getenv("SECONDARY_STORAGE");
                if ((secondaryStorages != null) && (secondaryStorages.length() > 0)) {
                    String[] storages = TextUtils.split(secondaryStorages, File.pathSeparator);
                    for (String storage : storages) {
                        initialFile = findZIPOnSD(firstDelta.getIn(), new File(storage));
                        if (initialFile != null) {
                            break;
                        }
                    }
                }
            }

            if (initialFile != null) {
                match = firstDelta.getIn().match(new File(initialFile), false, null);
            }
        }

        if ((needsProcessing != null) && (needsProcessing.length > 0)) {
            needsProcessing[0] = (initialFile != null) && (match != firstDelta.getIn().getStore());
        }

        return initialFile;
    }

    private boolean downloadFiles(List<DeltaInfo> deltas, boolean getFull, long totalDownloadSize, boolean force) {
        // Download all the files we do not have yet

        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);

        final String[] filename = new String[] { null };
        updateState(STATE_ACTION_DOWNLOADING, 0f, 0L, totalDownloadSize, null, null);

        final long[] last = new long[] { 0, totalDownloadSize, 0, SystemClock.elapsedRealtime() };
        DeltaInfo.ProgressListener progressListener = new DeltaInfo.ProgressListener() {
            @Override
            public void onProgress(float progress, long current, long total) {
                current += last[0];
                total = last[1];
                progress = ((float) current / (float) total) * 100f;
                long now = SystemClock.elapsedRealtime();
                if (now >= last[2] + 16L) {
                    updateState(STATE_ACTION_DOWNLOADING, progress, current, total, filename[0],
                            SystemClock.elapsedRealtime() - last[3]);
                    last[2] = now;
                }
            }
        };

        if (getFull) {
            filename[0] = lastDelta.getOut().getName();
            if (!downloadDeltaFile(config.getUrlBaseFull(), lastDelta.getOut(), lastDelta.getOut().getOfficial(),
                    progressListener, force)) {
                updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                Logger.d("download error");
                return false;
            }
        } else {
            for (DeltaInfo di : deltas) {
                filename[0] = di.getUpdate().getName();
                if (!downloadDeltaFile(config.getUrlBaseUpdate(), di.getUpdate(), di.getUpdate().getUpdate(),
                        progressListener, force)) {
                    updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                    Logger.d("download error");
                    return false;
                }
                last[0] += di.getUpdate().getUpdate().getSize();
            }

            if (config.getApplySignature()) {
                filename[0] = lastDelta.getSignature().getName();
                if (!downloadDeltaFile(config.getUrlBaseUpdate(), lastDelta.getSignature(),
                        lastDelta.getSignature().getUpdate(), progressListener, force)) {
                    updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                    Logger.d("download error");
                    return false;
                }
            }
        }
        updateState(STATE_ACTION_DOWNLOADING, 100f, totalDownloadSize, totalDownloadSize, null, null);

        return true;
    }

    private boolean applyPatches(List<DeltaInfo> deltas, String initialFile, boolean initialFileNeedsProcessing) {
        // Create storeSigned outfile from infile + deltas

        DeltaInfo firstDelta = deltas.get(0);
        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);

        int tempFile = 0;
        String[] tempFiles = new String[] { config.getPathBase() + "temp1", config.getPathBase() + "temp2" };
        try {
            long start = SystemClock.elapsedRealtime();
            long current = 0L;
            long total = 0L;

            if (initialFileNeedsProcessing)
                total += firstDelta.getIn().getStore().getSize();
            for (DeltaInfo di : deltas)
                total += di.getUpdate().getApplied().getSize();
            if (config.getApplySignature())
                total += lastDelta.getSignature().getApplied().getSize();

            if (initialFileNeedsProcessing) {
                if (!zipadjust(initialFile, tempFiles[tempFile], start, current, total)) {
                    updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                    Logger.d("zipadjust error");
                    return false;
                }
                tempFile = (tempFile + 1) % 2;
                current += firstDelta.getIn().getStore().getSize();
            }

            for (DeltaInfo di : deltas) {
                String inFile = tempFiles[(tempFile + 1) % 2];
                if (!initialFileNeedsProcessing && (di == firstDelta))
                    inFile = initialFile;
                String outFile = tempFiles[tempFile];
                if (!config.getApplySignature() && (di == lastDelta))
                    outFile = config.getPathBase() + lastDelta.getOut().getName();

                if (!dedelta(inFile, config.getPathBase() + di.getUpdate().getName(), outFile, start, current,
                        total)) {
                    updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                    Logger.d("dedelta error");
                    return false;
                }
                tempFile = (tempFile + 1) % 2;
                current += di.getUpdate().getApplied().getSize();
            }

            if (config.getApplySignature()) {
                if (!dedelta(tempFiles[(tempFile + 1) % 2],
                        config.getPathBase() + lastDelta.getSignature().getName(),
                        config.getPathBase() + lastDelta.getOut().getName(), start, current, total)) {
                    updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                    Logger.d("dedelta error");
                    return false;
                }
                tempFile = (tempFile + 1) % 2;
                current += lastDelta.getSignature().getApplied().getSize();
            }
        } finally {
            (new File(tempFiles[0])).delete();
            (new File(tempFiles[1])).delete();
        }

        return true;
    }

    private void writeString(OutputStream os, String s) throws UnsupportedEncodingException, IOException {
        os.write((s + "\n").getBytes("UTF-8"));
    }

    @SuppressLint("SdCardPath")
    private void flashUpdate() {
        if (getPackageManager().checkPermission(PERMISSION_ACCESS_CACHE_FILESYSTEM,
                getPackageName()) != PackageManager.PERMISSION_GRANTED) {
            Logger.d("[%s] required beyond this point", PERMISSION_ACCESS_CACHE_FILESYSTEM);
            return;
        }

        if (getPackageManager().checkPermission(PERMISSION_REBOOT,
                getPackageName()) != PackageManager.PERMISSION_GRANTED) {
            Logger.d("[%s] required beyond this point", PERMISSION_REBOOT);
            return;
        }

        String flashFilename = prefs.getString(PREF_READY_FILENAME_NAME, PREF_READY_FILENAME_DEFAULT);
        prefs.edit().putString(PREF_READY_FILENAME_NAME, PREF_READY_FILENAME_DEFAULT).commit();
        if ((flashFilename == null) || !flashFilename.startsWith(config.getPathBase()))
            return;

        // Remove the path to the storage from the filename, so we get a path
        // relative to the root of the storage
        String path_sd = Environment.getExternalStorageDirectory() + File.separator;
        flashFilename = flashFilename.substring(path_sd.length());

        // Find additional ZIPs to flash, strip path to sd
        List<String> extras = config.getFlashAfterUpdateZIPs();
        for (int i = 0; i < extras.size(); i++) {
            extras.set(i, extras.get(i).substring(path_sd.length()));
        }

        try {
            // TWRP - OpenRecoveryScript - the recovery will find the correct
            // storage root for the ZIPs, life is nice and easy.
            //
            // Optionally, we're injecting our own signature verification keys
            // and verifying against those. We place these keys in /cache
            // where only privileged apps can edit, contrary to the storage
            // location of the ZIP itself - anyone can modify the ZIP.
            // As such, flashing the ZIP without checking the whole-file
            // signature coming from a secure location would be a security
            // risk.
            {
                if (config.getInjectSignatureEnable()) {
                    FileOutputStream os = new FileOutputStream("/cache/recovery/keys", false);
                    try {
                        writeString(os, config.getInjectSignatureKeys());
                    } finally {
                        os.close();
                    }
                    setPermissions("/cache/recovery/keys", 0644, Process.myUid(), 2001 /* AID_CACHE */);
                }

                FileOutputStream os = new FileOutputStream("/cache/recovery/openrecoveryscript", false);
                try {
                    if (config.getInjectSignatureEnable()) {
                        writeString(os, "cmd cat /res/keys > /res/keys_org");
                        writeString(os, "cmd cat /cache/recovery/keys > /res/keys");
                        writeString(os, "set tw_signed_zip_verify 1");
                        writeString(os, String.format("install %s", flashFilename));
                        writeString(os, "set tw_signed_zip_verify 0");
                        writeString(os, "cmd cat /res/keys_org > /res/keys");
                        writeString(os, "cmd rm /res/keys_org");
                    } else {
                        writeString(os, "set tw_signed_zip_verify 0");
                        writeString(os, String.format("install %s", flashFilename));
                    }

                    if (!config.getSecureModeCurrent()) {
                        // any program could have placed these ZIPs, so ignore
                        // them in secure mode
                        for (String file : extras) {
                            writeString(os, String.format("install %s", file));
                        }
                    }
                    writeString(os, "wipe cache");
                } finally {
                    os.close();
                }

                setPermissions("/cache/recovery/openrecoveryscript", 0644, Process.myUid(), 2001 /* AID_CACHE */);
            }

            // CWM - ExtendedCommand - provide paths to both internal and
            // external storage locations, it's nigh impossible to know in
            // practice which will be correct, not just because the internal
            // storage location varies based on the external storage being
            // present, but also because it's not uncommon for community-built
            // versions to have them reversed. It'll give some horrible looking
            // results, but it seems to continue installing even if one ZIP
            // fails and produce the wanted result. Better than nothing ...
            //
            // We don't generate a CWM script in secure mode, because it
            // doesn't support checking our custom signatures
            if (!config.getSecureModeCurrent()) {
                FileOutputStream os = new FileOutputStream("/cache/recovery/extendedcommand", false);
                try {
                    writeString(os, String.format("install_zip(\"%s%s\");", "/sdcard/", flashFilename));
                    writeString(os, String.format("install_zip(\"%s%s\");", "/emmc/", flashFilename));
                    for (String file : extras) {
                        writeString(os, String.format("install_zip(\"%s%s\");", "/sdcard/", file));
                        writeString(os, String.format("install_zip(\"%s%s\");", "/emmc/", file));
                    }
                    writeString(os, "run_program(\"/sbin/busybox\", \"rm\", \"-rf\", \"/cache/*\");");
                } finally {
                    os.close();
                }

                setPermissions("/cache/recovery/extendedcommand", 0644, Process.myUid(), 2001 /* AID_CACHE */);
            } else {
                (new File("/cache/recovery/extendedcommand")).delete();
            }

            ((PowerManager) getSystemService(Context.POWER_SERVICE)).reboot("recovery");
        } catch (Exception e) {
            // We have failed to write something. There's not really anything
            // else to do at
            // at this stage than give up. No reason to crash though.
            Logger.ex(e);
        }
    }

    private void checkForUpdatesAsync(boolean userInitiated) {
        updateState(STATE_ACTION_CHECKING, null, null, null, null, null);
        wakeLock.acquire();
        wifiLock.acquire();

        stopNotification();

        Notification notification = (new Notification.Builder(this)).setSmallIcon(R.drawable.stat_notify_update)
                .setContentTitle(getString(R.string.title)).setContentText(getString(R.string.notify_checking))
                .setTicker(getString(R.string.notify_checking)).setShowWhen(false)
                .setContentIntent(getNotificationIntent(false)).build();
        startForeground(NOTIFICATION_BUSY, notification);

        final boolean force = userInitiated;

        handler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    List<DeltaInfo> deltas = new ArrayList<DeltaInfo>();

                    String flashFilename = null;
                    (new File(config.getPathBase())).mkdir();
                    (new File(config.getPathFlashAfterUpdate())).mkdir();

                    // Create a list of deltas to apply to get from our current
                    // version to the latest
                    String fetch = String.format(Locale.ENGLISH, "%s%s.delta", config.getUrlBaseDelta(),
                            config.getFilenameBase());
                    while (true) {
                        DeltaInfo delta = null;

                        try {
                            delta = new DeltaInfo(downloadUrlMemory(fetch), false);
                        } catch (JSONException e) {
                            // There's an error in the JSON. Could be bad JSON,
                            // could be a 404 text, etc
                            Logger.ex(e);
                        } catch (NullPointerException e) {
                            // Download failed
                            Logger.ex(e);
                        }

                        if (delta == null) {
                            // See if we have a revoked version instead, we
                            // still need it for chaining future deltas, but
                            // will not allow flashing this one
                            try {
                                delta = new DeltaInfo(downloadUrlMemory(fetch.replace(".delta", ".delta_revoked")),
                                        true);
                            } catch (JSONException e) {
                                // There's an error in the JSON. Could be bad
                                // JSON, could be a 404 text, etc
                                Logger.ex(e);
                            } catch (NullPointerException e) {
                                // Download failed
                                Logger.ex(e);
                            }

                            // We didn't get a delta or a delta_revoked - end of
                            // the delta availability chain
                            if (delta == null)
                                break;
                        }

                        Logger.d("delta --> [%s]", delta.getOut().getName());
                        fetch = String.format(Locale.ENGLISH, "%s%s.delta", config.getUrlBaseDelta(),
                                delta.getOut().getName().replace(".zip", ""));
                        deltas.add(delta);
                    }

                    if (deltas.size() > 0) {
                        // See if we have done past work and have newer ZIPs
                        // than the original of what's currently flashed

                        int last = -1;
                        for (int i = deltas.size() - 1; i >= 0; i--) {
                            DeltaInfo di = deltas.get(i);
                            String fn = config.getPathBase() + di.getOut().getName();
                            if (di.getOut().match(new File(fn), true,
                                    getMD5Progress(STATE_ACTION_CHECKING_MD5, di.getOut().getName())) != null) {
                                Logger.d("match found: %s", di.getOut().getName());
                                flashFilename = fn;
                                last = i;
                                break;
                            }
                        }

                        if (last > -1) {
                            for (int i = 0; i <= last; i++) {
                                deltas.remove(0);
                            }
                        }
                    }

                    while ((deltas.size() > 0) && (deltas.get(deltas.size() - 1).isRevoked())) {
                        // Make sure the last delta is not revoked
                        deltas.remove(deltas.size() - 1);
                    }

                    if (deltas.size() > 0) {
                        DeltaInfo lastDelta = deltas.get(deltas.size() - 1);

                        long deltaDownloadSize = getDeltaDownloadSize(deltas);
                        long fullDownloadSize = getFullDownloadSize(deltas);

                        Logger.d("download size --> deltas[%d] vs full[%d]", deltaDownloadSize, fullDownloadSize);

                        // Find the currently flashed ZIP, or a newer one
                        String initialFile = null;
                        boolean initialFileNeedsProcessing = false;
                        {
                            boolean[] needsProcessing = new boolean[] { false };
                            initialFile = findInitialFile(deltas, flashFilename, needsProcessing);
                            initialFileNeedsProcessing = needsProcessing[0];
                        }
                        flashFilename = null;

                        Logger.d("initial: %s", initialFile != null ? initialFile : "not found");

                        // If we don't have a file to start out with, or the
                        // combined deltas get big, just get the latest full ZIP
                        boolean getFull = ((initialFile == null) || (deltaDownloadSize > fullDownloadSize));

                        long requiredSpace = getRequiredSpace(deltas, getFull);
                        long freeSpace = (new StatFs(config.getPathBase())).getAvailableBytes();
                        if (freeSpace < requiredSpace) {
                            updateState(STATE_ERROR_DISK_SPACE, null, freeSpace, requiredSpace, null, null);
                            Logger.d("not enough space!");
                            return;
                        }

                        long downloadSize = getFull ? fullDownloadSize : deltaDownloadSize;

                        // Download all the files we do not have yet
                        if (!downloadFiles(deltas, getFull, downloadSize, force))
                            return;

                        // Reconstruct flashable ZIP
                        if (!getFull && !applyPatches(deltas, initialFile, initialFileNeedsProcessing))
                            return;

                        // Verify using MD5
                        if (lastDelta.getOut().match(new File(config.getPathBase() + lastDelta.getOut().getName()),
                                true,
                                getMD5Progress(STATE_ACTION_APPLYING_MD5, lastDelta.getOut().getName())) == null) {
                            updateState(STATE_ERROR_UNKNOWN, null, null, null, null, null);
                            Logger.d("final verification error");
                            return;
                        }
                        Logger.d("final verification complete");

                        // Cleanup
                        for (DeltaInfo di : deltas) {
                            (new File(config.getPathBase() + di.getUpdate().getName())).delete();
                            (new File(config.getPathBase() + di.getSignature().getName())).delete();
                            if (di != lastDelta)
                                (new File(config.getPathBase() + di.getOut().getName())).delete();
                        }
                        if (initialFile != null) {
                            if (initialFile.startsWith(config.getPathBase()))
                                (new File(initialFile)).delete();
                        }

                        flashFilename = config.getPathBase() + lastDelta.getOut().getName();
                    }

                    if (flashFilename != null) {
                        prefs.edit().putString(PREF_READY_FILENAME_NAME, flashFilename).commit();
                    }

                    prefs.edit().putLong(PREF_LAST_CHECK_TIME_NAME, System.currentTimeMillis()).commit();
                } finally {
                    stopForeground(true);
                    if (wifiLock.isHeld())
                        wifiLock.release();
                    if (wakeLock.isHeld())
                        wakeLock.release();
                    autoState();
                }
            }
        });
    }
}