com.darshancomputing.BatteryIndicatorPro.BatteryInfoService.java Source code

Java tutorial

Introduction

Here is the source code for com.darshancomputing.BatteryIndicatorPro.BatteryInfoService.java

Source

/*
Copyright (c) 2009-2016 Darshan-Josiah Barber
    
This program 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.
    
This program 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.
*/

package com.darshancomputing.BatteryIndicatorPro;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;

import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;

import java.util.Date;
import java.util.HashSet;

public class BatteryInfoService extends Service {
    private final IntentFilter batteryChanged = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    private final IntentFilter userPresent = new IntentFilter(Intent.ACTION_USER_PRESENT);
    private PendingIntent currentInfoPendingIntent, updatePredictorPendingIntent, alarmsPendingIntent;

    private NotificationManagerCompat mNotificationManager;
    private AlarmManager alarmManager;
    private SharedPreferences settings;
    private SharedPreferences sp_service;
    private SharedPreferences.Editor sps_editor;

    private Resources res;
    private Str str;
    private AlarmDatabase alarms;
    private LogDatabase log_db;
    private BatteryLevel bl;
    private CurrentHack currentHack;
    private CircleWidgetBackground cwbg;
    private BatteryInfo info;
    private long now;
    private boolean updated_lasts;
    private static java.util.HashSet<Messenger> clientMessengers;
    private static Messenger messenger;

    private static HashSet<Integer> widgetIds = new HashSet<Integer>();
    private static AppWidgetManager widgetManager;

    private static final String LOG_TAG = "com.darshancomputing.BatteryIndicatorPro - BatteryInfoService";

    private static final int NOTIFICATION_PRIMARY = 1;
    private static final int NOTIFICATION_ALARM_CHARGE = 3;
    private static final int NOTIFICATION_ALARM_HEALTH = 4;
    private static final int NOTIFICATION_ALARM_TEMP = 5;

    private static final int RC_MAIN = 100;
    private static final int RC_ALARMS = 101;

    public static final String KEY_PREVIOUS_CHARGE = "previous_charge";
    public static final String KEY_PREVIOUS_TEMP = "previous_temp";
    public static final String KEY_PREVIOUS_HEALTH = "previous_health";
    public static final String KEY_SERVICE_DESIRED = "serviceDesired";
    public static final String KEY_SHOW_NOTIFICATION = "show_notification";
    public static final String LAST_SDK_API = "last_sdk_api";

    private static final String EXTRA_UPDATE_PREDICTOR = "com.darshancomputing.BatteryBotPro.EXTRA_UPDATE_PREDICTOR";

    public static final String EXTRA_CURRENT_INFO = "com.darshancomputing.BatteryBotPro.EXTRA_CURRENT_INFO";
    public static final String EXTRA_EDIT_ALARMS = "com.darshancomputing.BatteryBotPro.EXTRA_EDIT_ALARMS";

    private static final Object[] EMPTY_OBJECT_ARRAY = {};
    private static final Class[] EMPTY_CLASS_ARRAY = {};

    private static final int plainIcon0 = R.drawable.plain000;
    private static final int small_plainIcon0 = R.drawable.small_plain000;
    private static final int chargingIcon0 = R.drawable.charging000;
    private static final int small_chargingIcon0 = R.drawable.small_charging000;

    /* Global variables for these Notification Runnables */
    private NotificationCompat.Builder mainNotificationB;
    private String mainNotificationTopLine, mainNotificationBottomLine;
    private RemoteViews notificationRV;

    private Predictor predictor;

    // Workaround for NotificationCompat.Builder losing custom content view on pre-Honeycomb
    // See https://code.google.com/p/android/issues/detail?id=30495
    private boolean needSetContentAgain = false;

    private final Handler mHandler = new Handler();

    private final Runnable mNotify = new Runnable() {
        public void run() {
            android.app.Notification n = mainNotificationB.build();

            if (needSetContentAgain)
                n.contentView = notificationRV;

            startForeground(NOTIFICATION_PRIMARY, n);
            mHandler.removeCallbacks(mNotify);
        }
    };

    private final Runnable runRenotify = new Runnable() {
        public void run() {
            registerReceiver(mBatteryInfoReceiver, batteryChanged);
        }
    };

