org.pixmob.freemobile.netstat.MonitorService.java Source code

Java tutorial

Introduction

Here is the source code for org.pixmob.freemobile.netstat.MonitorService.java

Source

/*
 * Copyright (C) 2012 Pixmob (http://github.com/pixmob)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.pixmob.freemobile.netstat;

import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.Process;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.telephony.CellIdentityGsm;
import android.telephony.CellIdentityWcdma;
import android.telephony.CellInfo;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoWcdma;
import android.telephony.CellLocation;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;

import org.pixmob.freemobile.netstat.content.NetstatContract.Events;
import org.pixmob.freemobile.netstat.util.IntentFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import static org.pixmob.freemobile.netstat.BuildConfig.DEBUG;
import static org.pixmob.freemobile.netstat.Constants.ACTION_NOTIFICATION;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_ENABLE_AUTO_RESTART_SERVICE;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_ENABLE_NOTIF_ACTIONS;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_STAT_NOTIF_SOUND_4G;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_STAT_NOTIF_SOUND_FEMTO;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_STAT_NOTIF_SOUND_FREE_MOBILE;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_STAT_NOTIF_SOUND_ORANGE;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_THEME;
import static org.pixmob.freemobile.netstat.Constants.SP_NAME;
import static org.pixmob.freemobile.netstat.Constants.TAG;
import static org.pixmob.freemobile.netstat.Constants.THEME_COLOR;
import static org.pixmob.freemobile.netstat.Constants.THEME_DEFAULT;
import static org.pixmob.freemobile.netstat.Constants.THEME_PIE;

/**
 * This foreground service is monitoring phone state and battery level. A notification shows which mobile network is the
 * phone is connected to.
 * 
 * @author Pixmob
 */
public class MonitorService extends Service implements OnSharedPreferenceChangeListener {
    /**
     * Notification themes.
     */
    private static final Map<String, Theme> THEMES = new HashMap<>(3);
    /**
     * Match network types from {@link TelephonyManager} with the corresponding string.
     */
    private static final SparseIntArray NETWORK_TYPE_STRINGS = new SparseIntArray(10);
    /**
     * Special data used for terminating the PendingInsert worker thread.
     */
    private static final Event STOP_PENDING_CONTENT_MARKER = new Event();
    /**
     * Femtocell's LAC start with this code.
     */
    private static final String FREE_MOBILE_FEMTOCELL_LAC_CODE = "98";
    /**
     * SDK Versions concerned with service auto-kill issue.
     */
    public static final String[] ANDROID_VERSIONS_ALLOWED_TO_AUTO_RESTART_SERVICE = new String[] { "4.4", "4.4.1",
            "4.4.2" };

    /**
     * Intent extra when requesting service restart after died
     */
    private static final String INTENT_ALARM_RESTART_SERVICE_DIED = "ALARM_RESTART_SERVICE_DIED";

    /**
     * Intent extra when requesting service restart after died
     */
    public static final String INTENT_UPDATE_NOTIF_ON_LOCKSCREEN = "UPDATE_NOTIF_ON_LOCKSCREEN";

    private static final List<Integer> FEMTOCELL_AVAILABLE_NETWORK_TYPE = new ArrayList<>();

    /**
     * This intent will open the main UI.
     */
    private PendingIntent openUIPendingIntent;
    private PendingIntent networkOperatorSettingsPendingIntent;
    private PendingIntent wirelessSettingsPendingIntent;
    private IntentFilter batteryIntentFilter;
    private PowerManager pm;
    private TelephonyManager tm;
    private ConnectivityManager cm;
    private BroadcastReceiver screenMonitor;
    private PhoneStateListener phoneMonitor;
    private BroadcastReceiver connectionMonitor;
    private BroadcastReceiver batteryMonitor;
    private BroadcastReceiver shutdownMonitor;
    private Boolean lastWifiConnected;
    private Boolean lastMobileNetworkConnected;
    private boolean powerOn = true;
    private boolean firstInsert = true;
    private String lastMobileOperatorId;
    private String mobileOperatorId;
    private boolean isFemtocell;
    private Boolean lastIsFemtocell;
    private boolean mobileNetworkConnected;
    private Integer lastMobileNetworkType;
    private Integer lastMobileNetworkTypeForLTEDetect;
    private int mobileNetworkType;
    private BlockingQueue<Event> pendingInsert;
    private SharedPreferences prefs;
    private Bitmap freeLargeIcon;
    private Bitmap freeFemtoLargeIcon;
    private Bitmap orangeLargeIcon;

