/*
* 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 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_NOTIF_ACTIONS;
import static org.pixmob.freemobile.netstat.Constants.SP_KEY_STAT_NOTIF_SOUND;
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;
import android.app.Notification;
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.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.support.v4.app.NotificationCompat;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.pixmob.freemobile.netstat.content.NetstatContract.Events;
import org.pixmob.freemobile.netstat.util.IntentFactory;
/**
* 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< String, Theme>(3);
/**
* Match network types from {@link TelephonyManager} with the corresponding string.
*/
private static final SparseIntArray NETWORK_TYPE_STRINGS = new SparseIntArray(8);
/**
* Special data used for terminating the PendingInsert worker thread.
*/
private static final Event STOP_PENDING_CONTENT_MARKER = new Event();
/**
* This intent will open the main UI.
*/
private PendingIntent openUIPendingIntent;
private PendingIntent networkOperatorSettingsPendingIntent;
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 String lastMobileOperatorId;
private String mobileOperatorId;
private boolean mobileNetworkConnected;
private int mobileNetworkType;
private BlockingQueue< Event> pendingInsert;
private SharedPreferences prefs;
private Bitmap freeLargeIcon;
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);
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);
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);
THEMES.put(THEME_DEFAULT, new Theme(R.drawable.ic_stat_notify_service_free,
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_orange_color));
THEMES.put(THEME_PIE, new Theme(R.drawable.ic_stat_notify_service_free_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);
}
}
@Override
public void onCreate() {
super.onCreate();
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);
freeLargeIcon =
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(),
R.drawable.ic_stat_notify_service_free_large), largeIconWidth, largeIconHeight, true);
orangeLargeIcon =
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(),
R.drawable.ic_stat_notify_service_orange_large), largeIconWidth, largeIconHeight, true);
}
pm = (PowerManager) getSystemService(POWER_SERVICE);
tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
// Initialize and start a worker thread for inserting rows into the
// application database.
final Context c = getApplicationContext();
pendingInsert = new ArrayBlockingQueue< Event>(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.
networkOperatorSettingsPendingIntent =
PendingIntent.getActivity(this, 0, IntentFactory.networkOperatorSettings(this),
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) {
mobileNetworkType = networkType;
updateNotification(false);
}
@Override
public void onServiceStateChanged(ServiceState serviceState) {
if (!DEBUG) {
// Check if the SIM card is compatible.
if (tm != null && TelephonyManager.SIM_STATE_READY == tm.getSimState()) {
final String rawMobOp = tm.getSimOperator();
final MobileOperator mobOp = MobileOperator.fromString(rawMobOp);
if (!MobileOperator.FREE_MOBILE.equals(mobOp)) {
Log.e(TAG, "SIM card is not compatible: " + rawMobOp);
// The service is stopped, since the SIM card is not
// compatible.
stopSelf();
}
}
}
mobileNetworkConnected =
serviceState != null && serviceState.getState() == ServiceState.STATE_IN_SERVICE;
final boolean phoneStateUpdated = onPhoneStateUpdated();
if (phoneStateUpdated) {
updateEventDatabase();
}
updateNotification(phoneStateUpdated);
}
};
tm.listen(phoneMonitor, PhoneStateListener.LISTEN_SERVICE_STATE |
PhoneStateListener.LISTEN_DATA_CONNECTION_STATE);
// 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);
}
@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) {
// Update with current state.
onConnectivityUpdated();
onPhoneStateUpdated();
updateNotification(false);
return START_STICKY;
}
/**
* Update the status bar notification.
*/
private void updateNotification(boolean playSound) {
final MobileOperator mobOp = MobileOperator.fromString(mobileOperatorId);
if (!mobileNetworkConnected) {
// Not connected to a mobile network: plane mode may be enabled.
stopForeground(true);
return;
}
final NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(getApplicationContext());
if (mobOp == null) {
// Connected to a foreign mobile network.
final String tickerText = getString(R.string.stat_connected_to_foreign_mobile_network);
final String contentText = getString(R.string.notif_action_open_network_operator_settings);
nBuilder.setTicker(tickerText).setContentText(contentText).setContentTitle(tickerText).setSmallIcon(
android.R.drawable.stat_sys_warning).setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(networkOperatorSettingsPendingIntent).setWhen(0);
} else {
final String tickerText =
String.format(getString(R.string.stat_connected_to_mobile_network), mobOp.toName(this));
Integer networkTypeRes = NETWORK_TYPE_STRINGS.get(mobileNetworkType);
if(networkTypeRes == null) {
networkTypeRes = R.string.network_type_unknown;
}
final String contentText =
String.format(getString(R.string.mobile_network_type), getString(networkTypeRes));
final int iconRes = getStatIcon(mobOp);
nBuilder.setSmallIcon(iconRes).setLargeIcon(getStatLargeIcon(mobOp)).setTicker(tickerText)
.setContentText(contentText).setContentTitle(tickerText).setPriority(
NotificationCompat.PRIORITY_LOW).setContentIntent(openUIPendingIntent).setWhen(0);
if (prefs.getBoolean(SP_KEY_ENABLE_NOTIF_ACTIONS, true)) {
nBuilder.addAction(R.drawable.ic_stat_notify_action_network_operator_settings,
getString(R.string.notif_action_open_network_operator_settings),
networkOperatorSettingsPendingIntent);
}
}
if (playSound) {
final String rawSoundUri = prefs.getString(SP_KEY_STAT_NOTIF_SOUND, null);
if (rawSoundUri != null) {
final Uri soundUri = Uri.parse(rawSoundUri);
nBuilder.setSound(soundUri);
}
}
final Notification n = nBuilder.build();
startForeground(R.string.stat_connected_to_mobile_network, n);
}
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)) {
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)) {
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.booleanValue() == 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.
*/
private boolean onPhoneStateUpdated() {
mobileOperatorId = tm != null ? tm.getNetworkOperator() : null;
if (TextUtils.isEmpty(mobileOperatorId)) {
mobileOperatorId = null;
}
// Prevent duplicated inserts.
if (lastMobileNetworkConnected != null && lastMobileOperatorId != null &&
lastMobileNetworkConnected.booleanValue() == mobileNetworkConnected &&
lastMobileOperatorId.equals(mobileOperatorId)) {
return false;
}
lastMobileNetworkConnected = mobileNetworkConnected;
lastMobileOperatorId = mobileOperatorId;
Log.i(TAG, "Phone state updated: operator=" + mobileOperatorId + "; connected=" + mobileNetworkConnected);
return true;
}
private void updateEventDatabase() {
final Event e = new Event();
e.timestamp = System.currentTimeMillis();
e.screenOn = pm != null ? pm.isScreenOn() : false;
e.batteryLevel = getBatteryLevel();
e.wifiConnected = Boolean.TRUE.equals(lastWifiConnected);
e.mobileConnected = powerOn ? Boolean.TRUE.equals(lastMobileNetworkConnected) : false;
e.mobileOperator = lastMobileOperatorId;
e.powerOn = powerOn;
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 orangeIcon;
public Theme(final int freeIcon, final int orangeIcon) {
this.freeIcon = freeIcon;
this.orangeIcon = orangeIcon;
}
}
}