    @Override
    public void onCreate() {
        res = getResources();
        str = new Str(res);
        log_db = new LogDatabase(this);

        info = new BatteryInfo();

        messenger = new Messenger(new MessageHandler());
        clientMessengers = new java.util.HashSet<Messenger>();

        predictor = new Predictor(this);
        bl = BatteryLevel.getInstance(this, BatteryLevel.SIZE_NOTIFICATION);
        cwbg = new CircleWidgetBackground(this);

        alarms = new AlarmDatabase(this);

        mNotificationManager = NotificationManagerCompat.from(this);
        mainNotificationB = new NotificationCompat.Builder(this);

        alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        loadSettingsFiles();
        sdkVersioning();

        currentHack = CurrentHack.getInstance(this);
        currentHack.setPreferFS(settings.getBoolean(SettingsActivity.KEY_CURRENT_HACK_PREFER_FS,
                res.getBoolean(R.bool.default_prefer_fs_current_hack)));

        Intent currentInfoIntent = new Intent(this, BatteryInfoActivity.class).putExtra(EXTRA_CURRENT_INFO, true);
        currentInfoPendingIntent = PendingIntent.getActivity(this, RC_MAIN, currentInfoIntent, 0);

        Intent updatePredictorIntent = new Intent(this, BatteryInfoService.class);
        updatePredictorIntent.putExtra(EXTRA_UPDATE_PREDICTOR, true);
        updatePredictorPendingIntent = PendingIntent.getService(this, 0, updatePredictorIntent, 0);

        Intent alarmsIntent = new Intent(this, BatteryInfoActivity.class).putExtra(EXTRA_EDIT_ALARMS, true);
        alarmsPendingIntent = PendingIntent.getActivity(this, RC_ALARMS, alarmsIntent, 0);

        widgetManager = AppWidgetManager.getInstance(this);

        Class[] appWidgetProviders = { BatteryInfoAppWidgetProvider.class, /* Circle widget! */
                FullAppWidgetProvider.class };

        for (int i = 0; i < appWidgetProviders.length; i++) {
            int[] ids = widgetManager.getAppWidgetIds(new ComponentName(this, appWidgetProviders[i]));

            for (int j = 0; j < ids.length; j++) {
                widgetIds.add(ids[j]);
            }
        }

        Intent bc_intent = registerReceiver(mBatteryInfoReceiver, batteryChanged);
        info.load(bc_intent, sp_service);
    }

    @Override
    public void onDestroy() {
        alarmManager.cancel(updatePredictorPendingIntent);
        alarms.close();
        unregisterReceiver(mBatteryInfoReceiver);
        mHandler.removeCallbacks(mNotify);
        mHandler.removeCallbacks(runRenotify);
        mNotificationManager.cancelAll();
        log_db.close();
        updateWidgets(null);
        stopForeground(true);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // TODO: Do I need a filter, or is it okay to just update(null) every time?
        //if (intent != null && intent.getBooleanExtra(EXTRA_UPDATE_PREDICTOR, false))
        update(null);

        return Service.START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return messenger.getBinder();
    }

    public class MessageHandler extends Handler {
        @Override
        public void handleMessage(Message incoming) {
            switch (incoming.what) {
            case RemoteConnection.SERVICE_CLIENT_CONNECTED:
                sendClientMessage(incoming.replyTo, RemoteConnection.CLIENT_SERVICE_CONNECTED);
                break;
            case RemoteConnection.SERVICE_REGISTER_CLIENT:
                clientMessengers.add(incoming.replyTo);
                sendClientMessage(incoming.replyTo, RemoteConnection.CLIENT_BATTERY_INFO_UPDATED, info.toBundle());
                break;
            case RemoteConnection.SERVICE_UNREGISTER_CLIENT:
                clientMessengers.remove(incoming.replyTo);
                break;
            case RemoteConnection.SERVICE_RELOAD_SETTINGS:
                reloadSettings(false);
                break;
            case RemoteConnection.SERVICE_CANCEL_NOTIFICATION_AND_RELOAD_SETTINGS:
                reloadSettings(true);
                break;
            case RemoteConnection.SERVICE_WIZARD_VALUE_DEFAULT:
                wizardValueChanged(NotificationWizard.VALUE_DEFAULT);
                break;
            case RemoteConnection.SERVICE_WIZARD_VALUE_MINIMAL:
                wizardValueChanged(NotificationWizard.VALUE_MINIMAL);
                break;
            case RemoteConnection.SERVICE_WIZARD_VALUE_NONE:
                wizardValueChanged(NotificationWizard.VALUE_NONE);
                break;
            default:
                super.handleMessage(incoming);
            }
        }
    }

    private static void sendClientMessage(Messenger clientMessenger, int what) {
        sendClientMessage(clientMessenger, what, null);
    }

    private static void sendClientMessage(Messenger clientMessenger, int what, Bundle data) {
        Message outgoing = Message.obtain();
        outgoing.what = what;
        outgoing.replyTo = messenger;
        outgoing.setData(data);
        try {
            clientMessenger.send(outgoing);
        } catch (android.os.RemoteException e) {
        }
    }

