package net.assemble.emailnotify.core;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import net.assemble.emailnotify.core.notification.EmailNotificationManager;
import net.assemble.emailnotify.core.notification.EmailNotificationService;
import net.assemble.emailnotify.core.notification.EmailNotifyScreenReceiver;
import net.assemble.emailnotify.core.preferences.EmailNotifyPreferences;
import net.orleaf.android.MyLog;
import net.orleaf.android.MyLogReportService;
/**
* メール着信監視サービス
*
* ログを監視し、WAP PUSH 受信を検出する。
*/
public class EmailNotifyObserveService extends Service {
private static final long RESTART_INTERVAL = 30 * 60 * 1000;
private static final long LOG_SEND_INTERVAL = 24 * 60 * 60 * 1000;
private static final int LOG_SEND_DISPERSION = 10 * 60; /* sec */
private static ComponentName mService;
private static boolean mActive = false;
private EmailNotifyScreenReceiver mScreenReceiver;
private LogCheckThread mLogCheckThread;
private boolean mStopLogCheckThread;
private long mLastCheck;
private int mSaveApplicationId;
private PendingIntent mRestartIntent = null;
@Override
public void onCreate() {
MyLog.v(this, EmailNotify.TAG, "+ " + Build.FINGERPRINT);
super.onCreate();
// プリファレンスのバージョンアップ
EmailNotifyPreferences.upgrade(this);
// ネットワーク復元情報を消去
EmailNotifyPreferences.unsetNetworkInfo(this);
// ACTION_SCREEN_ON レシーバの登録
mScreenReceiver = new EmailNotifyScreenReceiver();
registerReceiver(mScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_ON));
// 異常終了チェック
mLastCheck = EmailNotifyPreferences.getLastCheck(this);
if (mLastCheck != 0) {
Date d = new Date(mLastCheck);
MyLog.w(this, EmailNotify.TAG, "Service restarted unexpectedly. Last checked at " + d.toLocaleString());
// すべての通知を一旦消去する
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
// 未消去の通知を復元する
EmailNotificationService.restoreNotifications(this);
// ネットワーク復元情報を消去
EmailNotifyPreferences.unsetNetworkInfo(this);
} else {
// 正常に終了していた場合、サービス開始以前のものを通知しない。
mLastCheck = Calendar.getInstance().getTimeInMillis();
EmailNotifyPreferences.setLastCheck(this, mLastCheck);
}
// 定期的に再startを仕掛けておく
mRestartIntent = PendingIntent.getService(this, 0,
new Intent(this, EmailNotifyObserveService.class), 0);
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.setRepeating(AlarmManager.RTC, 0, RESTART_INTERVAL, mRestartIntent);
startCheck();
}
// This is the old onStart method that will be called on the pre-2.0
// platform. On 2.0 or later we override onStartCommand() so this
// method will not be called.
@SuppressWarnings("deprecation")
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent);
}
@TargetApi(5)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
private void handleCommand(@SuppressWarnings("UnusedParameters") Intent intent) {
MyLog.v(this, EmailNotify.TAG, "-");
startCheck();
}
public void startCheck() {
mActive = true;
// 常駐アイコン
if (EmailNotifyPreferences.getNotificationIcon(this)) {
EmailNotificationManager.showNotificationIcon(this);
} else {
EmailNotificationManager.clearNotificationIcon(this);
}
// ログ送信
//noinspection PointlessBooleanExpression
if (BuildConfig.FEATURE_SENDLOG && EmailNotifyPreferences.getSendLog(this)) {
long prev = EmailNotifyPreferences.getLogSent(this);
long current = Calendar.getInstance().getTimeInMillis();
if (prev == 0 || current - prev > LOG_SEND_INTERVAL) {
Random random = new Random();
String reporter_id = EmailNotifyPreferences.getPreferenceId(this);
int delay = random.nextInt(LOG_SEND_DISPERSION);
int waitconn;
if (EmailNotifyPreferences.getSendLogWifionly(this)) {
waitconn = MyLogReportService.WAIT_CONNECT_WIFIONLY;
} else {
waitconn = MyLogReportService.WAIT_CONNECT_ENABLE;
}
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "Start sending report. (delay=" + delay + ")");
Intent intent = new Intent(this, EmailNotifyReceiver.class);
intent.setAction(EmailNotify.ACTION_LOG_SENT);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
MyLogReportService.startService(this, reporter_id, pendingIntent, delay, waitconn);
}
}
// リアルタイムログ監視開始
startLogCheckThread();
}
public void onDestroy() {
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
MyLog.v(this, EmailNotify.TAG, "!");
super.onDestroy();
mActive = false;
if (mRestartIntent != null) {
am.cancel(mRestartIntent);
mRestartIntent = null;
}
// 通知アイコン
EmailNotificationManager.clearNotificationIcon(this);
// レシーバ解除
unregisterReceiver(mScreenReceiver);
// リアルタイムログ監視停止
stopLogCheckThread();
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
/**
* ログ日時解析
* @param line ログ行
* @return 日時
*/
private Calendar getLogDate(String line) {
String logdate = line.substring(0, 18);
Calendar cal = Calendar.getInstance();
Date date;
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
date = sdf.parse(cal.get(Calendar.YEAR) + "-" + logdate);
} catch (ParseException e) {
Log.w(EmailNotify.TAG, "Unexpected log date: " + logdate);
return null;
}
cal.setTime(date);
return cal;
}
/**
* ログ行をチェック
*
* @param line ログ文字列
* @return WapPdu WAP PDU (null:メール通知ではない)
*/
private WapPdu checkLogLine(String line) {
//if (BuildConfig.DEBUG) Log.v(EmailNotify.TAG, "> " + line);
if (line.length() >= 19 &&
(line.substring(19).startsWith("D/WAP PUSH") || line.substring(19).startsWith("D/EmailPushNotification"))) {
String[] lines =line.split(": ", 2);
String tag = lines[0].substring(19);
String log = lines[1];
Calendar ccal = getLogDate(line);
if (ccal == null) {
return null;
}
if (ccal.getTimeInMillis() <= mLastCheck) {
// チェック済
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "Already checked (" + ccal.getTimeInMillis() + " <= " + mLastCheck + ")");
return null;
}
MyLog.v(this, EmailNotify.TAG, "> " + line);
String data = null;
WapPdu pdu = null;
// LYNX(SH-01B)対応
// ex) D/WAP PUSH(XXXXXX): Received EMN
if (EmailNotifyPreferences.getLynxWorkaround(this) &&
tag.startsWith("D/WAP PUSH") && log.equals("Receive EMN")) {
MyLog.i(this, EmailNotify.TAG, "Detected EMN");
mLastCheck = ccal.getTimeInMillis();
return new WapPdu(EmailNotifyPreferences.SERVICE_UNSPEC, "");
}
// T-01D/SH-01D/SO-02D/SO-03D/F-03D/P-02Dなど
// ex) D/WAP PUSH(XXXXXX): wpman processMsg 36956:application/vnd.wap.emn+wbxml
// サービス不明として通知
// 他の要因でspモード通知できる場合は無視する。(初回は回避できないこともある)
if (!EmailNotifyPreferences.getNotifySupport(this, EmailNotifyPreferences.SERVICE_SPMODE) &&
tag.startsWith("D/WAP PUSH") &&
log.contains("wpman processMsg ") && log.endsWith(":application/vnd.wap.emn+wbxml")) {
MyLog.i(this, EmailNotify.TAG, "Detected processMsg:application/vnd.wap.emn+wbxml");
mLastCheck = ccal.getTimeInMillis();
return new WapPdu(EmailNotifyPreferences.SERVICE_UNSPEC, "");
}
// F-05D
// ex) startService[WiFi=Enable] : intent=Intent { cmp=jp.co.nttdocomo.carriermail/.SMSService (has extras) }
// ex) startService[WiFi=Disable] : intent=Intent { cmp=jp.co.nttdocomo.carriermail/.SMSService (has extras) }
if (tag.startsWith("D/WAP PUSH") &&
log.startsWith("startService[WiFi=") && log.endsWith("] : intent=Intent { cmp=jp.co.nttdocomo.carriermail/.SMSService (has extras) }")) {
MyLog.i(this, EmailNotify.TAG, "Detected startService jp.co.nttdocomo.carriermail/.SMSService");
mLastCheck = ccal.getTimeInMillis();
return new WapPdu(EmailNotifyPreferences.SERVICE_SPMODE, "docomo.ne.jp");
}
// Xperia arc(SO-01C)対応
// ex) D/WAP PUSH(XXXXXX): call startService : Intent { act=android.provider.Telephony.WAP_PUSH_RECEIVED typ=application/vnd.wap.emn+wbxml cmp=jp.co.nttdocomo.carriermail/.SMSService (has extras) }
if (EmailNotifyPreferences.getXperiaarcWorkaround(this)) {
// spモードメール
if (tag.startsWith("D/WAP PUSH") && log.equals("call startService : Intent { act=android.provider.Telephony.WAP_PUSH_RECEIVED typ=application/vnd.wap.emn+wbxml cmp=jp.co.nttdocomo.carriermail/.SMSService (has extras) }")) {
MyLog.i(this, EmailNotify.TAG, "Detected broadcast WAP_PUSH_RECEIVED for sp-mode");
mLastCheck = ccal.getTimeInMillis();
return new WapPdu(EmailNotifyPreferences.SERVICE_SPMODE, "docomo.ne.jp");
}
// mopera Uメール
if (tag.startsWith("D/EmailPushNotification") && log.startsWith("Wap data : ")) {
if (mSaveApplicationId == 0x09) {
// Content-Type: application/vnd.wap.emn+wbxml と仮定する
data = log.split("Wap data : ")[1];
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "Found Wap data : " + data);
try {
pdu = new WapPdu("application/vnd.wap.emn+wbxml", 0x09, hex2bytes(data));
} catch (Exception e) {
MyLog.w(this, EmailNotify.TAG, "Invalid PDU: " + data);
e.printStackTrace();
}
}
}
}
if (tag.startsWith("D/WAP PUSH") && log.contains("Rx: ")) {
data = log.split("Rx: ")[1];
try {
pdu = new WapPdu(hex2bytes(data));
} catch (Exception e) {
MyLog.w(this, EmailNotify.TAG, "Invalid PDU: " + data);
e.printStackTrace();
}
}
if (pdu != null) {
if (!pdu.decode()) {
MyLog.w(this, EmailNotify.TAG, "Unexpected PDU: " + data);
return null;
}
MyLog.d(this, EmailNotify.TAG, "Detected PDU: " + data);
MyLog.d(this, EmailNotify.TAG, " contentType=" + pdu.getContentType() + ", wapAppID=" + pdu.getApplicationId());
if (pdu.getTimestampDate() != null) {
MyLog.i(this, EmailNotify.TAG, "Detected: " + pdu.getMailbox() + " (" + pdu.getTimestampDate().toLocaleString() + ")");
} else {
MyLog.i(this, EmailNotify.TAG, "Detected: " + pdu.getMailbox());
}
mLastCheck = ccal.getTimeInMillis();
return pdu;
}
}
return null;
}
/**
* 16進数文字列をバイト配列に変換
*
* @param hex 16進数文字列
* @return バイト配列
*/
private byte[] hex2bytes(String hex) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i = 0; i < hex.length(); i += 2){
int b = Integer.parseInt(hex.substring(i, i + 2), 16);
baos.write(b);
}
return baos.toByteArray();
}
/**
* リアルタイムログ監視スレッド
*/
private class LogCheckThread extends Thread {
private Context mCtx;
/**
* Constructor
* @param ctx Context
*/
public LogCheckThread(Context ctx) {
super();
mCtx = ctx;
}
/**
* logcatクリア
*/
private void clearLog() {
try {
Process process = Runtime.getRuntime().exec(new String[] {"logcat", "-c" });
process.waitFor();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
MyLog.d(mCtx, EmailNotify.TAG, "Logcat cleared.");
}
/**
* エラー出力を取得する
*/
private String getErrorMessage(Process process) throws IOException {
String line;
BufferedReader errReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()), 1024);
StringBuilder errMsg = new StringBuilder();
while ((line = errReader.readLine()) != null) {
errMsg.append(line).append("\n");
}
return errMsg.toString().trim();
}
@Override
public void run() {
MyLog.d(mCtx, EmailNotify.TAG, "Starting log check thread.");
EmailNotificationManager.clearSuspendedNotification(mCtx);
String[] command = new String[] {
"logcat",
"-v", "time",
"-s", "*:D"
// ほんとうは「WAP PUSH」でフィルタしたいんだけどスペースがあるとうまくいかない…
};
int errCount = 0;
while (true) {
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(process.getInputStream()), 1024);
int readCount = 0;
String line;
while ((line = bufferedReader.readLine()) != null) {
if (mStopLogCheckThread) {
break;
}
readCount++;
WapPdu pdu = checkLogLine(line);
if (pdu != null) {
// 最終チェック日時を更新
EmailNotifyPreferences.setLastCheck(mCtx, mLastCheck);
EmailNotificationService.startService(mCtx, getLogDate(line).getTime(), pdu);
}
}
bufferedReader.close();
String errMsg = "";
if (line == null) {
errMsg = getErrorMessage(process);
}
process.destroy();
if (!mStopLogCheckThread) {
// 不正終了
MyLog.w(mCtx, EmailNotify.TAG, "Unexpectedly suspended. read=" + readCount);
process.waitFor();
MyLog.d(mCtx, EmailNotify.TAG, "exitValue=" + process.exitValue()+ "\n" + errMsg);
// 5回連続して全く読めなかった場合は通知を出して停止
if (readCount == 0) {
errCount++;
if (errCount >= 5) {
break;
}
} else {
errCount = 0;
}
// ログをクリアして再試行する。
clearLog();
Thread.sleep(5000);
continue;
}
} catch (IOException e) {
MyLog.e(mCtx, EmailNotify.TAG, "Unexpected error on log checking.");
e.printStackTrace();
} catch (InterruptedException e) {
MyLog.e(mCtx, EmailNotify.TAG, "Interrupted on log checking.");
e.printStackTrace();
}
break;
}
if (!mStopLogCheckThread) {
MyLog.e(mCtx, EmailNotify.TAG, "Log check thread suspended unexpectedly!");
EmailNotificationManager.showSuspendedNotification(mCtx);
} else {
MyLog.d(mCtx, EmailNotify.TAG, "Exiting log check thread.");
}
mLogCheckThread = null;
stopSelf();
}
}
/**
* リアルタイムログ監視スレッド開始
*/
private void startLogCheckThread() {
if (mLogCheckThread != null) {
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "Log check thread already running.");
return;
}
mStopLogCheckThread = false;
mLogCheckThread = new LogCheckThread(this);
mLogCheckThread.start();
}
/**
* リアルタイムログ監視スレッド停止指示
*/
private void stopLogCheckThread() {
if (mLogCheckThread == null) {
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "Log check thread not running.");
return;
}
mStopLogCheckThread = true;
}
/**
* サービス開始
*/
public static boolean startService(Context ctx) {
boolean result;
boolean restart = mActive;
mService = ctx.startService(new Intent(ctx, EmailNotifyObserveService.class));
if (mService == null) {
MyLog.e(ctx, EmailNotify.TAG, "Service start failed!");
result = false;
} else {
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "EmailNotifyService started: " + mService);
result = true;
}
if (!restart && result) {
Toast.makeText(ctx, R.string.service_started, Toast.LENGTH_SHORT).show();
MyLog.i(ctx, EmailNotify.TAG, "Service started.");
}
return result;
}
/**
* サービス停止
*/
public static void stopService(Context ctx) {
if (mService != null) {
Intent i = new Intent();
i.setComponent(mService);
boolean res = ctx.stopService(i);
if (!res) {
Log.e(EmailNotify.TAG, "EmailNotifyService could not stop!");
} else {
if (BuildConfig.DEBUG) Log.d(EmailNotify.TAG, "EmailNotifyService stopped: " + mService);
Toast.makeText(ctx, R.string.service_stopped, Toast.LENGTH_SHORT).show();
MyLog.i(ctx, EmailNotify.TAG, "Service stopped.");
mService = null;
// 正常に停止した場合は、次に開始するまでに受信した通知を
// 通知しないようにするため、最終チェック日時をクリアする。
EmailNotifyPreferences.setLastCheck(ctx, 0);
}
}
}
}