    static {
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_EDGE, R.string.network_type_edge);
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_GPRS, R.string.network_type_gprs);
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_HSDPA, R.string.network_type_hsdpa);
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_HSPA, R.string.network_type_hspa);
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2) {
            NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_HSPAP, R.string.network_type_hspap);
        }
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_HSUPA, R.string.network_type_hsupa);
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_UMTS, R.string.network_type_umts);
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
            NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_LTE, R.string.network_type_lte);
        }
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_CDMA, R.string.network_type_cdma);
        NETWORK_TYPE_STRINGS.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, R.string.network_type_unknown);

        FEMTOCELL_AVAILABLE_NETWORK_TYPE.add(TelephonyManager.NETWORK_TYPE_HSDPA);
        FEMTOCELL_AVAILABLE_NETWORK_TYPE.add(TelephonyManager.NETWORK_TYPE_HSPA);
        FEMTOCELL_AVAILABLE_NETWORK_TYPE.add(TelephonyManager.NETWORK_TYPE_HSUPA);
        FEMTOCELL_AVAILABLE_NETWORK_TYPE.add(TelephonyManager.NETWORK_TYPE_UMTS);
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2) {
            FEMTOCELL_AVAILABLE_NETWORK_TYPE.add(TelephonyManager.NETWORK_TYPE_HSPAP);
        }

        THEMES.put(THEME_DEFAULT, new Theme(R.drawable.ic_stat_notify_service_free,
                R.drawable.ic_stat_notify_service_free_femto, R.drawable.ic_stat_notify_service_orange));
        THEMES.put(THEME_COLOR,
                new Theme(R.drawable.ic_stat_notify_service_free_color,
                        R.drawable.ic_stat_notify_service_free_femto_color,
                        R.drawable.ic_stat_notify_service_orange_color));
        THEMES.put(THEME_PIE, new Theme(R.drawable.ic_stat_notify_service_free_pie,
                R.drawable.ic_stat_notify_service_free_femto_pie, R.drawable.ic_stat_notify_service_orange_pie));
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (SP_KEY_THEME.equals(key) || SP_KEY_ENABLE_NOTIF_ACTIONS.equals(key)) {
            updateNotification(false, false);
        }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @Override
    public void onCreate() {
        super.onCreate();

        pm = (PowerManager) getSystemService(POWER_SERVICE);
        tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);

        prefs = getSharedPreferences(SP_NAME, MODE_PRIVATE);
        prefs.registerOnSharedPreferenceChangeListener(this);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            final int largeIconWidth = getResources()
                    .getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
            final int largeIconHeight = getResources()
                    .getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
            if ((largeIconWidth > 0) && (largeIconHeight > 0)) {
                Bitmap freeLargeIconTmp = BitmapFactory.decodeResource(getResources(),
                        R.drawable.ic_stat_notify_service_free_large);
                if ((freeLargeIconTmp != null) && (freeLargeIconTmp.getWidth() > 0)
                        && (freeLargeIconTmp.getHeight() > 0)) {
                    freeLargeIcon = Bitmap.createScaledBitmap(freeLargeIconTmp, largeIconWidth, largeIconHeight,
                            true);
                }

                Bitmap freeFemtoLargeIconTmp = BitmapFactory.decodeResource(getResources(),
                        R.drawable.ic_stat_notify_service_free_femto_large);
                if ((freeFemtoLargeIconTmp != null) && (freeFemtoLargeIconTmp.getHeight() > 0)
                        && (freeFemtoLargeIconTmp.getWidth() > 0)) {
                    freeFemtoLargeIcon = Bitmap.createScaledBitmap(freeFemtoLargeIconTmp, largeIconWidth,
                            largeIconHeight, true);
                }

                Bitmap orangeLargeIconTmp = BitmapFactory.decodeResource(getResources(),
                        R.drawable.ic_stat_notify_service_orange_large);
                if ((orangeLargeIconTmp != null) && (orangeLargeIconTmp.getHeight() > 0)
                        && (orangeLargeIconTmp.getWidth() > 0)) {
                    orangeLargeIcon = Bitmap.createScaledBitmap(orangeLargeIconTmp, largeIconWidth, largeIconHeight,
                            true);
                }
            }
        }

        // Initialize and start a worker thread for inserting rows into the
        // application database.
        final Context c = getApplicationContext();
        pendingInsert = new ArrayBlockingQueue<>(8);
        new PendingInsertWorker(c, pendingInsert).start();

        // This intent is fired when the application notification is clicked.
        openUIPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_NOTIFICATION),
                PendingIntent.FLAG_CANCEL_CURRENT);

        // This intent is only available as a Jelly Bean notification action in
        // order to open network operator settings.
        Intent networkSettingsIntent = IntentFactory.networkOperatorSettings(this);
        if (networkSettingsIntent != null) {
            networkOperatorSettingsPendingIntent = PendingIntent.getActivity(this, 0, networkSettingsIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT);
        }
        Intent wirelessSettingsIntent = IntentFactory.wirelessSettings(this);
        if (wirelessSettingsIntent != null) {
            wirelessSettingsPendingIntent = PendingIntent.getActivity(this, 0, wirelessSettingsIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT);
        }

        // Watch screen light: is the screen on?
        screenMonitor = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateEventDatabase();
            }
        };

        final IntentFilter screenIntentFilter = new IntentFilter();
        screenIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
        screenIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        registerReceiver(screenMonitor, screenIntentFilter);

        // Watch Wi-Fi connections.
        connectionMonitor = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (onConnectivityUpdated()) {
                    updateEventDatabase();
                }
            }
        };

        final IntentFilter connectionIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        registerReceiver(connectionMonitor, connectionIntentFilter);

        // Watch mobile connections.
        phoneMonitor = new PhoneStateListener() {
            @Override
            public void onDataConnectionStateChanged(int state, int networkType) {
                updateService();
            }

            @Override
            public void onServiceStateChanged(ServiceState serviceState) {
                if (stopServiceIfSimOperatorIsNotFreeMobile())
                    return;

                mobileNetworkConnected = (serviceState != null)
                        && (serviceState.getState() == ServiceState.STATE_IN_SERVICE);

                updateService();
            }

            @Override
            public void onCellInfoChanged(List<CellInfo> cellInfo) {
                updateService();
            }

            private void updateService() {
                if (tm != null) { // Fix NPE - found by Acralyzer
                    mobileNetworkType = tm.getNetworkType(); //update the network type to have the latest
                }
                final int phoneStateUpdated = onPhoneStateUpdated();
                if (phoneStateUpdated >= 0)
                    updateEventDatabase();

                updateNotification(true, phoneStateUpdated == 1);
            }
        };
        int events = PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
            events |= PhoneStateListener.LISTEN_CELL_INFO;

        tm.listen(phoneMonitor, events);

        // Watch battery level.
        batteryMonitor = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateEventDatabase();
            }
        };

        batteryIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        registerReceiver(batteryMonitor, batteryIntentFilter);

        shutdownMonitor = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                onDeviceShutdown();
            }
        };
        final IntentFilter shutdownIntentFilter = new IntentFilter();
        shutdownIntentFilter.addAction(Intent.ACTION_SHUTDOWN);
        // HTC devices use a different Intent action:
        // http://stackoverflow.com/q/5076410/422906
        shutdownIntentFilter.addAction("android.intent.action.QUICKBOOT_POWEROFF");
        registerReceiver(shutdownMonitor, shutdownIntentFilter);

        if (prefs.getBoolean(SP_KEY_ENABLE_AUTO_RESTART_SERVICE, false) && Arrays
                .asList(ANDROID_VERSIONS_ALLOWED_TO_AUTO_RESTART_SERVICE).contains(Build.VERSION.RELEASE)) {
            // Kitkat and JellyBean auto-kill service workaround
            // http://stackoverflow.com/a/20735519/1527491
            ensureServiceStaysRunning();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // Tell the PendingInsert worker thread to stop.
        try {
            pendingInsert.put(STOP_PENDING_CONTENT_MARKER);
        } catch (InterruptedException e) {
            Log.e(TAG, "Failed to stop PendingInsert worker thread", e);
        }

        // Stop listening to system events.
        unregisterReceiver(screenMonitor);
        tm.listen(phoneMonitor, PhoneStateListener.LISTEN_NONE);
        unregisterReceiver(connectionMonitor);
        unregisterReceiver(batteryMonitor);
        unregisterReceiver(shutdownMonitor);

        tm = null;
        cm = null;
        pm = null;

        // Remove the status bar notification.
        stopForeground(true);

        prefs.unregisterOnSharedPreferenceChangeListener(this);
        prefs = null;

        if (freeLargeIcon != null) {
            freeLargeIcon.recycle();
            freeLargeIcon = null;
        }
        if (orangeLargeIcon != null) {
            orangeLargeIcon.recycle();
            orangeLargeIcon = null;
        }
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (intent != null) //we have intent to handle
        {
            if (intent.getBooleanExtra(INTENT_ALARM_RESTART_SERVICE_DIED, false)) { //intent to check service is alive
                if (DEBUG)
                    Log.d(TAG, "onStartCommand > after ALARM_RESTART_SERVICE_DIED [ Kitkat START_STICKY bug ]");
                if (isRunning()) {
                    if (DEBUG)
                        Log.d(TAG,
                                "onStartCommand > Service already running - return immediately... [ Kitkat START_STICKY bug ]");
                    ensureServiceStaysRunning();
                    return START_STICKY;
                }
            }

            if (intent.getBooleanExtra(INTENT_UPDATE_NOTIF_ON_LOCKSCREEN, false)) { //intent to update the notification on lockscreen (hide / show)
                if (DEBUG) {
                    Log.d(TAG, "onStartCommand > update the notification on lockscreen (hide / show)");
                }
                updateNotification(false, false);
            }
        }

        if (stopServiceIfSimOperatorIsNotFreeMobile())
            return START_NOT_STICKY;

        // Update with current state.
        onConnectivityUpdated();
        onPhoneStateUpdated();
        updateNotification(false, false);

        return START_STICKY;
    }

    /**
     * Stops the service if sim operator is not free mobile.
     *
     * @return true if the service was killed
     */
    private boolean stopServiceIfSimOperatorIsNotFreeMobile() {
        if (!DEBUG && MobileOperator.FREE_MOBILE.isCurrentSimOwner(getApplicationContext()) == 0) {
            stopSelf();
            return true;
        }

        return false;
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        if (prefs.getBoolean(SP_KEY_ENABLE_AUTO_RESTART_SERVICE, false) && Arrays
                .asList(ANDROID_VERSIONS_ALLOWED_TO_AUTO_RESTART_SERVICE).contains(Build.VERSION.RELEASE)) {
            // If task was removed, we should launch the service again.
            if (DEBUG)
                Log.d(TAG, "onTaskRemoved > setting alarm to restart service [ Kitkat START_STICKY bug ]");
            Intent restartService = new Intent(getApplicationContext(), this.getClass());
            restartService.setPackage(getPackageName());
            PendingIntent restartServicePI = PendingIntent.getService(getApplicationContext(), 1, restartService,
                    PendingIntent.FLAG_ONE_SHOT);
            AlarmManager alarmService = (AlarmManager) getApplicationContext()
                    .getSystemService(Context.ALARM_SERVICE);
            alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePI);
        }

    }

    private boolean isRunning() {
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE))
            if (MonitorService.class.getName().equals(service.service.getClassName()))
                return true;
        return false;
    }

    private void ensureServiceStaysRunning() {
        // KitKat appears to have (in some cases) forgotten how to honor START_STICKY
        // and if the service is killed, it doesn't restart.  On an emulator & AOSP device, it restarts...
        // on my CM device, it does not - WTF?  So, we'll make sure it gets back
        // up and running in a minimum of 10 minutes.  We reset our timer on a handler every
        // 2 minutes...but since the handler runs on uptime vs. the alarm which is on realtime,
        // it is entirely possible that the alarm doesn't get reset.  So - we make it a noop,
        // but this will still count against the app as a wakelock when it triggers.  Oh well,
        // it should never cause a device wakeup.  We're also at SDK 19 preferred, so the alarm
        // mgr set algorithm is better on memory consumption which is good.
        // http://stackoverflow.com/a/20735519/1527491
        if (prefs.getBoolean(SP_KEY_ENABLE_AUTO_RESTART_SERVICE, false) && Arrays
                .asList(ANDROID_VERSIONS_ALLOWED_TO_AUTO_RESTART_SERVICE).contains(Build.VERSION.RELEASE)) {
            if (DEBUG)
                Log.d(TAG, "ensureServiceStaysRunning > setting alarm. [ Kitkat START_STICKY bug ]");
            // A restart intent - this never changes...        
            final int restartAlarmInterval = 10 * 60 * 1000;
            final int resetAlarmTimer = 1 * 60 * 1000;
            final Intent restartIntent = new Intent(this, MonitorService.class);
            restartIntent.putExtra(INTENT_ALARM_RESTART_SERVICE_DIED, true);
            final AlarmManager alarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
            Handler restartServiceHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    // Create a pending intent
                    PendingIntent pintent = PendingIntent.getService(getApplicationContext(), 0, restartIntent, 0);
                    alarmMgr.set(AlarmManager.ELAPSED_REALTIME,
                            SystemClock.elapsedRealtime() + restartAlarmInterval, pintent);
                    sendEmptyMessageDelayed(0, resetAlarmTimer);
                }
            };
            restartServiceHandler.sendEmptyMessageDelayed(0, 0);
        }
    }

    /**
     * Update the status bar notification.
     * @param playSound play notification sound
     * @param phoneStateUpdated if phone state has been updated
     */
    private void updateNotification(boolean playSound, boolean phoneStateUpdated) {
        String tickerText, contentText;
        int smallIcon;
        final int notificationPriority = NotificationCompat.PRIORITY_LOW;
        Bitmap largeIcon;
        final PendingIntent contentIntent = openUIPendingIntent;
        boolean airplaneModeOn = false;

        MobileOperator mobOp = MobileOperator.fromString(mobileOperatorId);

        if (mobOp == null) { // Not Free Mobile nor Orange
            if (airplaneModeOn = isAirplaneModeOn()) { // Airplane mode
                tickerText = getString(R.string.stat_airplane_mode_on);
                contentText = getString(R.string.notif_monitoring_disabled);
            } else if (mobileOperatorId == null) { // No signal
                tickerText = getString(R.string.stat_no_signal);
                contentText = getString(R.string.notif_action_open_network_operator_settings);
            } else { // Foreign operator
                tickerText = getString(R.string.stat_connected_to_foreign_mobile_network);
                contentText = getString(R.string.notif_action_open_network_operator_settings);
            }

            smallIcon = android.R.drawable.stat_sys_warning;
            largeIcon = null; // Use small icon as large icon.

        } else { // Free Mobile or Orange detected
            tickerText = String.format(getString(R.string.stat_connected_to_mobile_network), mobOp.toName(this));

            final Integer networkTypeRes = NETWORK_TYPE_STRINGS.get(mobileNetworkType,
                    R.string.network_type_unknown);
            contentText = String.format(getString(R.string.mobile_network_type), getString(networkTypeRes));
            if (MobileOperator.FREE_MOBILE.equals(mobOp) && isFemtocell) {
                contentText = getString(R.string.network_free_femtocell, contentText);
            }

            smallIcon = getStatIcon(mobOp);
            largeIcon = getStatLargeIcon(mobOp);
        }

        final NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(getApplicationContext());
        nBuilder.setSmallIcon(smallIcon).setContentTitle(tickerText).setContentText(contentText)
                .setTicker(tickerText).setContentIntent(contentIntent) // always set the content intent - exception fired on GB if null
                .setPriority(notificationPriority).setWhen(0);

        //ACRA bug
        //see : http://stackoverflow.com/questions/15642900/bad-notification-posted-from-package-couldnt-expand-remoteviews
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2) {
            nBuilder.setLargeIcon(largeIcon);
        }

        if ((prefs != null) && (prefs.getBoolean(SP_KEY_ENABLE_NOTIF_ACTIONS, true))) {
            if (airplaneModeOn && wirelessSettingsPendingIntent != null) {
                nBuilder.addAction(android.R.drawable.ic_menu_preferences,
                        getString(R.string.notif_action_open_wireless_settings), wirelessSettingsPendingIntent);
            } else if (networkOperatorSettingsPendingIntent != null) {
                nBuilder.addAction(android.R.drawable.ic_menu_preferences,
                        getString(R.string.notif_action_open_network_operator_settings),
                        networkOperatorSettingsPendingIntent);
            }
        }

        if (playSound) {
            Log.d(TAG, "Play notification sound");

            // check if we need to trigger LTE alarm
            // network type changed from 3G to LTE
            boolean lteAlarm = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
                    && (lastMobileNetworkTypeForLTEDetect != null)
                    && (lastMobileNetworkTypeForLTEDetect != TelephonyManager.NETWORK_TYPE_LTE)
                    && (mobileNetworkType == TelephonyManager.NETWORK_TYPE_LTE));

            // we have just connected on a femtocell, trigger sound
            boolean femtocellConnection = ((isFemtocell) && (lastIsFemtocell != null) && (!lastIsFemtocell)
                    && MobileOperator.FREE_MOBILE.equals(mobOp));

            // other case : trigger FreeMobile 3G alarm if we changed network type from LTE to 3G
            if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) // first, are we using a compatible android version ?
                    && (mobOp == MobileOperator.FREE_MOBILE) // second, are we on FreeMobile network ?
                    && (!lteAlarm) // third, are not we on LTE network
                    && (lastMobileNetworkTypeForLTEDetect != null)
                    && (lastMobileNetworkTypeForLTEDetect.equals(TelephonyManager.NETWORK_TYPE_LTE))) { // fourth, and the last mobile network type is LTE
                phoneStateUpdated = true; // trigger 3G alarm
            }

            if ((phoneStateUpdated || lteAlarm || femtocellConnection) && (prefs != null)) {
                String rawSoundUri = null;
                if (lteAlarm) { // we are in LTE alarm case
                    Log.d(TAG, "Try to play LTE alarm");
                    rawSoundUri = prefs.getString(SP_KEY_STAT_NOTIF_SOUND_4G, null);
                } else if (femtocellConnection) { //we are in fetmocell connection alarm case
                    Log.d(TAG, "Try to play Femtocell alarm");
                    rawSoundUri = prefs.getString(SP_KEY_STAT_NOTIF_SOUND_FEMTO, null);
                } else { // we are in operator change case
                    Log.d(TAG, "Try to play normal operator alarm");
                    rawSoundUri = prefs
                            .getString((mobOp == MobileOperator.FREE_MOBILE) ? SP_KEY_STAT_NOTIF_SOUND_FREE_MOBILE
                                    : SP_KEY_STAT_NOTIF_SOUND_ORANGE, null);
                }
                if (rawSoundUri != null) {
                    final Uri soundUri = Uri.parse(rawSoundUri);
                    nBuilder.setSound(soundUri);
                }
            } else if (BuildConfig.DEBUG) {
                Log.d(TAG, "No notification sound to play");
            }
        }

        startForeground(R.string.stat_connected_to_mobile_network, nBuilder.build());
    }

    /**
     * Gets the state of Airplane Mode.
     */
    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    private boolean isAirplaneModeOn() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return Settings.System.getInt(getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) != 0;
        } else {
            return Settings.Global.getInt(getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
        }
    }

    private int getStatIcon(MobileOperator op) {
        final String themeKey = prefs.getString(SP_KEY_THEME, THEME_DEFAULT);
        Theme theme = THEMES.get(themeKey);
        if (theme == null) {
            theme = THEMES.get(THEME_DEFAULT);
        }

        if (MobileOperator.FREE_MOBILE.equals(op) && isFemtocell) {
            return theme.freeFemtoIcon;
        } else if (MobileOperator.FREE_MOBILE.equals(op)) {
            return theme.freeIcon;
        } else if (MobileOperator.ORANGE.equals(op)) {
            return theme.orangeIcon;
        }
        return android.R.drawable.ic_dialog_alert;
    }

    private Bitmap getStatLargeIcon(MobileOperator op) {
        if (MobileOperator.FREE_MOBILE.equals(op) && isFemtocell) {
            return freeFemtoLargeIcon;
        } else if (MobileOperator.FREE_MOBILE.equals(op)) {
            return freeLargeIcon;
        } else if (MobileOperator.ORANGE.equals(op)) {
            return orangeLargeIcon;
        }
        return null;
    }

    private void onDeviceShutdown() {
        Log.i(TAG, "Device is about to shut down");
        powerOn = false;
        updateEventDatabase();
    }

    /**
     * This method is called when the phone data connectivity is updated.
     */
    private boolean onConnectivityUpdated() {
        // Get the Wi-Fi connectivity state.
        final NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        final boolean wifiNetworkConnected = ni != null && ni.isConnected();

        // Prevent duplicated inserts.
        if (lastWifiConnected != null && lastWifiConnected == wifiNetworkConnected) {
            return false;
        }
        lastWifiConnected = wifiNetworkConnected;

        Log.i(TAG, "Wifi state updated: connected=" + wifiNetworkConnected);
        return true;
    }

    /**
     * This method is called when the phone service state is updated.
     * @return -1 : no update ; 0 : minor update ; 1 : major update
     * It is a major update if mobile operator changes or phone connects a network.
     */
    private int onPhoneStateUpdated() {
        mobileOperatorId = tm != null ? tm.getNetworkOperator() : null;
        if (TextUtils.isEmpty(mobileOperatorId)) {
            mobileOperatorId = null;
        }

        updateFemtocellStatus();

        // Prevent duplicated inserts.
        if (lastMobileNetworkConnected != null && lastMobileOperatorId != null && lastIsFemtocell != null
                && lastMobileNetworkType != null && lastMobileNetworkConnected.equals(mobileNetworkConnected)
                && lastIsFemtocell.equals(isFemtocell) && lastMobileOperatorId.equals(mobileOperatorId)
                && lastMobileNetworkType.equals(mobileNetworkType)) {
            return -1;
        }

        int ret = 0;

        if (lastMobileNetworkConnected != null && lastMobileNetworkConnected != mobileNetworkConnected
                || lastMobileOperatorId != null && !lastMobileOperatorId.equals(mobileOperatorId))
            ret = 1;

        lastMobileNetworkConnected = mobileNetworkConnected;
        lastMobileOperatorId = mobileOperatorId;
        lastIsFemtocell = isFemtocell;
        lastMobileNetworkTypeForLTEDetect = lastMobileNetworkType; // save previous network type for LTE detection
        lastMobileNetworkType = mobileNetworkType;

        Log.i(TAG, "Phone state updated: operator=" + mobileOperatorId + "; connected=" + mobileNetworkConnected
                + "; femtocell=" + isFemtocell);
        return ret;
    }

    /**
     * Check if we are connected on a Free Mobile femtocell
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void updateFemtocellStatus() {
        // No need to check LAC if current operator is not free mobile
        // And no need to check if network type is not femtocell supported network
        if ((!MobileOperator.FREE_MOBILE.equals(MobileOperator.fromString(mobileOperatorId)))
                || ((MobileOperator.FREE_MOBILE.equals(MobileOperator.fromString(mobileOperatorId)))
                        && (!FEMTOCELL_AVAILABLE_NETWORK_TYPE.contains(mobileNetworkType)))) {
            isFemtocell = false;
            return;
        }

        Integer lac = null;
        if (tm != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {

                //get the cell list
                List<CellInfo> cellInfos = tm.getAllCellInfo();
                if (cellInfos != null) {
                    for (CellInfo cellInfo : cellInfos) {

                        if (cellInfo.isRegistered()) { //we use only registered cells
                            if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
                                    && (cellInfo instanceof CellInfoWcdma)) { //manage the wcdma cell case
                                Log.d(TAG, "We got a WCDMA cell");
                                CellIdentityWcdma ci = ((CellInfoWcdma) cellInfo).getCellIdentity();
                                if (ci != null) { //save the LAC and exit loop
                                    lac = ci.getLac();
                                    Log.d(TAG, "We got the LAC - exit loop");
                                    break;
                                }

                            } else if (cellInfo instanceof CellInfoGsm) { //test the gsm case
                                CellIdentityGsm ci = ((CellInfoGsm) cellInfo).getCellIdentity();
                                Log.d(TAG, "We got a CDMA cell");
                                if (ci != null) { //save the LAC and exit loop
                                    lac = ci.getLac();
                                    Log.d(TAG, "We got the LAC - exit loop");
                                    break;
                                }
                            }

                        } else
                            Log.d(TAG, "Unregistered cell - skipping");
                    }
                } else
                    Log.d(TAG, "No cell infos available");

            }
            if (lac == null) { //use old API if LAC was not found with the new method (useful for buggy devices such as Samsung Galaxy S5) or if SDK is too old
                CellLocation cellLocation = tm.getCellLocation(); //cell location might be null... handle with care
                if ((cellLocation != null) && (cellLocation instanceof GsmCellLocation)) {
                    Log.d(TAG, "We got a old GSM cell with LAC");
                    lac = ((GsmCellLocation) cellLocation).getLac();
                }
            }
        }
        if (DEBUG)
            Log.d(TAG, "LAC value : " + lac);

        Log.i(TAG, "Femtocell value : " + isFemtocell);

        if (lac != null) {
            String lacAsString = String.valueOf(lac);
            isFemtocell = (lacAsString.length() == 4)
                    && (lacAsString.subSequence(1, 3).equals(FREE_MOBILE_FEMTOCELL_LAC_CODE));
        }
    }

    private void updateEventDatabase() {
        final Event e = new Event();
        e.timestamp = System.currentTimeMillis();
        e.screenOn = pm != null && pm.isScreenOn();
        e.batteryLevel = getBatteryLevel();
        e.wifiConnected = Boolean.TRUE.equals(lastWifiConnected);
        e.mobileConnected = powerOn && Boolean.TRUE.equals(lastMobileNetworkConnected);
        e.mobileOperator = lastMobileOperatorId;
        e.mobileNetworkType = lastMobileNetworkType != null ? lastMobileNetworkType
                : TelephonyManager.NETWORK_TYPE_UNKNOWN;
        e.powerOn = powerOn;
        e.femtocell = Boolean.TRUE.equals(lastIsFemtocell);
        e.firstInsert = firstInsert;
        firstInsert = false;

        try {
            pendingInsert.put(e);
        } catch (InterruptedException ex) {
            Log.w(TAG, "Failed to schedule event insertion", ex);
        }
    }

    private int getBatteryLevel() {
        if (batteryIntentFilter == null) {
            return 100;
        }
        final Intent i = registerReceiver(null, batteryIntentFilter);
        if (i == null) {
            return 100;
        }

        final int level = i.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
        final int scale = i.getIntExtra(BatteryManager.EXTRA_SCALE, 0);

        return scale == 0 ? 100 : (int) Math.round(level * 100d / scale);
    }

    /**
     * This internal thread is responsible for inserting data into the application database. This thread will prevent
     * the main loop from being used for interacting with the database, which could cause "Application Not Responding"
     * dialogs.
     */
    private static class PendingInsertWorker extends Thread {
        private final Context context;
        private final BlockingQueue<Event> pendingInsert;

        public PendingInsertWorker(final Context context, final BlockingQueue<Event> pendingInsert) {
            super("FreeMobileNetstat/PendingInsert");
            setDaemon(true);
            this.context = context;
            this.pendingInsert = pendingInsert;
        }

        @Override
        public void run() {
            if (DEBUG) {
                Log.d(TAG, "PendingInsert worker thread is started");
            }

            // Set a lower priority to prevent UI from lagging.
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            final ContentValues cv = new ContentValues(7);
            final ContentResolver cr = context.getContentResolver();

            final ContentValues lastCV = new ContentValues(7);
            long lastEventHashCode = 0;

            boolean running = true;
            while (running) {
                try {
                    final Event e = pendingInsert.take();
                    if (STOP_PENDING_CONTENT_MARKER == e) {
                        running = false;
                    } else {
                        e.write(cv);

                        // Check the last inserted event hash code:
                        // if the hash code is the same, the event is not
                        // inserted.
                        lastCV.putAll(cv);
                        lastCV.remove(Events.TIMESTAMP);
                        if (e.powerOn && lastCV.hashCode() == lastEventHashCode) {
                            if (DEBUG) {
                                Log.d(TAG, "Skip event insertion: " + e);
                            }
                        } else {
                            if (DEBUG) {
                                Log.d(TAG, "Inserting new event into database: " + e);
                            }
                            cr.insert(Events.CONTENT_URI, cv);
                        }
                        lastEventHashCode = lastCV.hashCode();
                        lastCV.clear();
                    }
                    cv.clear();
                } catch (InterruptedException e) {
                    running = false;
                } catch (Exception e) {
                    Log.e(TAG, "Pending insert failed", e);
                }
            }

            if (DEBUG) {
                Log.d(TAG, "PendingInsert worker thread is terminated");
            }
        }
    }

    /**
     * Notification theme.
     * 
     * @author Pixmob
     */
    private static class Theme {
        public final int freeIcon;
        public final int freeFemtoIcon;
        public final int orangeIcon;

        public Theme(final int freeIcon, final int freeFemtoIcon, final int orangeIcon) {
            this.freeIcon = freeIcon;
            this.freeFemtoIcon = freeFemtoIcon;
            this.orangeIcon = orangeIcon;
        }
    }
}