    public static class RemoteConnection implements ServiceConnection {
        // Messages clients send to the service
        public static final int SERVICE_CLIENT_CONNECTED = 0;
        public static final int SERVICE_REGISTER_CLIENT = 1;
        public static final int SERVICE_UNREGISTER_CLIENT = 2;
        public static final int SERVICE_RELOAD_SETTINGS = 3;
        public static final int SERVICE_CANCEL_NOTIFICATION_AND_RELOAD_SETTINGS = 4;
        public static final int SERVICE_WIZARD_VALUE_DEFAULT = 5;
        public static final int SERVICE_WIZARD_VALUE_MINIMAL = 6;
        public static final int SERVICE_WIZARD_VALUE_NONE = 7;

        // Messages the service sends to clients
        public static final int CLIENT_SERVICE_CONNECTED = 0;
        public static final int CLIENT_BATTERY_INFO_UPDATED = 1;

        public Messenger serviceMessenger;
        private Messenger clientMessenger;

        public RemoteConnection(Messenger m) {
            clientMessenger = m;
        }

        public void onServiceConnected(ComponentName name, IBinder iBinder) {
            serviceMessenger = new Messenger(iBinder);

            Message outgoing = Message.obtain();
            outgoing.what = SERVICE_CLIENT_CONNECTED;
            outgoing.replyTo = clientMessenger;
            try {
                serviceMessenger.send(outgoing);
            } catch (android.os.RemoteException e) {
            }
        }

        public void onServiceDisconnected(ComponentName name) {
            serviceMessenger = null;
        }
    }

    private void loadSettingsFiles() {
        settings = getSharedPreferences(SettingsActivity.SETTINGS_FILE, Context.MODE_MULTI_PROCESS);
        sp_service = getSharedPreferences(SettingsActivity.SP_SERVICE_FILE, Context.MODE_MULTI_PROCESS);
    }

    private void reloadSettings(boolean cancelFirst) {
        loadSettingsFiles();
        currentHack.setPreferFS(settings.getBoolean(SettingsActivity.KEY_CURRENT_HACK_PREFER_FS,
                res.getBoolean(R.bool.default_prefer_fs_current_hack)));

        str = new Str(res); // Language override may have changed

        applyNewSettings(cancelFirst);
    }

    private void applyNewSettings(boolean cancelFirst) {
        if (cancelFirst) {
            stopForeground(true);
            mainNotificationB = new NotificationCompat.Builder(this);
        }

        registerReceiver(mBatteryInfoReceiver, batteryChanged);
    }

    private void wizardValueChanged(int value) {
        SharedPreferences.Editor sps_editor = sp_service.edit();
        SharedPreferences.Editor settings_editor = settings.edit();

        // Writing to settings here should only happen when Wizard open, so shouldn't have conflict
        switch (value) {
        case NotificationWizard.VALUE_NONE:
            sps_editor.putBoolean(BatteryInfoService.KEY_SHOW_NOTIFICATION, false);

            break;
        case NotificationWizard.VALUE_MINIMAL:
            sps_editor.putBoolean(BatteryInfoService.KEY_SHOW_NOTIFICATION, true);
            settings_editor.putString(SettingsActivity.KEY_MAIN_NOTIFICATION_PRIORITY,
                    "" + NotificationCompat.PRIORITY_MIN);

            break;
        default:
            sps_editor.putBoolean(BatteryInfoService.KEY_SHOW_NOTIFICATION, true);
            int priority = Integer.valueOf(settings.getString(SettingsActivity.KEY_MAIN_NOTIFICATION_PRIORITY,
                    str.default_main_notification_priority));
            if (priority == NotificationCompat.PRIORITY_MIN)
                settings_editor.putString(SettingsActivity.KEY_MAIN_NOTIFICATION_PRIORITY,
                        "" + NotificationCompat.PRIORITY_LOW);
        }

        Str.apply(sps_editor);
        Str.apply(settings_editor);

        applyNewSettings(true);
    }

