package com.nolanlawson.logcat; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Random; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.widget.Toast; import com.nolanlawson.logcat.data.LogLine; import com.nolanlawson.logcat.data.SearchCriteria; import com.nolanlawson.logcat.helper.PreferenceHelper; import com.nolanlawson.logcat.helper.SaveLogHelper; import com.nolanlawson.logcat.helper.ServiceHelper; import com.nolanlawson.logcat.helper.WidgetHelper; import com.nolanlawson.logcat.reader.LogcatReader; import com.nolanlawson.logcat.reader.LogcatReaderLoader; import com.nolanlawson.logcat.util.ArrayUtil; import com.nolanlawson.logcat.util.LogLineAdapterUtil; import com.nolanlawson.logcat.util.UtilLogger; /** * Reads logs. * * @author nolan * */ public class LogcatRecordingService extends IntentService { private static final String ACTION_STOP_RECORDING = "com.nolanlawson.catlog.action.STOP_RECORDING"; public static final String URI_SCHEME = "catlog_recording_service"; public static final String EXTRA_FILENAME = "filename"; public static final String EXTRA_LOADER = "loader"; public static final String EXTRA_QUERY_FILTER = "filter"; public static final String EXTRA_LEVEL = "level"; private static UtilLogger log = new UtilLogger(LogcatRecordingService.class); private LogcatReader reader; private NotificationManager mNM; private Method mStartForeground; private Method mStopForeground; private Method mSetForeground; private boolean killed; private final Object lock = new Object(); private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { log.d("onReceive()"); // received broadcast to kill service killProcess(); ServiceHelper.stopBackgroundServiceIfRunning(context); } }; private Handler handler; public LogcatRecordingService() { super("AppTrackerService"); } @Override public void onCreate() { super.onCreate(); log.d("onCreate()"); mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); IntentFilter intentFilter = new IntentFilter(ACTION_STOP_RECORDING); intentFilter.addDataScheme(URI_SCHEME); registerReceiver(receiver, intentFilter); handler = new Handler(Looper.getMainLooper()); try { mStartForeground = getClass().getMethod("startForeground", int.class, Notification.class); mStopForeground = getClass().getMethod("stopForeground", boolean.class); } catch (NoSuchMethodException e) { // Running on an older platform. log.d(e,"running on older platform; couldn't find startForeground method"); mStartForeground = mStopForeground = null; } try { mSetForeground = getClass().getMethod("setForeground", boolean.class); } catch (NoSuchMethodException e) { // running on newer platform log.d(e,"running on newer platform; couldn't find setForeground method"); mSetForeground = null; } } private void initializeReader(Intent intent) { try { // use the "time" log so we can see what time the logs were logged at LogcatReaderLoader loader = intent.getParcelableExtra(EXTRA_LOADER); reader = loader.loadReader(); while (!reader.readyToRecord() && !killed) { reader.readLine(); // keep skipping lines until we find one that is past the last log line, i.e. // it's ready to record } if (!killed) { makeToast(R.string.log_recording_started, Toast.LENGTH_SHORT); } } catch (IOException e) { log.d(e, ""); } } @Override public void onDestroy() { log.d("onDestroy()"); super.onDestroy(); killProcess(); unregisterReceiver(receiver); stopForegroundCompat(R.string.notification_title); WidgetHelper.updateWidgets(getApplicationContext(), false); } // This is the old onStart method that will be called on the pre-2.0 // platform. @Override public void onStart(Intent intent, int startId) { log.d("onStart()"); super.onStart(intent, startId); handleCommand(intent); } private void handleCommand(Intent intent) { // notify the widgets that we're running WidgetHelper.updateWidgets(getApplicationContext()); CharSequence tickerText = getText(R.string.notification_ticker); // Set the icon, scrolling text and timestamp Notification notification = new Notification(R.drawable.status_icon, tickerText, System.currentTimeMillis()); Intent stopRecordingIntent = new Intent(); stopRecordingIntent.setAction(ACTION_STOP_RECORDING); // have to make this unique for God knows what reason stopRecordingIntent.setData(Uri.withAppendedPath(Uri.parse(URI_SCHEME + "://stop/"), Long.toHexString(new Random().nextLong()))); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0 /* no requestCode */, stopRecordingIntent, PendingIntent.FLAG_ONE_SHOT); // Set the info for the views that show in the notification panel. notification.setLatestEventInfo(this, getText(R.string.notification_title), getText(R.string.notification_subtext), pendingIntent); startForegroundCompat(R.string.notification_title, notification); } /** * This is a wrapper around the new startForeground method, using the older * APIs if it is not available. */ private void startForegroundCompat(int id, Notification notification) { // If we have the new startForeground API, then use it. if (mStartForeground != null) { try { mStartForeground.invoke(this, Integer.valueOf(id), notification); } catch (InvocationTargetException e) { // Should not happen. log.d(e, "Unable to invoke startForeground"); } catch (IllegalAccessException e) { // Should not happen. log.d(e, "Unable to invoke startForeground"); } return; } // Fall back on the old API. if (mSetForeground != null) { try { mSetForeground.invoke(this, Boolean.TRUE); } catch (IllegalAccessException e) { // Should not happen. log.d(e, "Unable to invoke setForeground"); } catch (InvocationTargetException e) { // Should not happen. log.d(e, "Unable to invoke setForeground"); } } mNM.notify(id, notification); } /** * This is a wrapper around the new stopForeground method, using the older * APIs if it is not available. */ private void stopForegroundCompat(int id) { // If we have the new stopForeground API, then use it. if (mStopForeground != null) { try { mStopForeground.invoke(this, Boolean.TRUE); } catch (InvocationTargetException e) { // Should not happen. log.d(e, "Unable to invoke stopForeground"); } catch (IllegalAccessException e) { // Should not happen. log.d(e, "Unable to invoke stopForeground"); } return; } // Fall back on the old API. Note to cancel BEFORE changing the // foreground state, since we could be killed at that point. mNM.cancel(id); if (mSetForeground != null) { try { mSetForeground.invoke(this, Boolean.FALSE); } catch (IllegalAccessException e) { // Should not happen. log.d(e, "Unable to invoke setForeground"); } catch (InvocationTargetException e) { // Should not happen. log.d(e, "Unable to invoke setForeground"); } } } protected void onHandleIntent(Intent intent) { log.d("onHandleIntent()"); handleIntent(intent); } private void handleIntent(Intent intent) { log.d("Starting up %s now with intent: %s", LogcatRecordingService.class.getSimpleName(), intent); String filename = intent.getStringExtra(EXTRA_FILENAME); String queryText = intent.getStringExtra(EXTRA_QUERY_FILTER); String logLevel = intent.getStringExtra(EXTRA_LEVEL); SearchCriteria searchCriteria = new SearchCriteria(queryText); CharSequence[] logLevels = getResources().getStringArray(R.array.log_levels_values); int logLevelLimit = ArrayUtil.indexOf(logLevels, logLevel); boolean searchCriteriaWillAlwaysMatch = searchCriteria.isEmpty(); boolean logLevelAcceptsEverything = logLevelLimit == 0; SaveLogHelper.deleteLogIfExists(filename); initializeReader(intent); StringBuilder stringBuilder = new StringBuilder(); try { String line; int lineCount = 0; int logLinePeriod = PreferenceHelper.getLogLinePeriodPreference(getApplicationContext()); while ((line = reader.readLine()) != null && !killed) { // filter if (!searchCriteriaWillAlwaysMatch || !logLevelAcceptsEverything) { if (!checkLogLine(line, searchCriteria, logLevelLimit)) { continue; } } stringBuilder.append(line).append("\n"); if (++lineCount % logLinePeriod == 0) { // avoid OutOfMemoryErrors; flush now SaveLogHelper.saveLog(stringBuilder, filename); stringBuilder.delete(0, stringBuilder.length()); // clear } } } catch (IOException e) { log.e(e, "unexpected exception"); } finally { killProcess(); log.d("CatlogService ended"); boolean logSaved = SaveLogHelper.saveLog(stringBuilder, filename); if (logSaved) { makeToast(R.string.log_saved, Toast.LENGTH_SHORT); startLogcatActivityToViewSavedFile(filename); } else { makeToast(R.string.unable_to_save_log, Toast.LENGTH_LONG); } } } private boolean checkLogLine(String line, SearchCriteria searchCriteria, int logLevelLimit) { LogLine logLine = LogLine.newLogLine(line, false); return searchCriteria.matches(logLine) && LogLineAdapterUtil.logLevelIsAcceptableGivenLogLevelLimit(logLine.getLogLevel(), logLevelLimit); } private void startLogcatActivityToViewSavedFile(String filename) { // start up the logcat activity if necessary and show the saved file Intent targetIntent = new Intent(getApplicationContext(), LogcatActivity.class); targetIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); targetIntent.setAction(Intent.ACTION_MAIN); targetIntent.putExtra("filename", filename); startActivity(targetIntent); } private void makeToast(final int stringResId, final int toastLength) { handler.post(new Runnable() { @Override public void run() { Toast.makeText(LogcatRecordingService.this, stringResId, toastLength).show(); } }); } private void killProcess() { if (!killed) { synchronized (lock) { if (!killed && reader != null) { // kill the logcat process reader.killQuietly(); killed = true; } } } } }