    private final BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context c, Intent intent) {
            if (!Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction()))
                return;

            update(intent);
        }
    };

    // Does anything needed when SDK API level increases and sets LAST_SDK_API
    private void sdkVersioning() {
        SharedPreferences.Editor sps_editor = sp_service.edit();
        SharedPreferences.Editor settings_editor = settings.edit();

        // Writing to settings here should only happen when Service first started, so shouldn't have conflict
        if (sp_service.getInt(LAST_SDK_API, 0) < 21 && android.os.Build.VERSION.SDK_INT >= 21) {
            settings_editor.putBoolean(SettingsActivity.KEY_USE_SYSTEM_NOTIFICATION_LAYOUT, true);
        }

        sps_editor.putInt(LAST_SDK_API, android.os.Build.VERSION.SDK_INT);

        Str.apply(sps_editor);
        Str.apply(settings_editor);
    }

    private void update(Intent intent) {
        now = System.currentTimeMillis();
        sps_editor = sp_service.edit();
        updated_lasts = false;

        if (intent != null)
            info.load(intent, sp_service);

        predictor.setPredictionType(
                settings.getString(SettingsActivity.KEY_PREDICTION_TYPE, str.default_prediction_type));
        predictor.update(info);
        info.prediction.updateRelativeTime();

        if (statusHasChanged())
            handleUpdateWithChangedStatus();
        else
            handleUpdateWithSameStatus();

        if (sp_service.getBoolean(KEY_SHOW_NOTIFICATION, true)) {
            prepareNotification();
            doNotify();
        }

        if (alarms.anyActiveAlarms())
            handleAlarms();

        updateWidgets(info);

        syncSpsEditor(); // Important to sync after other Service code that uses 'lasts' but before sending info to client

        for (Messenger messenger : clientMessengers) {
            // TODO: Can I send the same message to multiple clients instead of sending duplicates?
            sendClientMessage(messenger, RemoteConnection.CLIENT_BATTERY_INFO_UPDATED, info.toBundle());
        }

        alarmManager.set(AlarmManager.ELAPSED_REALTIME, android.os.SystemClock.elapsedRealtime() + (2 * 60 * 1000),
                updatePredictorPendingIntent);
    }

    private void updateWidgets(BatteryInfo info) {
        //Intent mainWindowIntent = new Intent(this, BatteryInfoActivity.class);
        //PendingIntent mainWindowPendingIntent = PendingIntent.getActivity(this, RC_MAIN, mainWindowIntent, 0);
        //PendingIntent currentInfoPendingIntent = PendingIntent.getActivity(this, RC_MAIN, currentInfoIntent, 0);

        if (info == null) {
            cwbg.setLevel(0);
        } else {
            bl.setLevel(info.percent);
            cwbg.setLevel(info.percent);
        }

        for (Integer widgetId : widgetIds) {
            RemoteViews rv;

            android.appwidget.AppWidgetProviderInfo awpInfo = widgetManager.getAppWidgetInfo(widgetId);
            if (awpInfo == null)
                continue; // Based on Developer Console crash reports, this can be null sometimes

            int initLayout = awpInfo.initialLayout;

            if (initLayout == R.layout.circle_app_widget) {
                rv = new RemoteViews(getPackageName(), R.layout.circle_app_widget);
                rv.setImageViewBitmap(R.id.circle_widget_image_view, cwbg.getBitmap());
            } else {
                rv = new RemoteViews(getPackageName(), R.layout.full_app_widget);

                if (info == null) {
                    rv.setImageViewBitmap(R.id.battery_level_view, cwbg.getBitmap());
                    rv.setTextViewText(R.id.fully_charged, "");
                    rv.setTextViewText(R.id.time_remaining, "");
                    rv.setTextViewText(R.id.until_what, "");
                } else {
                    rv.setImageViewBitmap(R.id.battery_level_view, bl.getBitmap());

                    if (info.prediction.what == BatteryInfo.Prediction.NONE) {
                        rv.setTextViewText(R.id.fully_charged, str.timeRemaining(info));
                        rv.setTextViewText(R.id.time_remaining, "");
                        rv.setTextViewText(R.id.until_what, "");
                    } else {
                        rv.setTextViewText(R.id.fully_charged, "");
                        rv.setTextViewText(R.id.time_remaining, str.timeRemaining(info));
                        rv.setTextViewText(R.id.until_what, str.untilWhat(info));
                    }
                }
            }

            if (info == null)
                rv.setTextViewText(R.id.level, "XX" + str.percent_symbol);
            else
                rv.setTextViewText(R.id.level, "" + info.percent + str.percent_symbol);

            rv.setOnClickPendingIntent(R.id.widget_layout, currentInfoPendingIntent);
            widgetManager.updateAppWidget(widgetId, rv);
        }
    }

    private void syncSpsEditor() {
        Str.apply(sps_editor);

        if (updated_lasts) {
            info.last_status_cTM = now;
            info.last_status = info.status;
            info.last_percent = info.percent;
            info.last_plugged = info.plugged;
        }
    }

    private void prepareNotification() {
        mainNotificationTopLine = lineFor(SettingsActivity.KEY_TOP_LINE);
        mainNotificationBottomLine = lineFor(SettingsActivity.KEY_BOTTOM_LINE);

        mainNotificationB.setSmallIcon(iconFor(info.percent)).setOngoing(true).setWhen(0).setShowWhen(false)
                .setContentIntent(currentInfoPendingIntent)
                .setPriority(Integer.valueOf(settings.getString(SettingsActivity.KEY_MAIN_NOTIFICATION_PRIORITY,
                        str.default_main_notification_priority)))
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

        if (settings.getBoolean(SettingsActivity.KEY_USE_SYSTEM_NOTIFICATION_LAYOUT,
                res.getBoolean(R.bool.default_use_system_notification_layout))) {
            mainNotificationB.setContentTitle(mainNotificationTopLine).setContentText(mainNotificationBottomLine);

            needSetContentAgain = false;
        } else {
            String icon_area = settings.getString(SettingsActivity.KEY_ICON_AREA,
                    res.getString(R.string.default_icon_area_content));

            int layout_id = R.layout.main_notification;
            if (icon_area.equals("percentage_first"))
                layout_id = R.layout.main_notification_percentage_first;

            notificationRV = new RemoteViews(getPackageName(), layout_id);

            if (icon_area.equals("percentage")) {
                notificationRV.setViewVisibility(R.id.percent, View.VISIBLE);
                notificationRV.setViewVisibility(R.id.battery, View.GONE);
            } else if (icon_area.equals("graphic")) {
                notificationRV.setViewVisibility(R.id.percent, View.GONE);
                notificationRV.setViewVisibility(R.id.battery, View.VISIBLE);
            }

            notificationRV.setImageViewBitmap(R.id.battery, bl.getBitmap());
            bl.setLevel(info.percent);

            notificationRV.setTextViewText(R.id.percent, "" + info.percent + str.percent_symbol);
            notificationRV.setTextViewText(R.id.top_line, android.text.Html.fromHtml(mainNotificationTopLine));
            notificationRV.setTextViewText(R.id.bottom_line, mainNotificationBottomLine);

            int color;
            color = colorFor(SettingsActivity.KEY_NOTIFICATION_PERCENTAGE_TEXT_COLOR,
                    SettingsActivity.KEY_CUSTOM_PERCENTAGE_TEXT_COLOR);
            if (color != 0)
                notificationRV.setTextColor(R.id.percent, color);
            color = colorFor(SettingsActivity.KEY_NOTIFICATION_TOP_LINE_COLOR,
                    SettingsActivity.KEY_CUSTOM_TOP_LINE_COLOR);
            if (color != 0)
                notificationRV.setTextColor(R.id.top_line, color);
            color = colorFor(SettingsActivity.KEY_NOTIFICATION_BOTTOM_LINE_COLOR,
                    SettingsActivity.KEY_CUSTOM_BOTTOM_LINE_COLOR);
            if (color != 0)
                notificationRV.setTextColor(R.id.bottom_line, color);

            boolean default_show_box = res.getBoolean(R.bool.default_show_box_around_icon_area);
            boolean show_box = settings.getBoolean(SettingsActivity.KEY_SHOW_BOX_AROUND_ICON_AREA,
                    default_show_box);

            if (show_box) {
                color = res.getColor(R.color.notification_box_default_color);
                if (!icon_area.equals("battery_first"))
                    notificationRV.setInt(R.id.percent, "setBackgroundColor", color);
                if (!icon_area.equals("percentage_first"))
                    notificationRV.setInt(R.id.battery, "setBackgroundColor", color);
            }

            if (android.os.Build.VERSION.SDK_INT < 11)
                needSetContentAgain = true;

            mainNotificationB.setContent(notificationRV);
        }
    }

    // Since alpha values aren't permitted, return 0 for default
    private int colorFor(String colorKey, String customKey) {
        String colorString = settings.getString(colorKey, "default");

        if (colorString.charAt(0) == '#')
            return colorFromHex(colorString);
        else if (colorString.equals("custom"))
            return settings.getInt(customKey, R.color.main_notification_default_custom_text_color);
        else
            return 0;
    }

    private static int colorFromHex(String hex) {
        if (hex.length() != 7)
            return 0;
        if (hex.charAt(0) != '#')
            return 0;

        int color = 0xff;

        for (int i = 1; i <= 6; i++) {
            color <<= 4;
            char c = hex.charAt(i);

            if (c >= '0' && c <= '9')
                color += c - '0';
            else if (c >= 'A' && c <= 'F')
                color += c - 'A' + 10;
            else if (c >= 'a' && c <= 'f')
                color += c - 'a' + 10;
        }

        return color;
    }

    private String lineFor(String key) {
        String req = settings.getString(key, key.equals(SettingsActivity.KEY_TOP_LINE) ? "remaining" : "vitals");

        if (req.equals("remaining"))
            return predictionLine();
        else if (req.equals("vitals"))
            return vitalStatsLine();
        else
            return statusDurationLine();
    }

    private String predictionLine() {
        String line;
        BatteryInfo.RelativeTime predicted = info.prediction.last_rtime;

        if (info.prediction.what == BatteryInfo.Prediction.NONE) {
            line = str.statuses[info.status];
        } else {
            if (predicted.days > 0)
                line = str.n_days_m_hours(predicted.days, predicted.hours);
            else if (predicted.hours > 0) {
                String verbosity = settings.getString(SettingsActivity.KEY_TIME_REMAINING_VERBOSITY,
                        res.getString(R.string.default_time_remaining_verbosity));
                if (verbosity.equals("condensed"))
                    line = str.n_hours_m_minutes_medium(predicted.hours, predicted.minutes);
                else if (verbosity.equals("verbose"))
                    line = str.n_hours_m_minutes_long(predicted.hours, predicted.minutes);
                else
                    line = str.n_hours_long_m_minutes_medium(predicted.hours, predicted.minutes);
            } else
                line = str.n_minutes_long(predicted.minutes);

            if (info.prediction.what == BatteryInfo.Prediction.UNTIL_CHARGED)
                line += res.getString(R.string.notification_until_charged);
            else
                line += res.getString(R.string.notification_until_drained);
        }

        return line;
    }

    private String vitalStatsLine() {
        Boolean convertF = settings.getBoolean(SettingsActivity.KEY_CONVERT_F,
                res.getBoolean(R.bool.default_convert_to_fahrenheit));

        String line = str.healths[info.health] + " / " + str.formatTemp(info.temperature, convertF);

        if (info.voltage > 500)
            line += " / " + str.formatVoltage(info.voltage);
        if (settings.getBoolean(SettingsActivity.KEY_ENABLE_CURRENT_HACK, false)
                && settings.getBoolean(SettingsActivity.KEY_DISPLAY_CURRENT_IN_VITAL_STATS, false)) {
            Long current = null;
            if (settings.getBoolean(SettingsActivity.KEY_PREFER_CURRENT_AVG_IN_VITAL_STATS, false))
                current = currentHack.getAvgCurrent();
            if (current == null) // Either don't prefer avg or avg isn't available
                current = currentHack.getCurrent();
            if (current != null)
                line += " / " + String.valueOf(current) + "mA";
        }
        if (settings.getBoolean(SettingsActivity.KEY_STATUS_DURATION_IN_VITAL_SIGNS, false)) {
            float statusDurationHours = (now - info.last_status_cTM) / (60 * 60 * 1000f);
            line += " / " + String.format("%.1f", statusDurationHours) + "h"; // TODO: Translatable 'h'
        }

        return line;
    }

    private String statusDurationLine() {
        long statusDuration = now - info.last_status_cTM;
        int statusDurationHours = (int) ((statusDuration + (1000 * 60 * 30)) / (1000 * 60 * 60));
        String line = str.statuses[info.status] + " ";

        if (statusDuration < 1000 * 60 * 60)
            line += str.since + " " + formatTime(new Date(info.last_status_cTM));
        else
            line += str.for_n_hours(statusDurationHours);

        return line;
    }

    private void doNotify() {
        mHandler.post(mNotify);
    }

    // I take advantage of (count on) R.java having resources alphabetical and incrementing by one.
    private int iconFor(int percent) {
        String default_set = "builtin.classic";
        if (android.os.Build.VERSION.SDK_INT >= 11)
            default_set = "builtin.plain_number";

        String icon_set = settings.getString(SettingsActivity.KEY_ICON_SET, "null");
        if (!icon_set.startsWith("builtin."))
            icon_set = "null"; // TODO: Remove this line to re-enable plugins

        if (icon_set.equals("null")) {
            icon_set = default_set;

            // Writing to settings here should only happen when Service first started, so shouldn't have conflict
            Str.apply(settings.edit().putString(SettingsActivity.KEY_ICON_SET, default_set));
        }

        Boolean indicate_charging = settings.getBoolean(SettingsActivity.KEY_INDICATE_CHARGING, true);

        if (icon_set.equals("builtin.plain_number")) {
            return ((info.status == BatteryInfo.STATUS_CHARGING && indicate_charging) ? chargingIcon0 : plainIcon0)
                    + info.percent;
        } else if (icon_set.equals("builtin.smaller_number")) {
            return ((info.status == BatteryInfo.STATUS_CHARGING && indicate_charging) ? small_chargingIcon0
                    : small_plainIcon0) + info.percent;
        } else if (android.os.Build.VERSION.SDK_INT >= 21
                && !settings.getBoolean(SettingsActivity.KEY_CLASSIC_COLOR_MODE, false)) {
            // Classic set is desired, but colors break notification icons on API level 21+
            return R.drawable.w000 + info.percent;
        } else {
            if (settings.getBoolean(SettingsActivity.KEY_RED, res.getBoolean(R.bool.default_use_red))
                    && info.percent < Integer
                            .valueOf(settings.getString(SettingsActivity.KEY_RED_THRESH, str.default_red_thresh))
                    && info.percent <= SettingsActivity.RED_ICON_MAX) {
                return R.drawable.r000 + info.percent - 0;
            } else if (settings.getBoolean(SettingsActivity.KEY_AMBER, res.getBoolean(R.bool.default_use_amber))
                    && info.percent < Integer.valueOf(
                            settings.getString(SettingsActivity.KEY_AMBER_THRESH, str.default_amber_thresh))
                    && info.percent <= SettingsActivity.AMBER_ICON_MAX
                    && info.percent >= SettingsActivity.AMBER_ICON_MIN) {
                return R.drawable.a000 + info.percent - 0;
            } else if (settings.getBoolean(SettingsActivity.KEY_GREEN, res.getBoolean(R.bool.default_use_green))
                    && info.percent >= Integer.valueOf(
                            settings.getString(SettingsActivity.KEY_GREEN_THRESH, str.default_green_thresh))
                    && info.percent >= SettingsActivity.GREEN_ICON_MIN) {
                return R.drawable.g020 + info.percent - 20;
            } else {
                return R.drawable.b000 + info.percent;
            }
        }
    }

    private boolean statusHasChanged() {
        int previous_charge = sp_service.getInt(KEY_PREVIOUS_CHARGE, 100);

        return (info.last_status != info.status || info.last_status_cTM >= now || info.last_plugged != info.plugged
                || (info.plugged == BatteryInfo.PLUGGED_UNPLUGGED && info.percent > previous_charge + 20));
    }

    private void handleUpdateWithChangedStatus() {
        if (settings.getBoolean(SettingsActivity.KEY_ENABLE_LOGGING, true)) {
            log_db.logStatus(info, now, LogDatabase.STATUS_NEW);

            if (info.status != info.last_status && info.last_status == BatteryInfo.STATUS_UNPLUGGED)
                log_db.prune(Integer
                        .valueOf(settings.getString(SettingsActivity.KEY_MAX_LOG_AGE, str.default_max_log_age)));
        }

        if (settings.getBoolean(SettingsActivity.KEY_ENABLE_CURRENT_HACK, false)
                && settings.getBoolean(SettingsActivity.KEY_DISPLAY_CURRENT_IN_VITAL_STATS, false)) {
            mHandler.postDelayed(runRenotify, 1000);
            mHandler.postDelayed(runRenotify, 3000);
            mHandler.postDelayed(runRenotify, 9000);
            mHandler.postDelayed(runRenotify, 27000);
        }

        /* TODO: Af first glance, I think I want to do this, but think about it a bit and decide for sure... */
        if (info.status != info.last_status && info.status == BatteryInfo.STATUS_UNPLUGGED)
            mNotificationManager.cancel(NOTIFICATION_ALARM_CHARGE);

        updated_lasts = true;
        sps_editor.putLong(BatteryInfo.KEY_LAST_STATUS_CTM, now);
        sps_editor.putInt(BatteryInfo.KEY_LAST_STATUS, info.status);
        sps_editor.putInt(BatteryInfo.KEY_LAST_PERCENT, info.percent);
        sps_editor.putInt(BatteryInfo.KEY_LAST_PLUGGED, info.plugged);
        sps_editor.putInt(KEY_PREVIOUS_CHARGE, info.percent);
        sps_editor.putInt(KEY_PREVIOUS_TEMP, info.temperature);
        sps_editor.putInt(KEY_PREVIOUS_HEALTH, info.health);
    }

    private void handleUpdateWithSameStatus() {
        if (settings.getBoolean(SettingsActivity.KEY_ENABLE_LOGGING, true))
            log_db.logStatus(info, now, LogDatabase.STATUS_OLD);

        if (info.percent % 10 == 0) {
            sps_editor.putInt(KEY_PREVIOUS_CHARGE, info.percent);
            sps_editor.putInt(KEY_PREVIOUS_TEMP, info.temperature);
            sps_editor.putInt(KEY_PREVIOUS_HEALTH, info.health);
        }
    }

    private void handleAlarms() {
        Cursor c;
        NotificationCompat.Builder nb;

        int previous_charge = sp_service.getInt(KEY_PREVIOUS_CHARGE, 100);

        if (info.status == BatteryInfo.STATUS_FULLY_CHARGED && info.status != info.last_status) {
            c = alarms.activeAlarmFull();
            if (c != null) {
                nb = parseAlarmCursor(c);
                nb.setContentTitle(str.alarm_fully_charged).setContentText(str.alarm_text)
                        .setContentIntent(alarmsPendingIntent);

                if (android.os.Build.VERSION.SDK_INT >= 21)
                    nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

                mNotificationManager.notify(NOTIFICATION_ALARM_CHARGE, nb.build());
                c.close();
            }
        }

        c = alarms.activeAlarmChargeDrops(info.percent, previous_charge);
        if (c != null) {
            sps_editor.putInt(KEY_PREVIOUS_CHARGE, info.percent);
            nb = parseAlarmCursor(c);
            nb.setContentTitle(str.alarm_charge_drops + c.getInt(alarms.INDEX_THRESHOLD) + str.percent_symbol)
                    .setContentText(str.alarm_text).setContentIntent(alarmsPendingIntent);

            if (android.os.Build.VERSION.SDK_INT >= 21)
                nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

            mNotificationManager.notify(NOTIFICATION_ALARM_CHARGE, nb.build());
            c.close();
        }

        c = alarms.activeAlarmChargeRises(info.percent, previous_charge);
        if (c != null && info.status != BatteryInfo.STATUS_UNPLUGGED) {
            sps_editor.putInt(KEY_PREVIOUS_CHARGE, info.percent);
            nb = parseAlarmCursor(c);
            nb.setContentTitle(str.alarm_charge_rises + c.getInt(alarms.INDEX_THRESHOLD) + str.percent_symbol)
                    .setContentText(str.alarm_text).setContentIntent(alarmsPendingIntent);

            if (android.os.Build.VERSION.SDK_INT >= 21)
                nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

            mNotificationManager.notify(NOTIFICATION_ALARM_CHARGE, nb.build());
            c.close();
        }

        c = alarms.activeAlarmTempRises(info.temperature, sp_service.getInt(KEY_PREVIOUS_TEMP, 1));
        if (c != null) {
            Boolean convertF = settings.getBoolean(SettingsActivity.KEY_CONVERT_F,
                    res.getBoolean(R.bool.default_convert_to_fahrenheit));

            sps_editor.putInt(KEY_PREVIOUS_TEMP, info.temperature);
            nb = parseAlarmCursor(c);
            nb.setContentTitle(
                    str.alarm_temp_rises + str.formatTemp(c.getInt(alarms.INDEX_THRESHOLD), convertF, false))
                    .setContentText(str.alarm_text).setContentIntent(alarmsPendingIntent);

            if (android.os.Build.VERSION.SDK_INT >= 21)
                nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

            mNotificationManager.notify(NOTIFICATION_ALARM_CHARGE, nb.build());
            c.close();
        }

        if (info.health > BatteryInfo.HEALTH_GOOD
                && info.health != sp_service.getInt(KEY_PREVIOUS_HEALTH, BatteryInfo.HEALTH_GOOD)) {
            c = alarms.activeAlarmFailure();
            if (c != null) {
                sps_editor.putInt(KEY_PREVIOUS_HEALTH, info.health);
                nb = parseAlarmCursor(c);
                nb.setContentTitle(str.alarm_health_failure + str.healths[info.health])
                        .setContentText(str.alarm_text).setContentIntent(alarmsPendingIntent);

                if (android.os.Build.VERSION.SDK_INT >= 21)
                    nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

                mNotificationManager.notify(NOTIFICATION_ALARM_CHARGE, nb.build());
                c.close();
            }
        }
    }

    private NotificationCompat.Builder parseAlarmCursor(Cursor c) {
        NotificationCompat.Builder nb = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.stat_notify_alarm).setAutoCancel(true);

        String ringtone = c.getString(alarms.INDEX_RINGTONE);
        if (!ringtone.equals(""))
            nb.setSound(android.net.Uri.parse(ringtone));

        if (c.getInt(alarms.INDEX_VIBRATE) == 1)
            nb.setVibrate(new long[] { 0, 200, 200, 400 });

        if (c.getInt(alarms.INDEX_LIGHTS) == 1)
            nb.setDefaults(NotificationCompat.DEFAULT_LIGHTS);

        return nb;
    }

    private String formatTime(Date d) {
        String format = android.provider.Settings.System.getString(getContentResolver(),
                android.provider.Settings.System.TIME_12_24);
        if (format == null || format.equals("12")) {
            return java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT, java.util.Locale.getDefault())
                    .format(d);
        } else {
            return (new java.text.SimpleDateFormat("HH:mm")).format(d);
        }
    }

    public static void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        widgetManager = appWidgetManager;

        for (int i = 0; i < appWidgetIds.length; i++) {
            widgetIds.add(appWidgetIds[i]);
        }

        context.startService(new Intent(context, BatteryInfoService.class));
    }

    public static void onWidgetDeleted(Context context, int[] appWidgetIds) {
        for (int i = 0; i < appWidgetIds.length; i++) {
            widgetIds.remove(appWidgetIds[i]);
        }
    }
}